├── .gitignore
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
└── app
├── __init__.py
├── main.py
├── requirements.txt
└── routes
├── __init__.py
├── analytics.py
├── auth.py
├── filters.py
├── helpers.py
├── model.py
├── settings.py
├── templates.py
└── templates
├── chart.js
├── index.html
├── login.html
└── session.html
/.gitignore:
--------------------------------------------------------------------------------
1 | res.json
2 | .env
3 | .deta/
4 | *.pyc
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Athul Cyriac Ajay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 | black = "*"
8 | pylint = "*"
9 |
10 | [packages]
11 |
12 | [requires]
13 | python_version = "3.7"
14 |
15 | [pipenv]
16 | allow_prereleases = true
17 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "7035a678aba981099f32c53d00612934d916ac834a5e89312db3dda2cc3b1c80"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.7"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {},
19 | "develop": {
20 | "appdirs": {
21 | "hashes": [
22 | "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
23 | "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
24 | ],
25 | "version": "==1.4.4"
26 | },
27 | "astroid": {
28 | "hashes": [
29 | "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703",
30 | "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"
31 | ],
32 | "version": "==2.4.2"
33 | },
34 | "black": {
35 | "hashes": [
36 | "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
37 | ],
38 | "index": "pypi",
39 | "version": "==20.8b1"
40 | },
41 | "click": {
42 | "hashes": [
43 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
44 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
45 | ],
46 | "version": "==7.1.2"
47 | },
48 | "isort": {
49 | "hashes": [
50 | "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7",
51 | "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"
52 | ],
53 | "version": "==5.6.4"
54 | },
55 | "lazy-object-proxy": {
56 | "hashes": [
57 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d",
58 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449",
59 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08",
60 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a",
61 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50",
62 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd",
63 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239",
64 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb",
65 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea",
66 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e",
67 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156",
68 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142",
69 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442",
70 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62",
71 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db",
72 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531",
73 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383",
74 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a",
75 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357",
76 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
77 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
78 | ],
79 | "version": "==1.4.3"
80 | },
81 | "mccabe": {
82 | "hashes": [
83 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
84 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
85 | ],
86 | "version": "==0.6.1"
87 | },
88 | "mypy-extensions": {
89 | "hashes": [
90 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
91 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
92 | ],
93 | "version": "==0.4.3"
94 | },
95 | "pathspec": {
96 | "hashes": [
97 | "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd",
98 | "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"
99 | ],
100 | "version": "==0.8.1"
101 | },
102 | "pylint": {
103 | "hashes": [
104 | "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210",
105 | "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"
106 | ],
107 | "index": "pypi",
108 | "version": "==2.6.0"
109 | },
110 | "regex": {
111 | "hashes": [
112 | "sha256:064d2fc83ab4ee0055fcc1ef38ec60e505742850a40061f854ac64cb3d8d6dd3",
113 | "sha256:0951c78fa4cb26d1278a4b3784fcf973fc97ec39c07483328a74b034b0cc569c",
114 | "sha256:0a235841237d4487329bcabcb5b902858f7967f5e684e08e968367f25b2c3d37",
115 | "sha256:11d9100bd874ce8b2a037db9150e732cd768359fc25fe5f77973208aa24eb13e",
116 | "sha256:19ac2bf0048a2f4d460ee20647e84ca160512a7ee8af844dc9207720778470f1",
117 | "sha256:267d1b13f863e664150948ce2a9ed4927bf4ac7a068780f1ee8af83352aa17a2",
118 | "sha256:3002ee2d4e8bbe4656237627203d8290a562d1fc1962deee470905ab63570345",
119 | "sha256:32f8714c4bcc4b0d2aa259b1647e3c5b6cfe2e923c6c124234a5e03408224227",
120 | "sha256:394b5be4fa72354a78763b317f82997ad881896dd4a860e429a6fa74afaacb07",
121 | "sha256:396411bb5a7849aeda9c49873b8295919fdc118c50b57122b09cb2097047c118",
122 | "sha256:3b46a4c73ec1f25361147a7a0fd86084f3627dc78d09bcbe14e70db12683efec",
123 | "sha256:412969d58ecd4f576510ec88bcb7602e9e582bbef78859ed8c9ca4de4f9e891c",
124 | "sha256:4159ecf20dffea07f4a7241b2a236f90eb622c7e8caab9f43caba5f27ca37284",
125 | "sha256:48e94218f06317b6d32feb4ecff8b6025695450009bcb3291fb23daf79689431",
126 | "sha256:56d1e298bb6482d0466399a6383181bf2627c37ad414e205b3ce0f85aa140be7",
127 | "sha256:68267a7a5fb0bd9676b86f967143b6a6ecefb3eed4042ecc9e7f0e014aef8f74",
128 | "sha256:6d128368def4b0cd95c0fc9d99a89ae73c083b25e67f27a410830e30f9df0edc",
129 | "sha256:6e50b3b417ab2fd67bfa6235f0df4782fe2ff8be83f0c4435e1dc43d25052ee8",
130 | "sha256:787e44e5f4fd027dd90b5ee0240b05dc1752cb43c2903617f25baa495fe551e9",
131 | "sha256:8060be04baec546fe3afa6975d2998e15d1b655d7255f0e6b0ed3f482cccc218",
132 | "sha256:826d0119f14f9a9ce25999a13ed5922c785b50e469800f6e5a6721318650ef49",
133 | "sha256:83a390a653c13be1ab26287240df1fd9324ca8a0d31b603fa57cd7d9520648fa",
134 | "sha256:84ab584dcb5e81815040d86148805a808acb0bee303d19638fe2f9488d704bc1",
135 | "sha256:86ad88c7c2512094a85b0a01ce053bab1e28eafb8f3868bb8c22f4903e33f147",
136 | "sha256:8cc3717146ce4040419639cf45455663a002a554806ddac46304acc5bd41dae2",
137 | "sha256:9e8b3187f6beea8e56cb4b33c35049cbe376cf69aefaee5bc035309d88c98ca5",
138 | "sha256:a9f76d9122359b09e38f27cd9c41729169171cf0fd73ec5b22cc4628f9e486ca",
139 | "sha256:bb17a7fe9c47167337009ce18cd6e6b3edf3ca0063bf6bed6ce02515129c016a",
140 | "sha256:beae9db1545f8116cfc9301a9601e9c975bb56ca22a38ac0fe06a72c3460f31a",
141 | "sha256:bf02ab95ff5261ba108725dbd795bf6395eaac1b8468b41472d82d35b12b0295",
142 | "sha256:c67fd5f3ad81f8301184354014e8e7510ab77e0c7e450a427d77f28ae8effbef",
143 | "sha256:c8b1ad791debd67221fb1266f8d09730ae927acacb32d0dad9fd07a7d341a28f",
144 | "sha256:ccfea4911ac28a8f744096bce1559e0bd86b09a53c8a9d5856ca8e1f5f4de1f5",
145 | "sha256:cdb98be55db1b94c950822cbc10d3d768f01e184365851ebb42cd377486ced7b",
146 | "sha256:cefcdb2ac3b67fd9f7244820ce1965c8cf352366199cc1358d67c6cc3c5c8bbc",
147 | "sha256:d1e57c16c4840f1c3543507742e99b8398609474a0e6a6925476914479de3488",
148 | "sha256:dd7bee615680d940dd44ac0a479f2bc5f73d6ca63a5915cd8d30739c14ca522c",
149 | "sha256:df50ba964812606663ca9d23d374036bc5ae3d71e86168409cdd84ca7948d8a3",
150 | "sha256:e03867f3baf64ecab47dfc9ddb58afc67acb6a0f80f6cf8ff9fa82962ec4d1cd",
151 | "sha256:e7cdd5ee8053c82607432b7ebad37e2ece54548fef2b254f7bce6f7831904586",
152 | "sha256:e899b69dd5d26655cb454835ea2fceb18832c9ee9c4fb45dc4cf8a6089d35312"
153 | ],
154 | "version": "==2020.11.11"
155 | },
156 | "six": {
157 | "hashes": [
158 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
159 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
160 | ],
161 | "version": "==1.15.0"
162 | },
163 | "toml": {
164 | "hashes": [
165 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
166 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
167 | ],
168 | "version": "==0.10.2"
169 | },
170 | "typed-ast": {
171 | "hashes": [
172 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
173 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
174 | "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
175 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
176 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
177 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
178 | "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
179 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
180 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
181 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
182 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
183 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
184 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
185 | "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
186 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
187 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
188 | "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
189 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
190 | "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
191 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
192 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
193 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
194 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
195 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
196 | "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
197 | "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
198 | "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
199 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
200 | "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
201 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
202 | ],
203 | "markers": "implementation_name == 'cpython' and python_version < '3.8'",
204 | "version": "==1.4.1"
205 | },
206 | "typing-extensions": {
207 | "hashes": [
208 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
209 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
210 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
211 | ],
212 | "version": "==3.7.4.3"
213 | },
214 | "wrapt": {
215 | "hashes": [
216 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
217 | ],
218 | "version": "==1.12.1"
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IMPORTANT: There seems to be an error in deploying this, planning for a rewrite of the whole.
2 |
3 | # Jimbru
4 |
5 | Jimbru is an Privacy Oriented web analytics Server which is built with FastAPI and Deta Base as DB.
6 |
7 | > Jimbru is heavily inspired by [Shynet](https://github.com/milesmcc/shynet) which is also an Analytics server built with Django.
8 |
9 |
10 | **Not for Production Level use. It's hacky to the core. I made it for a Personal Use and doesn't have any fancy features.**
11 |
12 |
13 | ## Features
14 |
15 | - Lightweight
16 | - Privacy Oriented
17 | - Easily Deployable (on deta.sh)
18 | - 6 lines of JS code gets the current URL, referrer and load time.
19 | - Charts with [Frappe Charts](https://frappe.io/charts) 📊
20 | - Jinja2 Templating and Tailwind CSS for Frontend
21 | - Cookie based Authentication
22 | - User OS and device from `user-agent` header
23 | - user location and network from user ip header
24 |
25 | ## Not Included Features
26 | - Caching
27 | - Bounce rate
28 | - Session Time
29 | - Unique Hits
30 |
31 | ## Demo
32 |
33 | 
34 |
35 | ## Deploying
36 |
37 | Before deploying you need to get some Credentials
38 |
39 | ### Prep Work
40 | - Signup for an account in https://deta.sh
41 | - Create a new Project
42 | - Get the Project Key and save it in the `.env` file as below
43 | - [Install](https://docs.deta.sh/docs/cli/install) the Deta cli. This is for deploying to Deta.
44 | - Create a `.env` file inside the `app/routes` directory with the following keys
45 |
46 | ```env
47 | TITLE=
48 | DOMAIN=
49 | PKEY=
50 | PNAME=
51 | SECRET_JWT=
52 | USERNAME=
53 | PASSWORD=
54 | ```
55 |
56 | > The `PNAME` can be anything.
57 |
58 | ---
59 |
60 | 1. Fork/Clone this Repository
61 | 2. You can run the code locally by installing the dependencies inside the `app` directory and running `uvicorn main:app --reload` inside the `app` directory.
62 | 3. Inside the `app` directory you can create a new [Micro](https://docs.deta.sh/docs/micros/about) with `$ deta new`. This will create a new Micro. You will get the domian from the output of the command. Save that domain in the `DOMAIN` key in the `.env` file, without a trailing `/`.
63 | 4. Run `$ deta update -e routes/.env` to update the environment variables in the micro.
64 | 5. Run `$ deta deploy` inside the app directory and the code will be deployed.
65 | 6. Profit
66 |
67 | ---
68 |
69 | ## Usage
70 |
71 | Add a `
75 | ```
76 |
77 | ## TODO
78 |
79 | - [ ] Caching
80 | - [ ] Better Auth
81 | - [ ] Session Time
82 |
83 | ## LICENSE
84 |
85 | **MIT**
86 |
87 | ## Contributors
88 |
89 | - [@akhilmhdh](https://github.com/akhilmhdh)
90 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/athul/jimbru/bc22449dbfbea19d9605e6271a154dbc7037bafb/app/__init__.py
--------------------------------------------------------------------------------
/app/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | try:
3 | from routes import analytics,templates,auth
4 | except:
5 | from .routes import analytics,templates,auth
6 |
7 |
8 | app = FastAPI()
9 |
10 | app.include_router(analytics.router)
11 | app.include_router(templates.router)
12 | app.include_router(auth.authr)
--------------------------------------------------------------------------------
/app/requirements.txt:
--------------------------------------------------------------------------------
1 | aiofiles==0.6.0
2 | aiohttp==3.7.2
3 | astroid==2.4.2
4 | async-timeout==3.0.1
5 | autopep8==1.5.4
6 | certifi==2020.11.8
7 | chardet==3.0.4
8 | click==7.1.2
9 | deta==0.7
10 | emoji-country-flag==1.2.3
11 | fastapi==0.61.2
12 | fastapi-login==1.5.1
13 | h11==0.11.0
14 | httpcore==0.12.1
15 | idna==2.10
16 | isort==5.6.4
17 | Jinja2==2.11.2
18 | lazy-object-proxy==1.4.3
19 | MarkupSafe==1.1.1
20 | mccabe==0.6.1
21 | multidict==5.0.0
22 | mypy-extensions==0.4.3
23 | passlib==1.7.4
24 | pycodestyle==2.6.0
25 | pydantic==1.7.2
26 | PyJWT==1.7.1
27 | pylint==2.6.0
28 | python-dateutil==2.8.1
29 | python-dotenv==0.15.0
30 | python-multipart==0.0.5
31 | pytz==2020.4
32 | PyYAML==5.3.1
33 | requests==2.25.0
34 | rfc3986==1.4.0
35 | six==1.15.0
36 | sniffio==1.2.0
37 | starlette==0.13.6
38 | toml==0.10.2
39 | typed-ast==1.4.1
40 | typing-extensions==3.7.4.3
41 | ua-parser==0.10.0
42 | urllib3==1.26.2
43 | user-agents==2.2.0
44 | uvicorn==0.12.2
45 | wrapt==1.12.1
46 | yarl==1.6.2
47 |
--------------------------------------------------------------------------------
/app/routes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/athul/jimbru/bc22449dbfbea19d9605e6271a154dbc7037bafb/app/routes/__init__.py
--------------------------------------------------------------------------------
/app/routes/analytics.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request, Response
2 | from base64 import b64decode
3 | from .helpers import pushtoDB
4 | from .settings import domain, title
5 |
6 | router = APIRouter()
7 |
8 | BEACON: str = b64decode(
9 | "R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==")
10 |
11 | DOMAIN: str = domain
12 | TITLE: str = title
13 | JS: str = """(function () {
14 | window.addEventListener('load', () => {
15 | i = new Image;
16 | i.src = '%s/a.gif?url=' + encodeURIComponent(document.location.href) + '&ref=' + encodeURIComponent(document.referrer) + '<=' + encodeURIComponent(window.performance.timing.domContentLoadedEventEnd - window.performance.timing.navigationStart);
17 | })
18 | })()""".replace("\n", "")
19 |
20 |
21 | @router.get("/a.gif")
22 | def getImg(request: Request):
23 | pushtoDB(request)
24 | return Response(
25 | content=BEACON,
26 | media_type="img/gif",
27 | headers={"Cache-Control": "private, no-cache"},
28 | )
29 |
30 |
31 | @router.get("/a.js")
32 | def getJs():
33 | return Response(content=JS % DOMAIN, media_type="text/javascript")
34 |
--------------------------------------------------------------------------------
/app/routes/auth.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter,Depends,status
2 | from fastapi.responses import RedirectResponse,HTMLResponse
3 | from fastapi.security import OAuth2PasswordRequestForm
4 | from fastapi_login import LoginManager
5 | from .settings import SECRET,USERNAME,PASSWORD
6 | from fastapi_login.exceptions import InvalidCredentialsException
7 |
8 | authr = APIRouter()
9 |
10 | manager = LoginManager(SECRET,tokenUrl="/auth/login",use_cookie=True)
11 | manager.cookie_name = "jimbru"
12 |
13 | DB = {USERNAME:{"password":PASSWORD}}
14 |
15 | @manager.user_loader
16 | def load_user(username:str):
17 | user = DB.get(username)
18 | return user
19 |
20 | @authr.post("/auth/login")
21 | def login(data: OAuth2PasswordRequestForm = Depends()):
22 | username = data.username
23 | password = data.password
24 | user = load_user(username)
25 | if not user:
26 | raise InvalidCredentialsException
27 | elif password != user['password']:
28 | raise InvalidCredentialsException
29 |
30 | access_token = manager.create_access_token(
31 | data={"sub":username}
32 | )
33 | resp = RedirectResponse(url="/dash",status_code=status.HTTP_302_FOUND)
34 | manager.set_cookie(resp,access_token)
35 | return resp
36 |
37 | @authr.get("/private")
38 | def logged_in_users_only(_=Depends(manager)):
39 | return "You are logged In"
40 |
41 |
--------------------------------------------------------------------------------
/app/routes/filters.py:
--------------------------------------------------------------------------------
1 | import flag
2 | import datetime
3 |
4 |
5 | def flagize(value):
6 | try:
7 | return flag.flag(value)
8 | except:
9 | return ""
10 |
11 |
12 | def hmantime(value,sm:bool=False):
13 | dtime = datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f%z')
14 | if sm:
15 | return dtime.strftime("%I:%M %p")
16 | else:
17 | return dtime.strftime("%B %d %Y | %I:%M:%S %p")
18 |
19 | def getConutryCode(value):
20 | cn = value.split("|")
21 | return f"{flag.flag(cn[0])} {cn[1]}"
--------------------------------------------------------------------------------
/app/routes/helpers.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Tuple
2 | from fastapi import Request
3 | from datetime import datetime, timedelta
4 | from pytz import timezone
5 | from collections import Counter
6 | from .model import db, Pageviews, URL
7 | from os import path
8 | import requests
9 | from user_agents import parse
10 |
11 |
12 | pth = path.dirname(__file__)
13 |
14 |
15 | def getDatafromIP(ip):
16 | data = requests.get(f"https://ipapi.co/{ip}/json/").json()
17 | return data
18 |
19 |
20 | def getDeviceDetails(agent) -> str:
21 | ua = parse(agent)
22 | device_type = "Other 👽"
23 | browser = ua.browser.family or ""
24 | os = ua.os.family
25 | device = ua.device.family or ua.device.model or ""
26 | if (
27 | ua.is_bot
28 | or (ua.browser.family or "").strip().lower() == "googlebot"
29 | or (ua.device.family or ua.device.model or "").strip().lower()
30 | == "spider"
31 | ):
32 | device_type = "Robot 🤖"
33 | elif ua.is_mobile:
34 | device_type = "Phone 📱"
35 | elif ua.is_tablet:
36 | device_type = "Tablet ⬆️📱"
37 | elif ua.is_pc:
38 | device_type = "Desktop 🖥"
39 | return browser, device_type, device, os
40 |
41 |
42 |
43 | def pushtoDB(req: Request) -> Dict:
44 | """
45 | @args: Request class
46 | """
47 | url = URL(url=req.query_params["url"])
48 | now = datetime.now().astimezone(timezone("Asia/Kolkata"))
49 | referrer = req.query_params["ref"] or ""
50 | loadTime = req.query_params["lt"] or ""
51 | headers = dict(req.headers)
52 | bro, dev_type, dev, os = getDeviceDetails(headers['user-agent'])
53 | ipdata = getDatafromIP(headers["x-real-ip"])
54 | ipdata['model'] = dev
55 | ipdata['os'] = os
56 | ipdata['browser'] = bro
57 |
58 | data = Pageviews(
59 | url=f"{url.url.scheme}://{url.url.host}{url.url.path}",
60 | referrer=referrer,
61 | headers=headers,
62 | day=now.strftime("%Y/%m/%d"),
63 | time=str(now),
64 | ip=ipdata,
65 | ip_addr=headers["x-real-ip"] or "",
66 | hour=now.strftime("%H"),
67 | device_browser=bro,
68 | device=dev,
69 | device_type=dev_type,
70 | loadtime=loadTime,
71 | os=os,
72 | )
73 | if any(l in url.url.host for l in ["localhost", "127.0.0.1"]):
74 | return {"msg": "no-data pushed"}
75 | else:
76 | db.put(vars(data))
77 | return vars(data)
78 |
79 | def getAllthings(ip:bool=False,dayNeed:bool=False):
80 | data = next(db.fetch())
81 | refs = []
82 | urls = []
83 | hours = []
84 | iptime = []
85 | dev = []
86 | devtype = []
87 | browser = []
88 | loadTime = []
89 | countries = []
90 | urlfromIP = []
91 | days = {}
92 | today = datetime.now().astimezone(timezone("Asia/Kolkata"))
93 | for datum in data:
94 | refs.append(datum["referrer"])
95 | urls.append(datum["url"])
96 | hours.append(datum["hour"])
97 | iptime.append({datum["time"]: datum["ip"]})
98 | dev.append(datum['os'])
99 | browser.append(datum['device_browser'])
100 | devtype.append(datum['device_type'])
101 | loadTime.append(int(datum['loadtime']))
102 | countries.append(f'{datum["ip"]["country"]}|{datum["ip"]["country_name"]}')
103 | urlfromIP.append({datum['ip_addr']:{"time":datum['time'],"url":datum['url'],"ref":datum['referrer'],"ldt":datum['loadtime']}})
104 |
105 | for i in range(0, 30):
106 | day = (today - timedelta(days=i)).strftime("%Y/%m/%d")
107 | days[day]=0
108 | for d in data:
109 | if d.get('day') == day:
110 | days[day]+=1
111 | sortedDays = {k:v for k,v in sorted(days.items(), key=lambda item: item[0])}
112 | if dayNeed:
113 | return sortedDays
114 | if ip:
115 | return iptime,urlfromIP
116 |
117 | refListCleaned = list(Counter(refs).keys())
118 | for i in range(len(refListCleaned)):
119 | if refListCleaned[i] == "":
120 | refListCleaned[i] = "Direct"
121 | refListNos = list(Counter(refs).values())
122 | # ---
123 | urlListCleaned = list(Counter(urls).keys())
124 | urlHitNos = list(Counter(urls).values())
125 | # ---
126 | HourListCleaned = list(Counter(hours).keys())
127 | HourHitNos = list(Counter(hours).values())
128 | # ---
129 | countryCleaned = list(Counter(countries).keys())
130 | CountryNos = list(Counter(countries).values())
131 | # ---
132 | deviceList = list(Counter(dev).keys())
133 | deviceListNos = list(Counter(dev).values())
134 | # ---
135 | browserListCl = list(Counter(browser).keys())
136 | browserListNos = list(Counter(browser).values())
137 | # ---
138 | devtypeList = list(Counter(devtype).keys())
139 | devtypeNos = list(Counter(devtype).values())
140 | # ---
141 | browserDict = {browserListCl[i]: browserListNos[i]
142 | for i in range(len(browserListCl))}
143 | deviceDict = {deviceList[i]: deviceListNos[i]
144 | for i in range(len(deviceList))}
145 | refDict = {refListCleaned[i]: refListNos[i]
146 | for i in range(len(refListCleaned))}
147 | urlHitDict = {urlListCleaned[i]: urlHitNos[i]
148 | for i in range(len(urlListCleaned))}
149 | devtypeDict = {devtypeList[i]: devtypeNos[i]
150 | for i in range(len(devtypeList))}
151 | countryDict = {countryCleaned[i]:CountryNos[i] for i in range(len(countryCleaned))}
152 | # ---
153 | browserSortDict = {
154 | k: v for k, v in sorted(browserDict.items(), key=lambda item: item[1], reverse=True)
155 | }
156 | devtypeSortDict ={
157 | k: v for k, v in sorted(devtypeDict.items(), key=lambda item: item[1], reverse=True)
158 | }
159 | refSortDict = {
160 | k: v for k, v in sorted(refDict.items(), key=lambda item: item[1], reverse=True)
161 | }
162 | deviceSortDict = {
163 | k: v for k, v in sorted(deviceDict.items(), key=lambda item: item[1], reverse=True)
164 | }
165 | urlHitSortDict = {
166 | k: v
167 | for k, v in sorted(urlHitDict.items(), key=lambda item: item[1], reverse=True)
168 | }
169 | avgLoadTime = (sum(loadTime)/len(loadTime))
170 |
171 | return refSortDict, urlHitSortDict, HourListCleaned, HourHitNos, iptime, sum(urlHitNos), deviceSortDict, browserSortDict,devtypeSortDict,avgLoadTime,countryDict
172 |
--------------------------------------------------------------------------------
/app/routes/model.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, HttpUrl
2 | from typing import Optional
3 | from .settings import deta_key,deta_pname
4 | from deta import Deta
5 |
6 | db = Deta(deta_key).Base(deta_pname)
7 |
8 |
9 | class URL(BaseModel):
10 | url: HttpUrl
11 |
12 |
13 | class Pageviews(BaseModel):
14 | url: str
15 | referrer: str
16 | headers: dict
17 | ip: dict
18 | ip_addr:str
19 | day: str
20 | time: str
21 | device:str
22 | device_browser:str
23 | device_type:str
24 | os:str
25 | loadtime:str
26 | hour: Optional[str]
27 |
--------------------------------------------------------------------------------
/app/routes/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dotenv import load_dotenv
3 | load_dotenv()
4 |
5 | title = os.getenv("TITLE")
6 | SECRET = os.getenv("SECRET_JWT")
7 | USERNAME = os.getenv("USERNAME")
8 | PASSWORD = os.getenv("PASSWORD")
9 | deta_key = os.getenv("PKEY")
10 | deta_pname = os.getenv("PNAME")
11 | domain = os.getenv("DOMAIN")
12 |
--------------------------------------------------------------------------------
/app/routes/templates.py:
--------------------------------------------------------------------------------
1 | from .analytics import router
2 | from .helpers import getAllthings
3 | from .filters import flagize,hmantime,getConutryCode
4 | from datetime import datetime
5 | from jinja2 import Template
6 | from .analytics import TITLE
7 | from .auth import manager
8 | from fastapi.responses import HTMLResponse
9 | from fastapi.templating import Jinja2Templates
10 | from fastapi import APIRouter, Request,Depends
11 | from os import path
12 |
13 | router = APIRouter()
14 |
15 | pth = path.dirname(__file__)
16 | templates = Jinja2Templates(directory=path.join(pth, "templates"))
17 | templates.env.filters['flagize'] = flagize
18 | templates.env.filters['dateit'] = hmantime
19 | templates.env.filters['getctCode'] = getConutryCode
20 |
21 |
22 | @router.get("/dash",response_class=HTMLResponse)
23 | def renderIndex(request: Request,user=Depends(manager)):
24 | js = Template(open(path.join(pth, "templates/chart.js")).read())
25 | dayHits = getAllthings(dayNeed=True)
26 | refs, hiturls, hours, hhits, iptime,totHits,os,browsers,dev,lt,ctDict = getAllthings()
27 | ipSorted = sorted(iptime, key=lambda k: sorted(k.keys()), reverse=True)
28 |
29 | return templates.TemplateResponse("index.html", {
30 | "request": request,
31 | "title": TITLE,
32 | "loadTime":lt,
33 | "tothits":totHits,
34 | "refs": refs,
35 | "urls": hiturls,
36 | "os": os,
37 | "dev":dev,
38 | "cflg": ipSorted,
39 | "browser":browsers,
40 | "time ": ipSorted,
41 | "countries":ctDict,
42 | "chart": js.render(
43 | hitarr=dayHits, hours=hours, hhits=hhits, os=os
44 | ),
45 | },
46 | )
47 |
48 | # return nw
49 | @router.get("/sess/{time}",response_class=HTMLResponse)
50 | def getVisitorDetails(req:Request,time:str,user=Depends(manager)):
51 | ip,urlsIP = getAllthings(True)
52 | trdict =[]
53 | ipSorted = sorted(ip, key=lambda k: sorted(k.keys()), reverse=True)
54 | urlSorted = sorted(urlsIP, key=lambda k: sorted(k.keys()), reverse=True)
55 | for data in ipSorted:
56 | if data.get(time):
57 | for udict in urlSorted:
58 | if udict.get(data[time]['ip']):
59 | trdict.append(udict)
60 | trSortdict = sorted(trdict,key=lambda key:key.get(data[time]['ip'])['time'],reverse=True)
61 | return templates.TemplateResponse("session.html",{"request": req,"ipdata":data,"hitdata":trSortdict})
62 |
63 | @router.get("/",response_class=HTMLResponse)
64 | def loginwithCreds(request:Request):
65 | with open(path.join(pth, "templates/login.html")) as f:
66 | return HTMLResponse(content=f.read())
--------------------------------------------------------------------------------
/app/routes/templates/chart.js:
--------------------------------------------------------------------------------
1 | var myChart = new frappe.Chart("#myChart", {
2 | type: 'line',
3 | title: "Hits per Day",
4 | colors: ['purple'],
5 | height:400,
6 | data: {
7 | labels: {{ hitarr.keys()|list| safe}},
8 | datasets: [
9 | {
10 | values: {{ hitarr.values()|list| safe}}
11 | },
12 | ]},
13 | axisOptions: {
14 | yAxisMode: "tick",
15 |
16 | },
17 | lineOptions: {
18 | hideDots: 1,
19 |
20 | regionFill: 1, // default: 0
21 | },
22 | });
23 | var hourPie = new frappe.Chart("#hourpie", {
24 | type: 'pie',
25 | title:"Hourly Hits",
26 | height:300,
27 | data: {
28 | labels: {{ hours| safe}},
29 | datasets: [
30 | {
31 | values: {{ hhits| tojson}},
32 | },
33 | ]},
34 | });
35 | var osPie = new frappe.Chart("#ospie", {
36 | type: 'donut',
37 | height:300,
38 | title:"Operating Systems",
39 | data: {
40 | labels: {{ os.keys() | list }},
41 | datasets: [{
42 | values: {{ os.values() | list }},
43 | }],
44 | },
45 | });
--------------------------------------------------------------------------------
/app/routes/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Jimbru Analytics for {{title}}
7 |
8 |
9 |
13 |
17 |
18 |
19 |
20 |
21 |
22 | Jimbru Analytics for {{title}}
23 |
24 |
25 |
29 |
30 |
Hits
31 |
{{tothits}}
32 |
All Time
33 |
34 |
35 |
Avg Load Time
36 |
{{loadTime|round|int}}
37 |
ms
38 |
39 |
40 |
45 |
57 |
58 |
97 |
135 |
173 |
202 |
203 |
204 |
207 |
214 |
215 |
--------------------------------------------------------------------------------
/app/routes/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Login
7 |
11 |
12 |
13 |
14 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/routes/templates/session.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Session
7 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ⬅ Back
24 |
25 | {% for key,value in ipdata.items()%}
26 |
29 |
30 |
31 |
32 | Time
33 |
34 |
35 | {{key|dateit}}
36 |
37 |
38 |
39 |
40 | Browser
41 |
42 |
43 | {{value['browser']}}
44 |
45 |
46 |
47 |
48 | OS
49 |
50 |
51 | {{value['os']}}
52 |
53 |
54 |
55 |
56 | Location
57 |
58 |
62 |
63 |
64 |
65 | Device
66 |
67 |
68 | {{value['model']}}
69 |
70 |
71 |
72 |
73 |
74 |
75 | Network
76 |
77 |
78 | {{value['org']}}
79 |
80 |
81 |
82 |
83 | Country
84 |
85 |
86 | {{value['country_code']|flagize}} {{value['country_name']}}
87 |
88 |
89 |
90 | {% endfor %}
91 |
92 | {% for hdict in hitdata %}{% for key,value in hdict.items() %}
93 |
94 |
95 |
{{value['time']|dateit(True)}}
96 |
97 |
98 |
99 | {{value['url']}}
100 |
101 |
102 | {% if value['ref']|length %}via {{value['ref']}} {%else%} Direct {%endif%}
103 |
104 |
105 |
106 |
107 |
108 | Load
109 |
110 |
111 | {{value['ldt']}}ms
112 |
113 |
114 |
115 |
116 |
117 |
118 | {% endfor %} {% endfor %}
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------