├── .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 | ![Demo Gif](https://user-images.githubusercontent.com/40897573/99864902-1fb1c980-2bcc-11eb-81e6-49380d36d263.gif) 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=<domain of the deployed server> 49 | PKEY=<Deta Project Key> 50 | PNAME=<Deta Base DB Name> 51 | SECRET_JWT=<Secret for JWT. Get than with→ import os; print(os.urandom(24).hex())> 52 | USERNAME=<username for authenticating> 53 | PASSWORD=<password for authenticating> 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 `<script>` tag for the site 72 | 73 | ```html 74 | <script src="https://<DOMAIN>/a.js" type="text/javascript"></script> 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 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 | <title>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 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {% for key,value in urls.items() %} 69 | 70 | 71 | 72 | 73 | {% endfor %} 74 | 75 |
Visited URLVisited Count
{{key}}{{value}}
76 |
77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {% for key,value in refs.items() %} 88 | 89 | 90 | 91 | 92 | {% endfor %} 93 | 94 |
Referrer URLReferrer Count
{{key}}{{value}}
95 |
96 |
97 |
98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {% for key,value in os.items() %} 108 | 109 | 110 | 111 | 112 | {% endfor %} 113 | 114 |
Operating SystemCount
{{key}}{{value}}
115 |
116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | {% for key,value in browser.items() %} 126 | 127 | 128 | 129 | 130 | {% endfor %} 131 | 132 |
Broswer TypesCount
{{key}}{{value}}
133 |
134 |
135 |
136 |
137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | {% for key,value in dev.items() %} 146 | 147 | 148 | 149 | 150 | {% endfor %} 151 | 152 |
Device TypesCount
{{key}}{{value}}
153 |
154 |
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | {% for key,value in countries.items() %} 164 | 165 | 166 | 167 | 168 | {% endfor %} 169 | 170 |
CountriesSessions
{{key|getctCode}}{{value}}
171 |
172 |
173 |
174 |
175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | {% for os in cflg %} {% for 187 | key,value in os.items()%} 188 | 189 | 190 | 191 | 194 | 195 | 196 | 197 | {% endfor %} {% endfor %} 198 | 199 |
TimeDeviceCountryProviderCity
{{key|dateit}}{{value['model']}} 192 | {{value['country_code']|flagize}} {{value['country_name']}} 193 | {{value['org']}}{{value['city']}}
200 |
201 |
202 |
203 | 204 | 207 | 214 | 215 | -------------------------------------------------------------------------------- /app/routes/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login 7 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 20 | 21 |
22 |
23 | 26 | 27 |
28 |
29 | 32 |
33 |
34 |
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 |
27 |
28 |
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 |
59 | Open 60 | in Maps ↗ 61 |
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 | --------------------------------------------------------------------------------