Compare commits

...

425 Commits

Author SHA1 Message Date
dependabot[bot]
06a7cbb0f1
Bump django from 4.1.10 to 4.1.13 (#298)
Bumps [django](https://github.com/django/django) from 4.1.10 to 4.1.13.
- [Commits](https://github.com/django/django/compare/4.1.10...4.1.13)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-03 15:43:09 -07:00
R. Miles McCain
24e0ff0129 Use new MaxMind license key 2023-10-27 14:10:40 -07:00
Peter Dave Hello
45fafc3507
Add basic zh_TW Traditional Chinese locale (#290) 2023-09-25 08:48:52 -07:00
R. Miles McCain
9e0c92d703
Remove broker transport options (did not work) 2023-09-25 08:40:24 -07:00
Sumit Singh
03f8cbfe7b
Add tests for DashboardApiView and ApiTokenRequiredMixin (#230)
* test(core): fix factories

* test(api): Add tests for DashboardApiView and ApiTokenRequiredMixin

* refactor(api): sort imports and ran black on api app
2023-09-22 23:21:29 -04:00
Peter Dave Hello
120ea02fde
Remove additional clean up steps in Dockerfile (#283)
`/var/lib/apt/lists/*` is `/var/cache/apk/*` for Debian/Ubuntu based apt and Alpine Linux apk, if Alpine Linux is using `apk` with `--no-cache`, then you don't need any additional steps to clean up these two path.
2023-09-22 23:17:24 -04:00
Kashall
cc16271683
Update docker build & push to use latest versions as well as push to GHCR.IO (#286)
* chore(`workflows`): update docker build dependencies and support ghcr.io

* chore(`workflows`): update docker/build-push-action to v5
2023-09-22 23:01:17 -04:00
dependabot[bot]
78a47e3a74
Bump cryptography from 41.0.3 to 41.0.4 (#288)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.3 to 41.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.3...41.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-22 22:58:58 -04:00
Kashall
b4c2ebc0bb
chore(celery): add option for master_name that goes here (#287)
* chore(`celery`): add option for master_name that goes here
2023-09-22 22:55:57 -04:00
dependabot[bot]
55112fac88
Bump certifi from 2022.12.7 to 2023.7.22 (#280)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 15:21:29 -07:00
dependabot[bot]
4f2bf7a64e
Bump aiohttp from 3.8.1 to 3.8.5 (#281)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.1 to 3.8.5.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/v3.8.5/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.1...v3.8.5)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 15:21:21 -07:00
dependabot[bot]
dfcbea56b8
Bump cryptography from 41.0.0 to 41.0.3 (#282)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.0 to 41.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.0...41.0.3)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 15:21:14 -07:00
dependabot[bot]
f87c2c9d50
Bump word-wrap from 1.2.3 to 1.2.4 (#273)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-29 12:12:48 -07:00
Jaryl Chng
4cc2dd4b54
Optimized Dockerfile to reduce uncompressed image size (#276)
* Optimized Dockerfile to reduce uncompressed image size

Combining apk del commands in the same layer as their add commands shaves off ~270MB when uncompressed.

* Skip libffi-dev rust cargo installation on x86_64
2023-08-29 12:12:32 -07:00
radeeyate
f1a0de2090
Update helpers.py (#278) 2023-08-09 16:11:03 -07:00
R. Miles McCain
1918af3f0e Bump version to 0.13.1 2023-07-28 03:16:05 +00:00
R. Miles McCain
f958129d09 Add additional build dependencies 2023-07-28 03:15:27 +00:00
R. Miles McCain
45735e7908 Add base dev container images 2023-07-28 03:07:50 +00:00
Jeremy Chabernaud
4d9a5fdaad
fix: PyYAML dependency install (check #183) (temporary) (#272)
* fix: PyYAML dependency install (check #183)

* misc: update gh-actions (versions, and add temporary patch)

* misc: revert to python 3.9
2023-07-18 07:45:05 -04:00
R. Miles McCain
ef32c66769 Bump version 2023-07-17 12:03:31 -04:00
dependabot[bot]
467c32657f
Bump django from 4.1.9 to 4.1.10 (#271)
Bumps [django](https://github.com/django/django) from 4.1.9 to 4.1.10.
- [Commits](https://github.com/django/django/compare/4.1.9...4.1.10)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 14:01:32 -07:00
rsp2k
b9ffb10668
Update TEMPLATE.env - add command to generate django secret (#270) 2023-06-12 10:59:26 -07:00
dependabot[bot]
9980cc12f2
Bump cryptography from 38.0.1 to 41.0.0 (#269)
Bumps [cryptography](https://github.com/pyca/cryptography) from 38.0.1 to 41.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/38.0.1...41.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 13:11:51 -07:00
dependabot[bot]
404a355c54
Bump requests from 2.28.1 to 2.31.0 (#268)
Bumps [requests](https://github.com/psf/requests) from 2.28.1 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.1...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 07:46:59 -07:00
havk
6cd23029a9
Fix performence in dashboard (#264)
* Limit results in dashboard

* Add service loction list view

* Increase RESULTS_LIMIT
2023-05-19 09:59:33 -07:00
dependabot[bot]
b54d3c64d5
Bump django from 4.1.7 to 4.1.9 (#267)
Bumps [django](https://github.com/django/django) from 4.1.7 to 4.1.9.
- [Commits](https://github.com/django/django/compare/4.1.7...4.1.9)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-11 11:58:22 -07:00
dependabot[bot]
d71f389b7e
Bump sqlparse from 0.4.2 to 0.4.4 (#265)
Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.2 to 0.4.4.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.2...0.4.4)

---
updated-dependencies:
- dependency-name: sqlparse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-21 15:43:20 -07:00
havk
4ffc3bdef7
Respect dnt in JS tracker (#257) 2023-02-23 13:39:26 -08:00
dependabot[bot]
d78fd9f6c5
Bump django from 4.1.6 to 4.1.7 (#256)
Bumps [django](https://github.com/django/django) from 4.1.6 to 4.1.7.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.1.6...4.1.7)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 13:09:24 -08:00
dependabot[bot]
f6bbb712e3
Bump django from 4.1.1 to 4.1.6 (#252)
Bumps [django](https://github.com/django/django) from 4.1.1 to 4.1.6.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.1.1...4.1.6)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-14 07:23:38 -08:00
0xflotus
b7fd2d16a1
fix: small typo (#250)
* fix: small typo

* Update GUIDE.md
2023-01-30 09:37:39 -08:00
Sérgio
7c5888179c
Add fallback to PORT in healthcheck (#249) 2023-01-20 07:01:18 -08:00
dependabot[bot]
2d7bbcaa8b
Bump certifi from 2022.6.15.1 to 2022.12.7 (#244)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.6.15.1 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2022.06.15.1...2022.12.07)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-12 23:32:11 -05:00
havk
b397d1fd6a
Add api token field to UserAdmin (#241) 2022-11-07 08:52:13 -08:00
havk
2de0b9b9e1
Update poetry version in Dockerfile (#240) 2022-11-07 08:50:12 -08:00
Tristan Pinaudeau
aeeff26e88
fix registeradmin's get_random_string missing argument (#236) 2022-10-20 11:11:38 -07:00
havk
ba2f47edb7
Add Black config and pre-commit hook (#231)
* Add black config to pyproject.toml

* Add flake8 config

* Add pre-commit black hook
2022-09-28 16:16:40 -07:00
Sumit Singh
e507dd5814
test(core): fix factories (#229) 2022-09-25 13:48:03 -07:00
R. Miles McCain
c34388e6c9 Fix migrations 2022-09-15 22:24:29 -07:00
R. Miles McCain
767fd49969 Update migrations 2022-09-15 22:08:28 -07:00
R. Miles McCain
f746bce100 Update TEMPLATE.env 2022-09-14 11:12:04 -07:00
R. Miles McCain
2715826611 Add proper CSRF origin parsing 2022-09-14 11:09:17 -07:00
R. Miles McCain
e7fef3b2f8 Add option to set CSRF_TRUSTED_ORIGINS (Django 4.0) 2022-09-14 11:04:47 -07:00
Christian Wiegand
e08c6e790b
Add missing "i18n" to some templates (#226) 2022-09-14 10:32:14 -07:00
R. Miles McCain
78135583ee Fix migration order 2022-09-13 14:23:00 -07:00
R. Miles McCain
487815a984 Bump version to 0.13.0 2022-09-13 13:14:53 -07:00
R. Miles McCain
6f715b5b77 Update ipware (fix CF IPs) 2022-09-13 12:19:43 -07:00
R. Miles McCain
1280433a49 Add an API! 2022-09-13 12:14:39 -07:00
R. Miles McCain
6febe7db19 Upgrade Django to 4 2022-09-13 12:09:36 -07:00
Paweł Jastrzębski
5e48e2dcf5 Use POST to api token refresh 2022-08-29 08:44:17 +02:00
Christian Wiegand
d134c0049d
Localization (#214)
* General localization:
  - Add gettext to literals
  - Add trans template tag to templates
  - Set localized date and time
  - Add locale option to TEMPLATE.env
  - Add migrations that result from model field changes

* Add german locale

* Add german locale
2022-08-28 15:22:50 -07:00
R. Miles McCain
b286c80754 Remove unneeded views 2022-08-28 15:07:05 -07:00
R. Miles McCain
c23f44d7b7 Merge commit '77cb1fb37c0da5bad39b3905f7a48cd3f176bac7' into api 2022-08-28 15:01:04 -07:00
Paweł Jastrzębski
b7f2e9cfe6 Remove basic option from api view 2022-08-28 13:35:22 +02:00
Matt Ronchetto
4d7c036acc
Add support for GPC header (#219)
* chore: reflect both headers in debugging

* chore: add Sec-GPC handling with DNT handling

The `if` statement is there purely so that nothing more has to change handling wise. If either value is true, DNT policy should kick in and no data should be stored/tracked. *Should* just work™.

* fix: meet Black style guide

* fix: comply with other header formatting

* fix: header typo
2022-08-27 18:35:56 -07:00
R. Miles McCain
77cb1fb37c Improve language 2022-08-27 14:52:02 -07:00
Jason A. Ribeiro
d75008a34f
Provide full path for example sqlite path (#217)
The Dockerfile currently creates a directory at /var/local/shynet/db/.
The `DB_NAME` in the example file caused migrations to fail as a result.
2022-08-27 14:28:37 -07:00
dependabot[bot]
204be741e1
Bump django from 3.2.14 to 3.2.15 (#222)
Bumps [django](https://github.com/django/django) from 3.2.14 to 3.2.15.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.14...3.2.15)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-11 18:59:55 -07:00
dependabot[bot]
dd1f113e05
Bump pyjwt from 2.3.0 to 2.4.0 (#211)
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.3.0...2.4.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-06 13:04:25 -07:00
dependabot[bot]
34d6c920bf
Bump django from 3.2.12 to 3.2.14 (#216)
Bumps [django](https://github.com/django/django) from 3.2.12 to 3.2.14.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.12...3.2.14)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-05 20:55:11 -07:00
Fidel Ramos
96c9c0feec
Fix SQLite support (#210)
SQLite DB was not writable because it was always located in
/usr/src/shynet/, which is owned by root and not writable by
appuser. SQLite needs the parent directory containing the DB file to be
writable by the running user.

The applied fix is to place the DB file in /var/local/shynet/db and to
create that directory in the Docker image with the right permissions.

SQLite setup is now documented in README as an alternative to Postgres.
2022-05-24 12:38:27 -07:00
Paweł Jastrzębski
d9bbeea892 Remove basic option from API
For simplicity
2022-05-12 12:10:44 +02:00
Paweł Jastrzębski
ca97453c3e Return 400 if date format is invalid 2022-04-26 10:13:52 +02:00
Paweł Jastrzębski
b87b158aab Fix typo 2022-04-22 08:28:09 +02:00
Paweł Jastrzębski
4a6af18765 Add django-cors-headers 2022-04-14 19:41:14 +02:00
dependabot[bot]
231421ebae
Bump minimist from 1.2.5 to 1.2.6 (#204)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-27 11:19:45 -04:00
dependabot[bot]
d17aa9ba38
Bump django from 3.2.10 to 3.2.12 (#202)
Bumps [django](https://github.com/django/django) from 3.2.10 to 3.2.12.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.10...3.2.12)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-10 10:19:04 -08:00
havk
f6188a8bd1
Add default 0 hits to chart_data (#195) 2022-01-11 14:16:49 -08:00
dependabot[bot]
6032b9f4ee
Bump celery from 5.2.1 to 5.2.2 (#192)
Bumps [celery](https://github.com/celery/celery) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/celery/celery/releases)
- [Changelog](https://github.com/celery/celery/blob/master/Changelog.rst)
- [Commits](https://github.com/celery/celery/compare/v5.2.1...v5.2.2)

---
updated-dependencies:
- dependency-name: celery
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-06 22:16:10 -08:00
Paweł Jastrzębski
6d84f63130 Add API documentation to GUIDE.md 2022-01-05 10:27:14 +01:00
Paweł Jastrzębski
ba91ed561d Add uuid validation 2022-01-05 09:47:14 +01:00
Paweł Jastrzębski
2aaadfe81c Display api urls on service management page 2022-01-05 09:47:05 +01:00
Paweł Jastrzębski
7f60b3abff Rename minimal parameter to basic 2022-01-05 08:53:46 +01:00
Paweł Jastrzębski
069b218828 Move api token info to security tab 2022-01-04 08:53:00 +01:00
havk
a460d1f045
Remove empty js script from template (#189)
* Remove empty js script from template
Serving empty file raise ValueError in gunicorn

* Remove empty js file
2022-01-02 14:15:07 -08:00
havk
8b0205a2f7
Fix last month date range (#187) 2022-01-02 09:37:19 -05:00
Paweł Jastrzębski
80647d960a Merge branch 'master' into api 2022-01-01 19:56:55 +01:00
Paweł Jastrzębski
364ef115c9 Merge branch 'api' of https://github.com/haaavk/shynet into api 2022-01-01 19:50:10 +01:00
R. Miles McCain
71ec196ec4 Use :edge in default kubernetes deployment 2022-01-01 00:11:43 -05:00
R. Miles McCain
4834c5722f Bump version to v0.12.0 2021-12-31 23:47:05 -05:00
R. Miles McCain
4b4c8f207e Fix dates in the new year 2021-12-31 23:44:48 -05:00
R. Miles McCain
aed88b7c9a Use Alpine 3.14 2021-12-31 13:26:57 -05:00
Paweł Jastrzębski
bcf94147c9 Fix problem with whitespaces in copied token 2021-12-31 12:11:42 -05:00
Paweł Jastrzębski
66b841fd86 Move token to User model + add API setting view 2021-12-31 12:11:42 -05:00
Paweł Jastrzębski
d809ec82d9 Add uuid filter and service uuid filter 2021-12-31 12:11:42 -05:00
Paweł Jastrzębski
e577aa4997 Add minimal argument to get_core_stats 2021-12-31 12:11:42 -05:00
Paweł Jastrzębski
5966ea2f84 Add DashboardApiView 2021-12-31 12:11:42 -05:00
Paweł Jastrzębski
a7248cd54b Add ApiTokenRequiredMixin 2021-12-31 12:11:42 -05:00
Paweł Jastrzębski
1dec03c724 Add ApiToken to admin 2021-12-31 12:11:42 -05:00
Paweł Jastrzębski
ff6933b4de Add api app with ApiToken model 2021-12-31 12:11:42 -05:00
R. Miles McCain
1fd46b019c
Lessen priorities on field buttons (#182)
* Lessen priorities on field buttons

* Use latest alpine
2021-12-31 12:11:03 -05:00
R. Miles McCain
e534269c77 Provide initial contributing guide 2021-12-21 00:23:29 -05:00
R. Miles McCain
0d64ef33b0 Remove installation with SSL from guide TOC 2021-12-21 00:15:15 -05:00
R. Miles McCain
56c82e7d23 Remove HTTPS without reverse proxy from the GUIDE 2021-12-21 00:14:18 -05:00
R. Miles McCain
c71d934c67 Remove armv7 support 2021-12-21 00:10:12 -05:00
R. Miles McCain
85ae56fcdb Add swap space to GitHub Actions runners 2021-12-18 15:34:11 -05:00
R. Miles McCain
cd422ffd71 Add note about ALLOWED_HOSTS to GUIDE.md 2021-12-18 15:14:11 -05:00
R. Miles McCain
060a9b2a96 Update Python dependencies 2021-12-18 14:38:31 -05:00
R. Miles McCain
8d13ccd0fd Update dependencies 2021-12-18 13:56:57 -05:00
lionep
0d46e6d1f4
Fix typo in GUIDE.md (#180) 2021-12-04 09:50:26 -08:00
R. Miles McCain
81ae84efb3 Add office hours link 2021-11-21 00:12:24 -08:00
Paweł Jastrzębski
8302aedaa7 Fix problem with whitespaces in copied token 2021-11-17 11:46:35 +01:00
Paweł Jastrzębski
e2d438134a Merge branch 'master' into api 2021-11-17 11:24:34 +01:00
Paweł Jastrzębski
787ce1775f Move token to User model + add API setting view 2021-11-17 11:00:52 +01:00
R. Miles McCain
ea5f58fbd3 Update lock 2021-11-13 21:11:52 -08:00
dependabot[bot]
4079a8494a
Bump sqlparse from 0.4.1 to 0.4.2 (#178)
Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.1...0.4.2)

---
updated-dependencies:
- dependency-name: sqlparse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-13 21:10:06 -08:00
Sérgio
780b71083a
Add first factories and first dashboard tests (#172)
* Add factories and first dashboard tests

* Code cleanup

Co-authored-by: R. Miles McCain <github@sendmiles.email>
2021-11-13 21:09:55 -08:00
Sérgio
62fbb014e7
Testing setup (dependencies and github actions) (#169)
* Add testing dependencies

* Add draft github action

* Fix testing env variables

* Newline lint

* Run tests on every pull request and push
2021-11-13 21:03:06 -08:00
Paweł Jastrzębski
d62d48c7b4 Add uuid filter and service uuid filter 2021-10-14 07:38:21 +02:00
Paweł Jastrzębski
2f8891a843 Add minimal argument to get_core_stats 2021-10-13 19:52:36 +02:00
Paweł Jastrzębski
a963694fd0 Add DashboardApiView 2021-10-13 19:21:52 +02:00
Paweł Jastrzębski
90b2896ded Add ApiTokenRequiredMixin 2021-10-13 16:01:31 +02:00
Paweł Jastrzębski
bec4b19366 Add ApiToken to admin 2021-10-11 12:37:01 +02:00
Paweł Jastrzębski
32adb64dc0 Add api app with ApiToken model 2021-10-11 11:33:18 +02:00
JuniorJPDJ
53bc690435
fix(docker): healthcheck (#175)
fixes healthcheck for $ALLOWED_HOSTS longer than 2 domains
2021-09-30 16:12:57 -07:00
JuniorJPDJ
04120323a6
feat(docker): healthcheck (#166) 2021-09-16 13:35:38 -07:00
R. Miles McCain
03ced00f63
Remove Pipfile 2021-07-31 23:54:08 +00:00
R. Miles McCain
8d04ed5c1f
Always install debug toolbar 2021-07-31 23:53:41 +00:00
R. Miles McCain
f2879775ef
Set default auto field 2021-07-31 23:49:01 +00:00
R. Miles McCain
c980567fee
Formatting 2021-07-31 23:44:05 +00:00
R. Miles McCain
57c8695bcc
Debug toolbar fix 2021-07-31 23:44:03 +00:00
R. Miles McCain
31ffa47fd3
Bump version 2021-07-20 13:37:37 +00:00
R. Miles McCain
73f3513dfe
Use Poetry, not Pipenv 2021-07-20 04:51:13 +00:00
R. Miles McCain
b2e9d50d78
Update psycopg2 2021-07-20 03:05:18 +00:00
Casper Verswijvelt
de235c02a7
Add ability to toggle between map chart and country/session table (#153)
* Add ability to toggle between geo map and table view

* Add back haaavk's bar visualisation for countries table

* Change text/location of map/table toggle buttons

* Add some common css to reusable class

* CSS consistency

* Use button, not span for interactive elements

Co-authored-by: R. Miles McCain <oci@sendmiles.email>
2021-07-19 23:04:56 -04:00
Kasper Seweryn
31cb616242
Change snippet url to display current host (#159)
* Change snippet url to current host

* Change site.domain to host in page.js

* Remove useless condition

* Change hostname in email messages

* Remove `hostname` command

* Fix startup_checks.sh

* Remove unused variable from startup_checks.py
2021-07-19 22:55:30 -04:00
havk
2d5fbae279
Add sessions key to hits_per_day dict (#160) 2021-07-19 22:38:45 -04:00
Casper Verswijvelt
0153b1f847
Fix ellipsis in multiple tables (#152) 2021-06-15 15:29:59 -04:00
R. Miles McCain
473ad93081
Properly name manual build action 2021-06-14 20:56:29 +00:00
R. Miles McCain
1225ad90e8
Add manual build option
Signed-off-by: R. Miles McCain <github@sendmiles.email>
2021-06-14 20:55:22 +00:00
R. Miles McCain
e43718f596
Bump version 2021-06-13 20:11:28 +00:00
R. Miles McCain
d9623a9905
Also build image on push to master 2021-06-13 20:08:45 +00:00
R. Miles McCain
011f1f13c8
Also build image on push to master 2021-06-13 19:58:15 +00:00
Casper Verswijvelt
9832de0c19
GeoIP Map (#142)
* First working version of world map chart

* Cleanup code, fix aspect ratio, add GeoIP Map header

* Remove limited-height on session list with already limited content

* Update package lock

* Integrate map into service page

* Adjust map colors

* Adjust colors further

Co-authored-by: R. Miles McCain <github@sendmiles.email>
2021-06-13 15:50:01 -04:00
havk
83b20643d2
Add hits to chart data (#141)
* Add hits to chart data

* Hide legend

Co-authored-by: R. Miles McCain <github@sendmiles.email>
2021-06-13 14:39:45 -04:00
R. Miles McCain
ab44ba8318
Add subtle rounding to bar 2021-06-13 18:32:55 +00:00
havk
fcea6d3be9
Percents + visualization (#139)
* Black autoformat

* Add percent and divide filters

* Remove divide filter

* Add percents in brackets and visualization

* Apply percents and visualization to all data

* Switch absolute to relative

* Increase percent bar height

* Move bar to separated file

* Add USE_RELATIVE_MAX_IN_BAR_VISUALIZATION to settings

* Add flex items-center

* Move bar to left

* Remove spaces

* Fix USE_RELATIVE_MAX_IN_BAR_VISUALIZATION

* Remove unnecessary True

* Add bar_width tag

* Add flex-none to make flag not get squished

* Fix flex-none
2021-06-13 14:11:40 -04:00
Ian MacIntosh
f3a89bff78
Document possible time zone values (#147)
This might be a normal expectation, but it was new for me.

I added a URL to fast-track the next person in the same position.
2021-06-13 13:58:13 -04:00
dependabot[bot]
3c9bc9f3c9
Bump django from 3.1.9 to 3.1.12 (#150)
Bumps [django](https://github.com/django/django) from 3.1.9 to 3.1.12.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.9...3.1.12)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-13 13:50:48 -04:00
dependabot[bot]
2f5d0ba7e5
Bump django from 3.1.8 to 3.1.9 (#146)
Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.9.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.8...3.1.9)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-04 22:32:57 -04:00
dependabot[bot]
1c866209c9
Bump urllib3 from 1.26.4 to 1.26.5 (#145)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-02 11:27:14 -04:00
Casper Verswijvelt
a4785b1a0c
Datepicker mobile overflow (#143)
* Remove w-auto to fix datepicker overflow on mobile

* Hardcoded width for datepicker input element
2021-05-23 12:14:03 -04:00
R. Miles McCain
2928e663db Move sample Kubernetes deployment to latest 2021-05-14 17:01:15 +00:00
R. Miles McCain
ff97a46fd9 Bump version 2021-05-14 16:33:11 +00:00
R. Miles McCain
afb78dc499 Merge branch 'pagination-settings' 2021-05-14 16:25:22 +00:00
R. Miles McCain
a4eaef0117 Merge branch 'hourly-chart' 2021-05-14 16:25:06 +00:00
R. Miles McCain
7891866214 Merge branch 'skip_heartbeat' 2021-05-14 16:24:47 +00:00
R. Miles McCain
eedcbc4e85 Merge branch 'haaavk/master' 2021-05-14 16:24:20 +00:00
R. Miles McCain
0d006620dd Merge branch 'custom-map-provider' 2021-05-14 16:23:56 +00:00
R. Miles McCain
0b78f6df72 Merge branch 'ui-improvements' 2021-05-14 16:22:31 +00:00
R. Miles McCain
74ddef1670 Merge branch 'flag-icons' 2021-05-14 16:21:34 +00:00
R. Miles McCain
9d9d4a7b1e Bump version to v0.9.0 2021-05-14 16:16:12 +00:00
Paweł Jastrzębski
d66f683104 Add DASHBOARD_PAGE_SIZE to settings
Add DASHBOARD_PAGE_SIZE to TEMPLATE.env and app.json

Parse dashboard page size value
2021-05-14 16:09:45 +00:00
CasperVerswijvelt
b44642e023 When daterange is 1 day, show hourly data in chart
Show hourly chart starting from ranges of 3 days and less

Use timedelta instead of checking days difference which did not work correctly

Fix hourly chart bug

Add click handler for going from daily to hourly chart view by clicking chart at specific date
2021-05-14 16:09:45 +00:00
R. Miles McCain
c12a7e9e71 Document ACTIVE_USER_TIMEDELTA 2021-05-14 16:09:45 +00:00
R. Miles McCain
0294d31ea4 Remove console warning from script 2021-05-14 16:09:45 +00:00
Paweł Jastrzębski
40cb5afbad Skip heartbeat if there is no response
Fix xhr callbacks
2021-05-14 16:09:45 +00:00
Paweł Jastrzębski
073bd94112 Update litepicker and add ranges plugin
Fix litepicker colors

Fix litepicker event

Add custom ranges to litepicker

Fix code style

Remove some date ranges

Fix date ranges

Replace yesterday date range with last 3 days

Update packages

Improve litepicker box shadow
2021-05-14 16:09:45 +00:00
CasperVerswijvelt
3a01fefcff Add custom location url from environment variable
Remove trailing dollar in long and lat placeholder
2021-05-14 16:09:45 +00:00
CasperVerswijvelt
14a7ec68f3 Make favicon not squish and add ellipsis overflow
General styling improvements

Many UI Improvements

- Consistent spacing between titles and content
- Removed many ugly text squishing by hiding overflowing text with ellipsis
- Fixed Service favicon being squisched by long service name
- Hide scrollbar in 'more session' screen when content isn't scrollable
- Fix apexcharts tooltips and labels being cut off by card class

Disable wrapping in table cells, prefer ellipsis

Ellipsis overflow for long url on hit page

Fix flex grow in header not working as intended

Remove forgotten truncatechars

Fix code checks, add button role and tabindex
2021-05-14 16:09:45 +00:00
CasperVerswijvelt
fdf2ab719b Use flag icons instead of emoji's 2021-05-14 16:09:45 +00:00
R. Miles McCain
737eeb5df4 Contextual date improvements 2021-05-14 16:09:45 +00:00
CasperVerswijvelt
cb4855e4fc Preserve date range in urls in side menu 2021-05-14 16:09:45 +00:00
CasperVerswijvelt
f4127cf9b1 Preserve date range query parameters 2021-05-14 16:09:45 +00:00
R. Miles McCain
159015de1c Update test for my new instance 2021-05-14 16:09:45 +00:00
Paweł Jastrzębski
a7548d7eba Fix currently_online
Make currently_online aware of SCRIPT_HEARTBEAT_FREQUENCY
2021-05-14 16:09:45 +00:00
R. Miles McCain
da87ddb18f Fix ingest when MMDB not found 2021-05-14 16:09:45 +00:00
R. Miles McCain
4a76ab32fc Improve service page when no hits are recorded 2021-05-14 16:09:45 +00:00
R. Miles McCain
4afeced7d3 Improve homepage when no services exist 2021-05-14 16:09:45 +00:00
CasperVerswijvelt
2a6efe1b7f Merge 2 steps 2021-05-14 16:09:44 +00:00
CasperVerswijvelt
07f3926a9c Show snippet on service page when not hits are recorded yet 2021-05-14 16:09:44 +00:00
R. Miles McCain
14ed0b7979 Merge branch 'docker-github-build' 2021-05-14 16:06:37 +00:00
CasperVerswijvelt
ab51089647 Fix code checks, add button role and tabindex 2021-05-14 15:01:58 +00:00
CasperVerswijvelt
86695dbcc4 Remove forgotten truncatechars 2021-05-14 15:01:57 +00:00
CasperVerswijvelt
4e4cfe081b Fix flex grow in header not working as intended 2021-05-14 15:01:31 +00:00
CasperVerswijvelt
f54b67ef0f Ellipsis overflow for long url on hit page 2021-05-14 15:01:11 +00:00
CasperVerswijvelt
43f339e32b Disable wrapping in table cells, prefer ellipsis 2021-05-14 14:59:38 +00:00
CasperVerswijvelt
b144efaa9b Many UI Improvements
- Consistent spacing between titles and content
- Removed many ugly text squishing by hiding overflowing text with ellipsis
- Fixed Service favicon being squisched by long service name
- Hide scrollbar in 'more session' screen when content isn't scrollable
- Fix apexcharts tooltips and labels being cut off by card class
2021-05-14 14:58:00 +00:00
CasperVerswijvelt
c06b7a094a General styling improvements 2021-05-14 14:54:44 +00:00
CasperVerswijvelt
f13745f15e Make favicon not squish and add ellipsis overflow 2021-05-14 14:54:42 +00:00
CasperVerswijvelt
5c782ddb7d Use flag icons instead of emoji's 2021-05-14 14:52:11 +00:00
R. Miles McCain
a6a508899a Contextual date improvements 2021-05-14 14:50:04 +00:00
CasperVerswijvelt
2b003b8fa9 Preserve date range in urls in side menu 2021-05-08 12:27:32 +02:00
CasperVerswijvelt
023e0fde15 Preserve date range query parameters 2021-05-06 21:23:05 +02:00
Paweł Jastrzębski
a1a083a403 Replace yesterday date range with last 3 days 2021-05-05 14:56:17 +02:00
Paweł Jastrzębski
8b167b2c74 Fix date ranges 2021-05-05 14:54:33 +02:00
CasperVerswijvelt
4cd0c4735d Add click handler for going from daily to hourly chart view by clicking chart at specific date 2021-05-01 22:04:38 +02:00
Paweł Jastrzębski
d9e1ffddb1 Add DASHBOARD_PAGE_SIZE to TEMPLATE.env and app.json 2021-04-30 14:01:12 +02:00
Paweł Jastrzębski
9fb875f749 Add DASHBOARD_PAGE_SIZE to settings 2021-04-30 08:36:50 +02:00
CasperVerswijvelt
f6e502dfbd Remove trailing dollar in long and lat placeholder 2021-04-28 21:50:05 +02:00
CasperVerswijvelt
7c69b0bd81 Fix hourly chart bug 2021-04-28 18:08:26 +02:00
CasperVerswijvelt
78bea501a8 Use timedelta instead of checking days difference which did not work correctly 2021-04-27 23:39:58 +02:00
CasperVerswijvelt
c2daf3a5a5 Show hourly chart starting from ranges of 3 days and less 2021-04-27 21:58:07 +02:00
CasperVerswijvelt
df6786e037 Change docker push tags 2021-04-26 18:21:31 +02:00
Casper Verswijvelt
6621625d90 Multi arch docker build 2021-04-26 17:36:38 +02:00
CasperVerswijvelt
32ae0aa5f3 Change locationUrl to snake casing 2021-04-25 22:55:17 +02:00
CasperVerswijvelt
2221a99662 When daterange is 1 day, show hourly data in chart 2021-04-25 22:54:06 +02:00
Paweł Jastrzębski
69ec37331a Fix xhr callbacks 2021-04-25 17:32:23 +02:00
R. Miles McCain
03f88af03c Update test for my new instance 2021-04-24 17:35:55 +00:00
R. Miles McCain
87b7ce2edc Merge branch 'improve_currently_online' into dev 2021-04-24 17:27:27 +00:00
R. Miles McCain
26c1ae2bce Fix ingest when MMDB not found 2021-04-24 17:21:29 +00:00
R. Miles McCain
36d72508e6 Merge branch 'CasperVerswijvelt/master' into dev 2021-04-24 17:07:24 +00:00
R. Miles McCain
68945df17d Improve service page when no hits are recorded 2021-04-24 17:06:32 +00:00
R. Miles McCain
fef531efa9 Improve homepage when no services exist 2021-04-24 17:06:19 +00:00
CasperVerswijvelt
46176b19fc Merge 2 steps 2021-04-24 13:04:21 +02:00
CasperVerswijvelt
94c53d2ab5 Show snippet on service page when not hits are recorded yet 2021-04-24 13:01:03 +02:00
Paweł Jastrzębski
ea893b2322 Skip heartbeat if there is no response 2021-04-23 19:28:31 +02:00
Paweł Jastrzębski
71431fcaa6 Fix currently_online
Make currently_online aware of SCRIPT_HEARTBEAT_FREQUENCY
2021-04-23 11:00:32 +02:00
Paweł Jastrzębski
e9536f1816 Remove some date ranges 2021-04-22 19:58:13 +02:00
Paweł Jastrzębski
6f835a4f27 Fix code style 2021-04-22 17:04:06 +02:00
Paweł Jastrzębski
faf4f48e75 Add custom ranges to litepicker 2021-04-22 15:57:19 +02:00
CasperVerswijvelt
278306daa4 Add custom location url from environment variable 2021-04-21 20:16:28 +02:00
Paweł Jastrzębski
2c0fafefea Fix litepicker event 2021-04-21 18:30:33 +02:00
Paweł Jastrzębski
6eb41e016a Fix litepicker colors 2021-04-21 18:13:33 +02:00
CasperVerswijvelt
369f4d8d6b Use flag icons instead of emoji's 2021-04-21 16:14:26 +02:00
Paweł Jastrzębski
3d43f223eb Update litepicker and add ranges plugin 2021-04-21 11:47:50 +02:00
dependabot[bot]
351efff147
Bump django from 3.1.7 to 3.1.8 (#115)
Bumps [django](https://github.com/django/django) from 3.1.7 to 3.1.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.7...3.1.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-16 17:02:07 -04:00
dependabot[bot]
6867cbd282
Bump django-debug-toolbar from 3.2 to 3.2.1 (#117)
Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 3.2 to 3.2.1.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/3.2...3.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-16 17:01:58 -04:00
CasperVerswijvelt
c03ef52ba8 Fix pixel request from not allowed origin triggering a hit 2021-04-02 21:21:24 +02:00
R. Miles McCain
9cb030ecbd Bump version 2021-03-29 15:09:27 +00:00
R. Miles McCain
8bab14cc8a Separate bounce migration into two 2021-03-29 15:04:36 +00:00
R. Miles McCain
fe8e766670 Format 2021-03-29 14:37:59 +00:00
R. Miles McCain
b63863e283 Bump version 2021-03-29 14:37:21 +00:00
R. Miles McCain
516f9fb951 Fix aggressive hash salting 2021-03-29 14:37:08 +00:00
R. Miles McCain
c2234ec647 Bump version 2021-03-28 22:15:30 +00:00
R. Miles McCain
02cbee5c8c Cache bounce 2021-03-28 21:55:38 +00:00
R. Miles McCain
518436ffd2 Relock npm packages 2021-03-28 21:37:09 +00:00
R. Miles McCain
311aa2b1ac Drop Turbolinks 2021-03-28 21:36:53 +00:00
R. Miles McCain
8ad44ddc23 Add pagination to dashboard 2021-03-28 21:29:54 +00:00
R. Miles McCain
874aad87a8 Store service directly in Hit 2021-03-28 20:54:19 +00:00
R. Miles McCain
f2e875d03d Add indexes to key Hit fields 2021-03-28 19:18:57 +00:00
R. Miles McCain
45fd32c8ca Index last_seen 2021-03-28 19:15:03 +00:00
R. Miles McCain
08b36ba69f Integrate debug toolbar 2021-03-28 19:14:56 +00:00
R. Miles McCain
d5cfe577a0 Add debug toolbar 2021-03-28 19:14:33 +00:00
R. Miles McCain
c131cfef27 Merge branch 'patch-1' into dev 2021-03-28 18:54:24 +00:00
R. Miles McCain
526d4cd133 Relock Pipfile 2021-03-28 18:53:49 +00:00
R. Miles McCain
8e09871b44 Merge branch 'dependabot/pip/django-3.1.6' into dev 2021-03-28 18:51:33 +00:00
R. Miles McCain
6aa3ce0b32 Merge branch 'dependabot/pip/pyyaml-5.4' into dev 2021-03-28 18:49:49 +00:00
dependabot[bot]
23ea8e493e
Bump pyyaml from 5.3.1 to 5.4
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.3.1 to 5.4.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/5.3.1...5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-25 23:41:11 +00:00
dependabot[bot]
22d996bed7
Bump django from 3.1.3 to 3.1.6
Bumps [django](https://github.com/django/django) from 3.1.3 to 3.1.6.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.3...3.1.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-19 02:04:53 +00:00
Kasper Seweryn
9df864787c
Fix #99 2021-02-17 02:24:35 +00:00
dependabot[bot]
b7a6ac9ec0
Bump apexcharts from 3.23.1 to 3.24.0 (#97)
Bumps [apexcharts](https://github.com/apexcharts/apexcharts.js) from 3.23.1 to 3.24.0.
- [Release notes](https://github.com/apexcharts/apexcharts.js/releases)
- [Commits](https://github.com/apexcharts/apexcharts.js/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-15 18:56:29 -05:00
R. Miles McCain
38d8d416e1
Update kubernetes deployments 2021-01-25 13:39:53 -05:00
R. Miles McCain
592613a99a
Use dev shynet in kubernetes deployments 2021-01-23 23:28:24 -05:00
R. Miles McCain
e9f43c6a53
Bump version 2021-01-23 23:23:19 -05:00
R. Miles McCain
89c6800913
Fix formatting 2021-01-23 23:16:33 -05:00
R. Miles McCain
db9c807289
Add optional more aggressive salting (fixes #95) 2021-01-23 23:13:44 -05:00
R. Miles McCain
6e48a3eac7
Merge branch 'global-ip-block' into dev 2021-01-23 23:01:53 -05:00
R. Miles McCain
ba9a716913
Merge branch 'heartbeat-frequency' into dev 2021-01-23 22:41:25 -05:00
R. Miles McCain
6d7292a60a
Fix duration change being unknown (fixes #89) 2021-01-23 22:40:19 -05:00
Oliver Kamer
c0d02732e7
Add additional env variable to template 2021-01-19 22:05:33 +01:00
Oliver Kamer
d071a91917
Block Collect IP option if disabled globally 2021-01-19 22:02:57 +01:00
Oliver Kamer
d67e14b08f
Block IP collection from settings 2021-01-19 21:41:54 +01:00
Oliver Kamer
174a386f54
Add block all ips to settings 2021-01-19 21:31:02 +01:00
Oliver Kamer
ce23cfc5b5
Add pycharm gitignore stuff 2021-01-19 21:20:30 +01:00
Oliver Kamer
8be690c417
Use heartbeat frequency for currently active
If the heartbeat frequency is more than 10 seconds, shynet will display as not active, even though it still is.

Using 2x the heartbeat frequency should give better results.
2021-01-19 11:32:36 +01:00
R. Miles McCain
2f778dc4b4
Bump version to v0.7.3 2021-01-11 12:12:15 -05:00
R. Miles McCain
e0c165313b
Add fallback to percent_change_display (fixes #89) 2021-01-11 12:11:27 -05:00
R. Miles McCain
c86192d301
Improve a17t colors 2021-01-10 12:25:08 -05:00
R. Miles McCain
775c105d1d
Bump version to 0.7.2 2021-01-10 12:20:17 -05:00
R. Miles McCain
be85c0a560
Update and trim dependencies 2021-01-10 12:19:54 -05:00
R. Miles McCain
70e1af15cc
Fix division by zero error 2021-01-10 12:17:53 -05:00
R. Miles McCain
6afea91c5f
Bump version 2020-11-26 21:03:52 +00:00
R. Miles McCain
7a4c892804
Remove background from favicon 2020-11-26 21:03:18 +00:00
imgbot[bot]
9b50b1ea42
[ImgBot] Optimize images (#87)
*Total -- 765.10kb -> 502.59kb (34.31%)

/images/service.png -- 366.95kb -> 239.64kb (34.69%)
/images/homepage.png -- 398.15kb -> 262.95kb (33.96%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>

Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2020-11-26 16:00:51 -05:00
R. Miles McCain
52a18d21f1
Small visual improvements 2020-11-26 20:59:03 +00:00
R. Miles McCain
8aaf312c67
Bump version 2020-11-26 20:10:28 +00:00
R. Miles McCain
ede06900e5
Format code 2020-11-26 20:09:55 +00:00
R. Miles McCain
a42455c9dc
Add browser icon 2020-11-26 20:09:34 +00:00
R. Miles McCain
547a84f2fc
Update screenshots 2020-11-26 20:04:48 +00:00
R. Miles McCain
f56ea99dc2
Add demo data command 2020-11-26 19:44:07 +00:00
R. Miles McCain
4cea5d2310
Add additional icons 2020-11-26 19:43:44 +00:00
R. Miles McCain
e4f09b4e68
Ensure times are always correct 2020-11-26 19:43:25 +00:00
R. Miles McCain
cc094fe04e
Add icons to dashboard 2020-11-26 18:09:42 +00:00
R. Miles McCain
ac5c743390
Update dependencies 2020-11-26 18:00:19 +00:00
R. Miles McCain
963db18642
Bump version 2020-10-17 19:11:25 +00:00
R. Miles McCain
748fb76eaf
Bump version 2020-10-17 19:03:37 +00:00
R. Miles McCain
d93a698e87
Update django-redis-cache 2020-10-17 19:03:18 +00:00
R. Miles McCain
ca9ee2f1f5
Bump version 2020-10-17 18:54:22 +00:00
R. Miles McCain
9146c889ac
Show Shynet version when envvar is true 2020-10-17 18:53:46 +00:00
R. Miles McCain
8b1034ebb0
Fix prerelease usage 2020-10-17 18:34:09 +00:00
R. Miles McCain
a8fd263855
Bump version 2020-10-14 18:00:35 +00:00
R. Miles McCain
31fa3d55d5
Update dependencies 2020-10-14 18:00:31 +00:00
Sudipto Ghosh
13229f64aa
removed unused import 2020-08-31 18:27:55 +05:30
Sudipto Ghosh
101d26d356
added SHOW_SHYNET_VERSION env var to control display of version info 2020-08-31 18:08:00 +05:30
R. Miles McCain
c524325f0a
Bump version to v0.6.5 2020-08-28 17:20:16 +00:00
R. Miles McCain
4a07ab80ce
Prevent multiple emails from pointing to same collaborator (fixes #78) 2020-08-28 17:19:57 +00:00
R. Miles McCain
c8dead4457
Prevent services from showing up twice on homepage 2020-08-28 17:19:07 +00:00
R. Miles McCain
4a06357137
Bump version 2020-08-19 23:08:45 +00:00
Nicholas Bentley
29ac82a91b smtp ssl/tls fix 2020-08-18 23:34:20 -04:00
R. Miles McCain
fecea17a9d
Bump version 2020-08-18 15:42:17 +00:00
R. Miles McCain
03062e3de5
Fix session detail page for collaborators (fixes #74) 2020-08-18 15:41:50 +00:00
R. Miles McCain
6652acdf14
Bump version 2020-08-11 21:56:59 +00:00
R. Miles McCain
1dfbec06e1
Split testing options 2020-08-11 21:56:26 +00:00
R. Miles McCain
3e315f06ed
Enforce origin checking on pixel trackers (indirectly fixes #65) 2020-08-11 21:56:20 +00:00
R. Miles McCain
2d42674e1a
Add warning when hostname starts with http (fixes #68) 2020-08-11 21:39:08 +00:00
R. Miles McCain
e4deab2072
Fix file path creation (fixes #69) 2020-08-11 21:34:39 +00:00
R. Miles McCain
c5ed5ef0e7
Merge branch 'MagnumDingusEdu/master' into dev 2020-08-11 21:32:03 +00:00
R. Miles McCain
7268a4ea84
Improve GUIDE language 2020-08-11 21:31:39 +00:00
Vividh Mariy
2cbc5ac441 Added deployment using docker-compose. Fixed #70 2020-08-10 00:00:52 +05:30
R. Miles McCain
058601d669
Fix button styling on session page (fixes #63) 2020-07-31 16:32:15 +00:00
Jake Malachowski
213c44a45a
Add Render as a deployment option (#62)
* Add Render deployment option

Add Render as deployment option

* Remove Render feature descriptions
2020-07-21 11:45:35 -04:00
R. Miles McCain
8b98cf2277
Update pixel cache control 2020-07-11 17:26:53 +00:00
R. Miles McCain
4c53b94588
Add SPA section to guide TOC 2020-07-07 03:25:31 +00:00
R. Miles McCain
a70e07be05
Finish transition to startup checks 2020-07-07 03:15:07 +00:00
R. Miles McCain
0195c4595b
Document SPA behavior 2020-07-07 03:01:48 +00:00
R. Miles McCain
a54d9e6840
Bump version 2020-07-07 02:45:11 +00:00
R. Miles McCain
a4245eb733
Update dependencies 2020-07-07 02:45:03 +00:00
R. Miles McCain
7e0584b5d2
Fix button styling 2020-07-07 02:44:38 +00:00
R. Miles McCain
37396cde63
Improve service form 2020-07-07 02:23:48 +00:00
R. Miles McCain
a1e4bef08f
Use a17t v0.2.2 2020-07-07 02:23:09 +00:00
R. Miles McCain
c3510278e3
Improve origin explanation language 2020-07-07 01:41:01 +00:00
R. Miles McCain
da61b9b400
Document primary key integration (fixes #56) 2020-07-07 01:38:16 +00:00
R. Miles McCain
98187a39f8
Document health check endpoint (fixes #59) 2020-07-07 01:27:08 +00:00
R. Miles McCain
3d27efba8b
Check IP versions before comparing (fixes #57) 2020-07-07 00:22:29 +00:00
R. Miles McCain
80c66ceb8e
Remove unnecessary ipaddress dependency 2020-07-07 00:18:33 +00:00
R. Miles McCain
a2776e64f6
Rename from "sanity results" to "startup results" 2020-07-07 00:18:24 +00:00
imgbot[bot]
c73f96525a
[ImgBot] Optimize images (#55)
*Total -- 911.50kb -> 583.94kb (35.94%)

/images/slogo.png -- 2.51kb -> 0.91kb (63.77%)
/images/service.png -- 589.15kb -> 359.70kb (38.95%)
/images/homepage.png -- 307.99kb -> 214.75kb (30.28%)
/images/logo.png -- 11.85kb -> 8.58kb (27.54%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>

Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2020-06-28 15:11:48 -04:00
R. Miles McCain
510df192d8
Add script injection option 2020-06-28 18:59:05 +00:00
R. Miles McCain
2e7620f1eb
Bump version 2020-06-28 17:56:29 +00:00
R. Miles McCain
93d4ee5241
Use specific version of pipenv in dockerfile 2020-06-28 17:56:09 +00:00
R. Miles McCain
1a7594be93
Use SHA256 for more secure session association 2020-06-28 17:55:59 +00:00
R. Miles McCain
f464a7ee67
Add automatic release drafter 2020-06-28 17:47:58 +00:00
R. Miles McCain
a1cd3d4609
Code cleanup 2020-06-28 17:36:20 +00:00
R. Miles McCain
358fb234a7
Fix multiple origin support (closes #52) 2020-06-28 17:36:12 +00:00
R. Miles McCain
94fed58de3
Fix charts 2020-06-28 17:18:58 +00:00
R. Miles McCain
49f452d9f2
Lock Python package versions 2020-06-28 17:11:08 +00:00
R. Miles McCain
40d07fe159
Add IE11 support 2020-06-28 04:10:06 +00:00
R. Miles McCain
e150e6bede
Bump version 2020-06-28 04:01:50 +00:00
R. Miles McCain
87a411f42d
Make ingress processing more resilient 2020-06-28 03:48:40 +00:00
R. Miles McCain
88f25b6743
Analytics script cleanup (closes #54) 2020-06-28 03:31:10 +00:00
R. Miles McCain
bb0dc2e90f
Remove all external dependencies 2020-06-28 02:58:49 +00:00
R. Miles McCain
4a8939796e
Arbitrarily update the pixel test 2020-06-28 02:45:46 +00:00
R. Miles McCain
ba795ccd5c
Clarify collaborators description 2020-06-28 02:44:59 +00:00
Alexandre Bulté
c9b5a677d3
SIGNUPS_ENABLED -> ACCOUNT_SIGNUPS_ENABLED in docs (#51) 2020-06-18 15:15:10 -04:00
Alexandre Bulté
affcb893fa
Add timeout to curl / geoip (#50)
W/o timeout, if the download is unresponsive it can be a pain on some environments (eg `dokku`) because the build hangs forever.
2020-06-18 14:00:06 -04:00
R. Miles McCain
e030807acb
Fix GeoIP2 license keys 2020-06-18 16:46:39 +00:00
R. Miles McCain
4ced1365d4
Bump version 2020-06-15 20:14:56 +00:00
Ruben van Erk
dcdbb7cd45 Add option to ignore robots 2020-06-15 19:07:13 +02:00
R. Miles McCain
b3102f5f32
Remove erroneous app.json default 2020-06-14 02:05:11 +00:00
R. Miles McCain
2a61cf1b51
Update GUIDE structure 2020-06-14 01:51:44 +00:00
R. Miles McCain
0d7c9c4c33
Bump version 2020-06-14 01:49:24 +00:00
R. Miles McCain
6649aeaaf0
Update dependencies 2020-06-14 01:49:18 +00:00
R. Miles McCain
cb11dc0c4e
Add mention of Heroku to docs 2020-06-14 00:12:31 +00:00
R. Miles McCain
4a4f2645df
Add Heroku setup instructions 2020-06-14 00:05:39 +00:00
R. Miles McCain
81a836df53
Improve startup behavior 2020-06-13 23:59:36 +00:00
Thomas Letsch Groch
919ca52ca1
👌 Refine code review changes 2020-06-06 06:02:33 -03:00
Thomas Letsch Groch
f7ecb88659
👌 Code review changes
commented out mysql logic and refine GUIDE.md
2020-06-02 20:34:02 -03:00
Thomas Letsch Groch
1a0dcf7579
📝 Improving heroku form 2020-06-01 19:31:19 -03:00
Thomas Letsch Groch
0f3037b315
📝 update template on heroku button 2020-06-01 18:47:30 -03:00
Thomas Letsch Groch
b234ef2917
Add heroku compatibility
Recognize heroku with the environment variable DATABASE_URL, parse it and replace the others based on it.
2020-06-01 18:41:39 -03:00
R. Miles McCain
1b344fb90c
Bump version 2020-05-28 22:02:50 +00:00
R. Miles McCain
d164306f8b
Make email verification optional (fixes #30) 2020-05-28 22:01:22 +00:00
R. Miles McCain
c61d23caf1
Add health check endpoint (fixes #31) 2020-05-28 22:00:49 +00:00
R. Miles McCain
fcfbbe8809
Remove confusing setup variables; migrate to commands. 2020-05-28 21:47:17 +00:00
R. Miles McCain
1bb4aac32f
Use $PORT env variable 2020-05-28 21:40:43 +00:00
R. Miles McCain
d895eac14d
Add note about watching the repo 2020-05-24 01:47:01 +00:00
R. Miles McCain
5cce890ff6
Add localhost help to guide 2020-05-21 21:06:55 -04:00
R. Miles McCain
387c1e375d
Add database connection timeout 2020-05-21 20:59:58 -04:00
R. Miles McCain
4e13842334
Bump version 2020-05-19 17:58:21 -04:00
R. Miles McCain
62c3a87cda
Add remote address to nginx conf 2020-05-19 17:57:02 -04:00
Santiago Alessandri
cac6d44166
Quote whitelabel to allow whitespace (#33)
The SHYNET_WHITELABEL variable was being used without quotes,
which breaks the commnand if it contains whitespaces
2020-05-17 00:23:38 -04:00
R. Miles McCain
1a5f68e353
Fix typo in README 2020-05-09 12:35:59 -04:00
R. Miles McCain
4569744726
Add email sending task 2020-05-09 12:35:50 -04:00
R. Miles McCain
6978bbd03e
Bump version, cleanup 2020-05-07 17:49:18 -04:00
R. Miles McCain
d88f61b281
Add better tracking script protocol support 2020-05-07 17:49:02 -04:00
R. Miles McCain
c84dac6b01
Add referrer hiding support (closes #26) 2020-05-07 17:44:39 -04:00
R. Miles McCain
abe37800ec
Small GUIDE expansions 2020-05-07 17:10:31 -04:00
R. Miles McCain
8aef1f0dc7
Update Kubernetes defaults 2020-05-07 17:06:04 -04:00
R. Miles McCain
1c01c27326
Merge #27 (duplication fix) 2020-05-07 16:54:49 -04:00
R. Miles McCain
a766c1eaa2
Add ip address exclusion support (closes #22)
Co-authored-by: Anthony Abeo <anthonyabeo@gmail.com>
2020-05-07 16:53:03 -04:00
Abeo Anthony, A
a457c2be7b remove duplicated device_types value 2020-05-07 19:59:09 +00:00
Abeo Anthony, A
6a5ce6ddb9 ignore vagrant config files 2020-05-07 19:58:31 +00:00
R. Miles McCain
bd88617dc5
Update Kubernetes settings 2020-05-05 14:42:26 -04:00
R. Miles McCain
77f1fbc2cc
Fix faulty parallelization 2020-05-04 14:20:34 -04:00
R. Miles McCain
0a0f76d84e
Bump version 2020-05-03 10:41:19 -04:00
R. Miles McCain
364ec655a0
Improve build process 2020-05-03 10:41:14 -04:00
R. Miles McCain
9fe79c9f23
Add troubleshooting guide 2020-05-03 10:39:13 -04:00
R. Miles McCain
446d672004
Optimize docker image (merges #21) 2020-05-03 10:11:02 -04:00
R. Miles McCain
fe1cb39bc5
Add note about Celery in TEMPLATE.env 2020-05-02 19:43:18 -04:00
Windyo
4737aa1295 Optimized Docker Image
Changed to Alpine
Optimized Docker Layers
2020-05-02 23:14:30 +02:00
R. Miles McCain
77871dd56a
Update Kubernetes to use default entrypoint 2020-05-02 12:54:39 -04:00
R. Miles McCain
1a0fe6e304
Bump version 2020-05-02 12:37:02 -04:00
R. Miles McCain
26778f0219
Add option to not collect IP addresses (closes #18) 2020-05-02 12:35:47 -04:00
R. Miles McCain
a210e23bb3
Use hashing to associate sessions 2020-05-02 12:16:57 -04:00
R. Miles McCain
34e698e309
Update and expand the GUIDE (merges #3) 2020-05-02 11:06:14 -04:00
R. Miles McCain
f33e0e342c
Merge branch 'dev' into identex/master 2020-05-02 10:30:38 -04:00
R. Miles McCain
dfb78b3669
Add docker-compose support (closes #19) 2020-05-02 10:27:16 -04:00
R. Miles McCain
5d26ab292b
Refactoring & consistency changes
Make all scripts executable

Disable debug mode by default

Use eager tasks by default

Fix typo in settings

Refactoring
2020-05-02 10:24:57 -04:00
R. Miles McCain
837f939de1
Improve noscript tracker security 2020-05-02 09:18:45 -04:00
R. Miles McCain
725496cc0f
Make celery default to eager 2020-05-02 09:16:18 -04:00
Windyo
6fa67f0531 Docker-Compose Single-Run
Add firstrun checks

Avoids running commands on firstrun
Add Docker-compose file

allows setup in a single docker-compose up command
Updated Readme re: docker-compose

Added stub detailing how to use the compose file.
Update README.md
Changed Entrypoint

Moved sanity checks to their own script, changed entrypoint logic
updated entrypoint
2020-04-30 22:59:22 +02:00
Jason Carpenter
9b9d70f711
Merge branch 'master' into master 2020-04-29 13:33:17 -04:00
R. Miles McCain
c896a4c150
Mention CoC in README 2020-04-28 11:04:01 -04:00
R. Miles McCain
bb1860b5c8
Add code of conduct 2020-04-28 11:02:20 -04:00
R. Miles McCain
653594ca48
Update roadmap section (mention 2FA, see #2) 2020-04-28 10:58:21 -04:00
0xflotus
73dad4cb6b
Add syntax highlighting to GUIDE.md (#10)
I enabled Syntax Highlighting in GUIDE.md for better readability
2020-04-28 10:53:35 -04:00
R. Miles McCain
dd6a9d1eaf
Bump version 2020-04-28 10:19:05 -04:00
R. Miles McCain
3c74331a74
Fix x-padding on smaller screen sizes (fixes #7) 2020-04-28 10:18:43 -04:00
R. Miles McCain
7bfcb1caff
Add reference to a17t 2020-04-28 10:15:35 -04:00
Jason Carpenter
0a3441428a
Merge pull request #1 from milesmcc/master
Merge 3
2020-04-25 21:16:12 -04:00
R. Miles McCain
f7e8580114
Add service screenshot 2020-04-25 12:02:32 -04:00
R. Miles McCain
25b7b1d0e5
Show version in sidebar (fixes #5) 2020-04-25 11:55:56 -04:00
R. Miles McCain
2223530f51
Show IP address in session details (fixes #6) 2020-04-25 11:55:31 -04:00
Jason Carpenter
c41e999028 De-list the images 2020-04-24 18:11:07 -04:00
Jason Carpenter
1a9d57ed0c Merge remote-tracking branch 'upstream/master' 2020-04-24 17:14:46 -04:00
Jason Carpenter
d2c930fa17 Added Basic Usage Guide 2020-04-24 17:14:38 -04:00
R. Miles McCain
62844db6bf
Reformatting & cleanup 2020-04-24 16:29:23 -04:00
R. Miles McCain
3a63f6f850
Improve README consistency 2020-04-24 16:28:28 -04:00
R. Miles McCain
2e386a7e25
Fix README typo & link 2020-04-24 16:26:15 -04:00
Jason Carpenter
f8d33cbc4d Typo 2020-04-24 15:59:30 -04:00
Jason Carpenter
2d85a23a20 Merge remote-tracking branch 'upstream/master' 2020-04-24 15:53:23 -04:00
Jason Carpenter
36de929577 Added documentation for reverse proxies 2020-04-24 15:53:08 -04:00
R. Miles McCain
db8dbb7723
Improve GDPR language 2020-04-24 15:12:23 -04:00
Jason Carpenter
23f1fdbb3f Merge remote-tracking branch 'upstream/master' 2020-04-24 15:10:56 -04:00
R. Miles McCain
1f13408f7f
Improve README style 2020-04-24 14:42:56 -04:00
R. Miles McCain
5c2838af27
Improve fonts 2020-04-24 14:13:33 -04:00
R. Miles McCain
20c530f669
Clarify guide settings 2020-04-24 14:09:37 -04:00
R. Miles McCain
17cdf052d8
Add CORS origin management 2020-04-24 14:07:34 -04:00
R. Miles McCain
e693406114
Add advanced settings area 2020-04-24 14:06:59 -04:00
R. Miles McCain
39ef4c9645
Improve script performance 2020-04-24 13:39:25 -04:00
R. Miles McCain
3f7aaa8f0d
Add heartbeat frequency setting 2020-04-24 13:22:22 -04:00
Jason Carpenter
9881dedac0
Expand installation instructions (#1)
* Expanded installation instructions

Installation was moved to a new file, GUIDE.md, where you can include all documented information about Shynet.

I also included SSL without a reverse proxy instructions and a shell script for SSL through Gunicorn.
2020-04-24 13:00:20 -04:00
Jason Carpenter
ee99218f2a Fixed mistake in SSL instructions 2020-04-24 12:59:00 -04:00
Jason Carpenter
d5e6be7cba Expanded installation instructions
Installation was moved to a new file, GUIDE.md, where you can include all documented information about Shynet.

I also included SSL without a reverse proxy instructions and a shell script for SSL through Gunicorn.
2020-04-24 12:38:06 -04:00
138 changed files with 8886 additions and 1334 deletions

15
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.11-bullseye
ENV PYTHONUNBUFFERED 1
# [Optional] If your requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

View File

@ -0,0 +1,29 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/postgres
{
"name": "Python 3 & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
}
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
// "forwardPorts": [5000, 5432],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip install --user -r requirements.txt",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@ -0,0 +1,35 @@
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
volumes:
postgres-data:

2
.flake8 Normal file
View File

@ -0,0 +1,2 @@
[flake8]
max-line-length = 88

4
.github/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,4 @@
template: |
## Whats Changed
$CHANGES

58
.github/workflows/build-docker-edge.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Build edge Docker images
on:
push:
branches:
- master
jobs:
publish_to_docker_hub:
runs-on: ubuntu-latest
steps:
- name: Create Docker Metadata
id: metadata
uses: docker/metadata-action@v5
with:
images: |
milesmcc/shynet
ghcr.io/milesmcc/shynet
tags:
type=edge
- name: Set swap space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 5
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push advanced image
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

View File

@ -0,0 +1,58 @@
name: Build manual Docker images
on:
workflow_dispatch:
inputs:
tag:
description: 'Docker image tag'
jobs:
publish_to_docker_hub:
runs-on: ubuntu-latest
steps:
- name: Create Docker Metadata
id: metadata
uses: docker/metadata-action@v5
with:
images: |
milesmcc/shynet
ghcr.io/milesmcc/shynet
tags:
type=raw,value=${{ github.event.inputs.tag }}
- name: Set swap space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 5
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push advanced image
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

View File

@ -0,0 +1,60 @@
name: Build release Docker images
on:
push:
tags:
- "*"
jobs:
publish_to_docker_hub:
runs-on: ubuntu-latest
steps:
# https://github.com/docker/metadata-action/tree/v4/#typeref
- name: Create Docker Metadata
id: metadata
uses: docker/metadata-action@v5
with:
images: |
milesmcc/shynet
ghcr.io/milesmcc/shynet
tags:
type=raw,value=latest
type=ref,event=tag
- name: Set swap space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 5
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push advanced image
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

14
.github/workflows/draft.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Release Drafter
on:
push:
branches:
- master
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5.11.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

38
.github/workflows/run-tests.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Run tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
db:
image: postgres:12.3-alpine
env:
POSTGRES_USER: shynet_db_user
POSTGRES_PASSWORD: shynet_db_user_password
POSTGRES_DB: shynet_db
ports:
- 5432:5432
strategy:
max-parallel: 4
matrix:
python-version: [3.9]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Run image
uses: abatilo/actions-poetry@v2.0.0
with:
poetry-version: 1.2.2
- name: Preinstall dependencies (temporary)
run: poetry run pip install "Cython<3.0" "pyyaml==5.4.1" --no-build-isolation
- name: Install dependencies
run: poetry install
- name: Django Testing project
run: |
cp TEMPLATE.env .env
poetry run ./shynet/manage.py test

11
.gitignore vendored
View File

@ -3,6 +3,9 @@ __pycache__/
*.py[cod]
*$py.class
# JavaScript packages
node_modules/
# C extensions
*.so
@ -109,6 +112,9 @@ venv/
ENV/
env.bak/
venv.bak/
Vagrantfile
.vagrant
ubuntu-xenial-16.04-cloudimg-console.log
# Spyder project settings
.spyderproject
@ -131,3 +137,8 @@ dmypy.json
# Secrets & env
secrets.yml
.vscode
.DS_Store
compiledstatic/
# Pycharm
.idea

16
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,16 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
#- repo: https://github.com/pre-commit/pre-commit-hooks
#rev: v3.2.0
#hooks:
#- id: trailing-whitespace
#- id: end-of-file-fixer
#- id: check-yaml
#- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 22.8.0
hooks:
- id: black
exclude: 'migrations|^shynet/shynet/settings.py'

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at shynet@sendmiles.email. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

15
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,15 @@
# Contributing
This document provides an overview of how to contribute to Shynet. Currently, it focuses on the more technical elements of contributing --- for example, setting up your development environment. Eventually, we will expand this guide to cover the social and governance oriented side of contributing as well.
## Setting up your development environment
To contribute to Shynet, you must have a reliable development environment. Because Shynet is intended to be run inside containers, we strongly encourage you to run Shynet in a container in development as well. The development setup described in this guide will use Docker and Docker Compose.
To begin, clone the Shynet repository to your computer, and ensure that you have Docker and Docker Compose installed.
Copy `TEMPLATE.env` to a new file called `.env`. This `.env` file will be used in your development environment. Paste `DEBUG=True` into the end of your new `.env` file so that Shynet will know to run in development mode.
Finally, follow the steps in [GUIDE.md](GUIDE.md) on setting up a Shynet instance with Docker Compose. This is where you'll setup an admin user.
_Did you have to perform additional steps to setup your environment? Document them here and submit a pull request!_

View File

@ -1,33 +1,51 @@
FROM python:3
FROM python:alpine3.14
# Getting things ready
WORKDIR /usr/src/shynet
RUN apt update
RUN apt install -y gettext
# URL from https://github.com/shlinkio/shlink/issues/596 :)
RUN curl "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz" | tar -xvz -C /tmp
RUN curl "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz" | tar -xvz -C /tmp
RUN mv /tmp/GeoLite2*/*.mmdb /etc
RUN pip install pipenv
COPY Pipfile.lock ./
COPY Pipfile ./
RUN pipenv install --system --deploy
COPY shynet .
RUN python manage.py collectstatic --noinput
RUN python manage.py compilemessages
# Install dependencies & configure machine
ARG GF_UID="500"
ARG GF_GID="500"
RUN apk update && \
apk add --no-cache gettext bash npm postgresql-libs && \
test "$(arch)" != "x86_64" && apk add libffi-dev rust cargo || echo "amd64 build, skipping Rust installation"
# libffi-dev and rust are used for the cryptography package,
# which we indirectly rely on. Necessary for aarch64 support.
# add group & user
RUN groupadd -r -g $GF_GID appgroup && \
useradd appuser -r -u $GF_UID -g appgroup
# Collect GeoIP Database
RUN apk add --no-cache curl && \
curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=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&suffix=tar.gz" | tar -xvz -C /tmp && \
curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=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&suffix=tar.gz" | tar -xvz -C /tmp && \
mv /tmp/GeoLite2*/*.mmdb /etc && \
apk --purge del curl
# Move dependency files
COPY poetry.lock pyproject.toml ./
COPY package.json package-lock.json ../
# Django expects node_modules to be in its parent directory.
# Install more dependencies and cleanup build dependencies afterwards
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev libressl-dev libffi-dev && \
npm i -P --prefix .. && \
pip install poetry==1.2.2 && \
poetry config virtualenvs.create false && \
poetry run pip install "Cython<3.0" "pyyaml==5.4.1" --no-build-isolation && \
poetry install --no-dev --no-interaction --no-ansi && \
apk --purge del .build-deps
# Setup user group
RUN addgroup --system -g $GF_GID appgroup && \
adduser appuser --system --uid $GF_UID -G appgroup && \
mkdir -p /var/local/shynet/db/ && \
chown -R appuser:appgroup /var/local/shynet
# Install Shynet
COPY shynet .
RUN python manage.py collectstatic --noinput && \
python manage.py compilemessages
# Launch
USER appuser
EXPOSE 8080
CMD [ "./webserver.sh" ]
HEALTHCHECK CMD bash -c 'wget -o /dev/null -O /dev/null --header "Host: ${ALLOWED_HOSTS%%,*}" "http://127.0.0.1:${PORT:-8080}/healthz/?format=json"'
CMD [ "./entrypoint.sh" ]

256
GUIDE.md Normal file
View File

@ -0,0 +1,256 @@
# Usage Guide
## Table of Contents
- [Installation](#installation)
- [Heroku](#heroku)
- [Render](#render)
- [Updating Your Configuration](#updating-your-configuration)
- [Advanced Usage](#advanced-usage)
* [Configuring a Reverse Proxy](#configuring-a-reverse-proxy)
+ [Cloudflare](#cloudflare)
+ [Nginx](#nginx)
* [Health Checks](#health-checks)
* [Primary Key Integration](#primary-key-integration)
* [Usage with Single-Page Applications](#usage-with-single-page-applications)
+ [Troubleshooting](#troubleshooting)
---
## Staying Updated
**If you install Shynet, you should strongly consider enabling notifications when new versions are released.** You can do this under the "Watch" tab on GitHub (above). This will ensure that you are notified when new versions are available, some of which may be security updates. (Shynet will never automatically update itself.)
> **When you do update, read the release notes!** These will tell you if you need to make changes to your deployment. (E.g., Shynet 0.13.1 requires additional configuration.)
## Installation
Installation of Shynet is easy! Follow the [Basic Installation](#basic-installation) guide or the [Basic Installation with Docker Compose](#basic-installation-with-docker-compose) below for a minimal installation, or if you are going to be running Shynet over HTTPS through a reverse proxy.
> **These commands assume Ubuntu.** If you're installing Shynet on a different platform, the process will be different.
Before continuing, please be sure to have the latest version of Docker installed.
### Basic Installation
1. Pull the latest version of Shynet using `docker pull milesmcc/shynet:latest`. If you don't have Docker installed, [install it](https://docs.docker.com/get-docker/).
2. For database you can use either PostgreSQL or SQLite:
2.1 To use PostgreSQL you need a server ready to go. This can be on the same machine as the deployment, or elsewhere. You'll need a username, password, host, and port, set in the appropriate `DB_` environment variables (see next). (For info on how to setup a PostgreSQL server on Ubuntu, follow [this guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04)).
2.2 SQLite doesn't need a server, just a file. Set `SQLITE=True` in the environment file and create a Docker volume to hold the persistent DB with `docker volume create shynet_db`. Then whenever you run the container include `-v shynet_db:/var/local/shynet/db:rw` to mount the volume into the container. See the [Docker documentation on volumes](https://docs.docker.com/storage/volumes/).
3. Configure an environment file for Shynet, using [this file](/TEMPLATE.env) as a template. (This file is typically named `.env`.) Make sure you set the database settings, or Shynet won't be able to run. Also consider setting `ALLOWED_HOSTS` inside the environment file to your deployment's domain for better security.
4. Launch the Shynet server for the first time by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. Provided you're using the default environment information (i.e., `PERFORM_CHECKS_AND_SETUP` is `True`), you'll see a few warnings about not having an admin user or host setup; these are normal. Don't worry — we'll do this in the next step. You only need to stop if you see a stacktrace about being unable to connect to the database.
5. Create an admin user by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py registeradmin <your email>`. A temporary password will be printed to the console.
6. Set the whitelabel of your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: `"My Shynet Instance"` or `"Acme Analytics"`.)
7. Launch your webserver by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. You may need to bind Docker's port 8080 (where Shynet runs) to your local port 80 (http); this can be done using the flag `-p 80:8080` after `run`. Visit your service's homepage, and verify everything looks right! You should see a login prompt. Log in with the credentials from step 5. You'll probably be prompted to "confirm your email"—if you haven't set up an email server, the confirmation email will be printed to the console instead.
8. Create a service by clicking "+ Create Service" in the top right hand corner. Fill out the options as appropriate. Once you're done, press "create" and you'll be redirected to your new service's analytics page.
9. Finally, click on "Manage" in the top right of the service's page to get the tracking script code. Inject this script on all pages you'd like the service to track.
### Basic Installation with Docker Compose
> Make sure you have `docker-compose` installed. If not, [install it](https://docs.docker.com/compose/install/)
1. Clone the repository.
2. Using [TEMPLATE.env](/TEMPLATE.env) as a template, configure the environment for your Shynet instance and place the modified config in a file called `.env` in the root of the repository. Do _not_ change the port number at the end; you can set the public facing port in the next step.
3. On line 2 of the `nginx.conf` file located in the root of the repository, replace `example.com` with your hostname. Then, in the `docker-compose.yml` file, set the port number by replacing `8080` in line 38 ( `- 8080:80` ) with whatever local port you want to bind it to. For example, set the port number to `- 80:80` if you want your site will be available via HTTP (port 80) at `http://<your hostname>`.
4. Launch the Shynet server for the first time by running `docker-compose up -d`. If you get an error like "permission denied" or "Couldn't connect to Docker daemon", either prefix the command with `sudo` or add your user to the `docker` group.
5. Create an admin user by running `docker exec -it shynet_main ./manage.py registeradmin <your email>`. A temporary password will be printed to the console.
6. Set the whitelabel of your Shynet instance by running `docker exec -it shynet_main ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: "My Shynet Instance" or "Acme Analytics".)
Your site should now be accessible at `http://hostname:port`. Now you can follow steps 9-10 of the [Basic Installation](#basic-installation) guide above to get Shynet integrated on your sites.
## Heroku
You may wish to deploy Shynet on Heroku. Note that Heroku's free offerings (namely the free Postgres addon) are unlikely to support running any Shynet instance that records more than a few hundred requests per day &mdash; the database will quickly fill up. In most cases, the more cost-effective option for running Shynet is renting a VPS from a full cloud service provider. However, if you're sure Heroku is the right option for you, or you just want to try Shynet out, you can use the Quick Deploy button then follow the steps below.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/milesmcc/shynet/tree/master)
Once you deploy, you'll need to setup an admin user and whitelabel before you can use Shynet. Do that with the following commands:
1. `heroku run --app=<your app> ./manage.py registeradmin <your email>`
2. `heroku run --app=<your app> ./manage.py whitelabel "<your Shynet instance's name>"`
## Render
[Render](https://render.com) is a modern cloud platform to build and run all your apps and websites with free SSL, a global CDN, private networks and auto deploys from Git. To deploy Shynet, click the `Deploy to Render` button and follow the steps below.
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/render-examples/shynet)
Once your deploy has completed, use the **Render Shell** to configure your app:
1. Set your email: `./manage.py registeradmin your-email@example.com`
2. Set your whitelabel: `./manage.py whitelabel "Your Shynet Instance Name"`
See the [Render docs](https://render.com/docs/deploy-shynet) for more information on deploying your application on Render.
---
## Advanced Usage
### Configuring a Reverse Proxy
A reverse proxy has many benefits. It can be used for DDoS protection, caching files to reduce server load, routing HTTPS and/or HTTP connections, hosting multiple services on a single server, [and more](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/)!
#### Cloudflare
[Cloudflare](https://www.cloudflare.com/) is a great reverse proxy option. It's free, automatically configures HTTPs, offers out-of-the-box security features, provides DNS, and requires minimal setup.
1. Follow Cloudflare's [getting started guide](https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website).
2. After setting up Cloudflare, here are a few things you should consider doing:
* Under the `SSL` Tab > `Overview` > Change your `SSL/TLS Encryption Mode` to `Flexible`
* The following will block your admin panel from anyone who isn't on your IP address. This is optional, but great for security.
* Under the `Firewall` tab > `Overview` > `+ Create Firewall Rule`:
* Name: `Admin Panel Restriction`
* Field: `URI Path`
* Operator: `equals`
* Value: `/admin`
* Click `AND`
* Field: `IP Address`
* Operator: `does not equal`
* Value: `<your public IP address>`
* Then: `Block`
#### Nginx
Nginx is a self hosted, highly configurable webserver. Nginx can be configured to run as a reverse proxy on either the same machine or a remote machine.
> **These commands assume Ubuntu.** If you're installing Nginx on a different platform, the process will be different.
0. Before starting, shut down your Docker containers (if any are running)
* Run `docker container ls` to find the container ID
* Run `docker stop <container id from the last step>`
1. Update your packages and install Nginx
* `sudo apt-get update`
* `sudo apt-get install nginx`
2. Disable the default Nginx placeholder
* `sudo unlink /etc/nginx/sites-enabled/default`
3. Create the Nginx reverse proxy config file
* `cd /etc/nginx/sites-available/`
* `vi reverse-proxy.conf` or `nano reverse-proxy.conf`
* Paste the following configuration into that file:
```nginx
# Know what you're pasting! Read the Reference!
# Reference: https://nginx.org/en/docs/
server {
listen 80;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://127.0.0.1:8080;
}
}
```
* Save and exit the text editor
* `:wq` for vi
* `ctrl+x` then `y` for nano
* Link Nginx's `sites-enabled` to read the new config
* `sudo ln -s /etc/nginx/sites-available/reverse-proxy.conf /etc/nginx/sites-enabled/reverse-proxy.conf`
* Make sure the config is working
* `service nginx configtest`
* `service nginx restart`
4. Restart your Docker image, but this time use `8080` as the local bind port, as that's where we configured Nginx to look
* `cd ~/`
* `docker run -p 8080:8080 --env-file=<your env file> milesmcc/shynet:latest`
5. Finally, time to test!
* Go to `http://<your site>/admin`
6. If everything is working as expected, please read through some of the following links below to customize Nginx
* [How to add SSL/HTTPS to Nginx (Ubuntu 18.04)](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04)
* [How to add SSL/HTTPS to Nginx (Ubuntu 16.04)](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04)
* [Nginx Documentation](https://nginx.org/en/docs/)
### Health Checks
By default, Shynet includes a default health check endpoint at `/healthz/`. If the instance is running normally, this endpoint will return an HTTP status code of 200; if something is wrong, it will have a non-200 status code. To view the health data as JSON, send your request to `/healthz/?format=json`.
This feature is helpful when running Shynet with Kubernetes, as it allows you to setup [startup readiness probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) that prevent traffic from being sent to your Shynet instances before they are ready.
### Primary-Key Integration
In some cases, it is useful to associate particular users on your platform with their sessions in Shynet. In Shynet, this is called _primary key integration_, and is done by adding an additional element to the Shynet script url for each particular user.
If the Shynet script location (for either the pixel or the script) is, for example, `//shynet.example.com/ingress/your_service_uuid/pixel.gif` and `//shynet.example.com/ingress/your_service_uuid/script.js`, the URLs for primary-key enabled users would be `//shynet.example.com/ingress/your_service_uuid/USER_PRIMARY_KEY/pixel.gif` and `//shynet.example.com/ingress/your_service_uuid/USER_PRIMARY_KEY/script.js`.
Adding this path can be done easily using server-side rendering. For example, here is a Django template that adds users' primary keys to the Shynet tracking script:
```html
{% if request.user.is_authenticated %}
<noscript>
<img src="//shynet.example.com/ingress/service-uuid/{{request.user.email|urlencode:""}}/pixel.gif">
</noscript>
<script src="//shynet.example.com/ingress/service-uuid/{{request.user.email|urlencode:""}}/script.js"></script>
{% else %}
<noscript>
<img src="//shynet.example.com/ingress/service-uuid/pixel.gif">
</noscript>
<script src="//shynet.example.com/ingress/service-uuid/script.js"></script>
{% endif %}
```
### Usage with Single-Page Applications
In a single-page application, the page never reloads. (That's the entire point of single-page applications, after all!) Unfortunately, this also means that Shynet will not automatically recognize and track when the user navigates between pages _within_ your application.
Fortunately, Shynet offers a simple method you can call from anywhere within your JavaScript to indicate that a new page has been loaded: `Shynet.newPageLoad()`. Add this method call to the code that handles routing in your app, and you'll be ready to go.
### API
All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/v1/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's personal API token (```'Authorization: Token <user API token>'```).
There are 3 optional query parameters:
* `uuid` - to get data only from one service
* `startDate` - to set start date in format YYYY-MM-DD
* `endDate` - to set end date in format YYYY-MM-DD
Example in HTTPie:
```http get '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{user_api_token}}'```
Example in cURL:
```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01'```
---
## Troubleshooting
Here are solutions for some common issues. If your situation isn't described here or the solution didn't work, feel free to [create an issue](https://github.com/milesmcc/shynet/issues/new) (but be sure to check for duplicate issues first).
#### The admin panel works, but no page views are showing up!
* If you are running a single Shynet webserver instance (i.e., you followed the default installation instructions), verify that you haven't set `CELERY_TASK_ALWAYS_EAGER` to `False` in your environment file.
* Verify that your cache is properly configured. In single-instance deployments, this means making sure that you haven't set any `REDIS_*` or `CELERY_*` environment variables (those are for more advanced deployments; you'll just want the defaults).
* If your service is configured to respect Do Not Track (under "Advanced Settings"), verify that your browser isn't sending the `DNT=1` header with your requests (or temporarily disable DNT support in Shynet while testing). Sometimes, an adblocker or privacy browser extension will add this header to requests unexpectedly.
#### Shynet isn't linking different pageviews from the same visitor into a single session!
* Verify that your cache is properly configured. (See #2 above.) In multi-instance deployments, it's critical that all webservers are using the _same_ cache—so make sure you configure a Redis cache if you're using a non-default installation.
* This can happen between Shynet restarts if you're not using an external cache provider (like Redis).
#### I changed the `SHYNET_WHITELABEL`/`SHYNET_HOST` environment variable, but nothing happened!
* Those values only affect how your Shynet instance is setup on first run; once it's configured, they have no effect. See [updating your configuration](#updating-your-configuration) for help on how to update your configuration. (Note: these environment variables are not present in newer Shynet versions; they have been removed from the guide.)
#### Shynet can't connect to my database running on `localhost`/`127.0.0.1`
* The problem is likely that to Shynet, `localhost` points to the local network in the container itself, not on the host machine. Try adding the `--network='host'` option when you run Docker.

28
Pipfile
View File

@ -1,28 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
black = "*"
[packages]
django = "*"
django-allauth = "*"
geoip2 = "*"
whitenoise = "*"
celery = "*"
django-ipware = "*"
pyyaml = "*"
ua-parser = "*"
user-agents = "*"
emoji-country-flag = "*"
rules = "*"
gunicorn = "*"
psycopg2-binary = "*"
redis = "*"
django-redis-cache = "*"
pycountry = "*"
[pipenv]
allow_prereleases = true

401
Pipfile.lock generated
View File

@ -1,401 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "2cd3e33ea333a40476d2030b6be66826e93c3a4de67032655061725835c92f09"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"amqp": {
"hashes": [
"sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8",
"sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"
],
"version": "==2.5.2"
},
"asgiref": {
"hashes": [
"sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
"sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
],
"version": "==3.2.7"
},
"billiard": {
"hashes": [
"sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede",
"sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"
],
"version": "==3.6.3.0"
},
"celery": {
"hashes": [
"sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f",
"sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a"
],
"index": "pypi",
"version": "==4.4.2"
},
"certifi": {
"hashes": [
"sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
"sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
],
"version": "==2020.4.5.1"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"defusedxml": {
"hashes": [
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
],
"version": "==0.6.0"
},
"django": {
"hashes": [
"sha256:642d8eceab321ca743ae71e0f985ff8fdca59f07aab3a9fb362c617d23e33a76",
"sha256:d4666c2edefa38c5ede0ec1655424c56dc47ceb04b6d8d62a7eac09db89545c1"
],
"index": "pypi",
"version": "==3.0.5"
},
"django-allauth": {
"hashes": [
"sha256:7ab91485b80d231da191d5c7999ba93170ef1bf14ab6487d886598a1ad03e1d8"
],
"index": "pypi",
"version": "==0.41.0"
},
"django-ipware": {
"hashes": [
"sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"
],
"index": "pypi",
"version": "==2.1.0"
},
"django-redis-cache": {
"hashes": [
"sha256:06d4e48545243883f88dc9263dda6c8a0012cb7d0cee2d8758d8917eca92cece",
"sha256:b19ee6654cc2f2c89078c99255e07e19dc2dba8792351d76ba7ea899d465fbb0"
],
"index": "pypi",
"version": "==2.1.1"
},
"emoji-country-flag": {
"hashes": [
"sha256:67c0cb6a3765fb53f31b34160d6b1c8a5f44b297bc278d1835c6f2e5b0a9a592",
"sha256:ae7edb38077b0840210fa9e37673f481f2b9c032446e13ad6dab2b1108cd7ad6"
],
"index": "pypi",
"version": "==1.2.1"
},
"geoip2": {
"hashes": [
"sha256:5869e987bc54c0d707264fec4710661332cc38d2dca5a7f9bb5362d0308e2ce0",
"sha256:99ec12d2f1271a73a0a4a2b663fe6ce25fd02289c0a6bef05c0a1c3b30ee95a4"
],
"index": "pypi",
"version": "==3.0.0"
},
"gunicorn": {
"hashes": [
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
],
"index": "pypi",
"version": "==20.0.4"
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
],
"version": "==2.9"
},
"kombu": {
"hashes": [
"sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76",
"sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2"
],
"version": "==4.6.8"
},
"maxminddb": {
"hashes": [
"sha256:d0ce131d901eb11669996b49a59f410efd3da2c6dbe2c0094fe2fef8d85b6336"
],
"version": "==1.5.2"
},
"oauthlib": {
"hashes": [
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
],
"version": "==3.1.0"
},
"psycopg2-binary": {
"hashes": [
"sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac",
"sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a",
"sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5",
"sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04",
"sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1",
"sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5",
"sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce",
"sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434",
"sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9",
"sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057",
"sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98",
"sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522",
"sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505",
"sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa",
"sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3",
"sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f",
"sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4",
"sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4",
"sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266",
"sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66",
"sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38",
"sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3",
"sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389",
"sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab",
"sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb",
"sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6",
"sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d",
"sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162",
"sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e",
"sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd"
],
"index": "pypi",
"version": "==2.8.5"
},
"pycountry": {
"hashes": [
"sha256:3c57aa40adcf293d59bebaffbe60d8c39976fba78d846a018dc0c2ec9c6cb3cb"
],
"index": "pypi",
"version": "==19.8.18"
},
"python3-openid": {
"hashes": [
"sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa",
"sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502"
],
"version": "==3.1.0"
},
"pytz": {
"hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.3"
},
"pyyaml": {
"hashes": [
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
],
"index": "pypi",
"version": "==5.3.1"
},
"redis": {
"hashes": [
"sha256:0dcfb335921b88a850d461dc255ff4708294943322bd55de6cfd68972490ca1f",
"sha256:b205cffd05ebfd0a468db74f0eedbff8df1a7bfc47521516ade4692991bb0833"
],
"index": "pypi",
"version": "==3.4.1"
},
"requests": {
"hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
],
"version": "==2.23.0"
},
"requests-oauthlib": {
"hashes": [
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
],
"version": "==1.3.0"
},
"rules": {
"hashes": [
"sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"
],
"index": "pypi",
"version": "==2.2"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
"version": "==1.14.0"
},
"sqlparse": {
"hashes": [
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
"sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
],
"version": "==0.3.1"
},
"ua-parser": {
"hashes": [
"sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a",
"sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033"
],
"index": "pypi",
"version": "==0.10.0"
},
"urllib3": {
"hashes": [
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
],
"version": "==1.25.9"
},
"user-agents": {
"hashes": [
"sha256:da54371d856c35d8ead0622da24ad5ef6d667eda3629a750e3373a3e847a054b",
"sha256:e727ab6f169e829bc25d41dbd25b9ff679b4631bd81959bcf7de1e246da67194"
],
"index": "pypi",
"version": "==2.1"
},
"vine": {
"hashes": [
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
],
"version": "==1.3.0"
},
"whitenoise": {
"hashes": [
"sha256:0f9137f74bd95fa54329ace88d8dc695fbe895369a632e35f7a136e003e41d73",
"sha256:62556265ec1011bd87113fb81b7516f52688887b7a010ee899ff1fd18fd22700"
],
"index": "pypi",
"version": "==5.0.1"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"black": {
"hashes": [
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
],
"index": "pypi",
"version": "==19.10b0"
},
"click": {
"hashes": [
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
],
"version": "==7.1.1"
},
"pathspec": {
"hashes": [
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
"sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
],
"version": "==0.8.0"
},
"regex": {
"hashes": [
"sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b",
"sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8",
"sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3",
"sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e",
"sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683",
"sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1",
"sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142",
"sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3",
"sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468",
"sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e",
"sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3",
"sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a",
"sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f",
"sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6",
"sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156",
"sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b",
"sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db",
"sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd",
"sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a",
"sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948",
"sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"
],
"version": "==2020.4.4"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"typed-ast": {
"hashes": [
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
],
"version": "==1.4.1"
}
}
}

117
README.md
View File

@ -1,15 +1,18 @@
<p align="center">
<h3 align="center">🔭 Shynet 🔭</h3>
<img align="center" src="images/logo.png" height="50" alt="Shynet logo">
<br>
<p align="center">
Web analytics that's self hosted, cookie free, privacy friendly, and useful(?)
<br>
<br>
<a href="#installation"><strong>Getting started »</strong></a>
Modern, privacy-friendly, and cookie-free web analytics.
<br>
<strong><a href="#installation">Getting started »</a></strong>
</p>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#features">Features</a> &bull; <a href="https://miles.land/officehours/">Office Hours</a></p>
</p>
<br>
## Motivation
There are a _lot_ of web analytics tools. Unfortunately, most of them come with the following caveats:
@ -31,16 +34,21 @@ _Note: These screenshots have been edited to hide sensitive data. The "real" Shy
![Shynet's homepage](images/homepage.png)
_Shynet's homepage, where you can see all of your services at a glance._
Not shown: service view, management view, session view, full service view. (You'll need to install Shynet to see those!)
![A service page](images/service.png)
_A real service page, where you can see higher-level details about a site._
Not shown: management view, session view, full service view. (You'll need to install Shynet for yourself to see those!)
> **Shynet is built using [a17t](https://github.com/milesmcc/a17t),** an atomic design library. Customization and extension is simple; [learn more about a17t](https://github.com/milesmcc/a17t).
## Features
#### Architecture
* **Runs on a single machine** &mdash; Because it's so small, Shynet can easily run as a single docker container on a single small VPS.
* **...or across a giant Kubernetes cluster** &mdash; For higher traffic installations, Shynet can be deployed with as many parallelized ingress nodes as needed, with Redis caching and separate backend workers for database IO.
* **Built using Django** &mdash; Shynet is built using Django, so deploying, updating, and migrating can be done without headaches.
* **Multiple users and sites** &mdash; A single Shynet instance can support multiple users, each tracking multiple different sites.
* **Runs on a single machine** &mdash; Because it's so small, Shynet can easily run as a single docker container on a single small VPS
* **...or across a giant Kubernetes cluster** &mdash; For higher traffic installations, Shynet can be deployed with as many parallelized ingress nodes as needed, with Redis caching and separate backend workers for database IO
* **Built using Django** &mdash; Shynet is built using Django, so deploying, updating, and migrating can be done without headaches
* **Multiple users and sites** &mdash; A single Shynet instance can support multiple users, each tracking multiple different sites
#### Tracking
@ -67,11 +75,11 @@ Here's the information Shynet can give you about your visitors:
#### Workflow
* **Collaboration built-in** &mdash; Administrators can easily share services with other users, as well
* **Accounts (or not)** &mdash; Shynet has a fully featured account management workflow (powered by [Django Allauth](https://github.com/pennersr/django-allauth/)).
* **Accounts (or not)** &mdash; Shynet has a fully featured account management workflow (powered by [Django Allauth](https://github.com/pennersr/django-allauth/))
## Recommendations
Shynet isn't for everyone. It's great for personal projects and small to medium size websites, but hasn't been tested with ultra-high traffic sites. It's also requires a fair amount of technical know-how to deploy and maintain, so if you need a one-click solution, you're best served with other tools.
Shynet isn't for everyone. It's great for personal projects and small to medium size websites, but hasn't been tested with ultra-high traffic sites. It also requires a fair amount of technical know-how to deploy and maintain, so if you need a one-click solution, you're best served with other tools.
## Concepts
@ -85,88 +93,21 @@ Shynet is pretty simple, but there are a few key terms you need to know in order
## Installation
To install Shynet using the simplest possible setup, follow these instructions. Instructions for multi-machine deployments will be available soon.
> **These commands assume Ubuntu.** If you're installing Shynet on a different platform, the process will be different.
1. Pull the latest version of Shynet using `docker pull milesmcc/shynet:latest`. If you don't have Docker installed, [install it](https://docs.docker.com/get-docker/).
2. Have a PostgreSQL server ready to go. This can be on the same machine as the deployment, or elsewhere. You'll just need a username, password, and host. (For info on how to setup a PostgreSQL server on Ubuntu, follow [this guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04)).
3. Configure an environment file for Shynet. (For example, create a file called `.env`.) Be sure to swap out the variables below with the correct values for your setup. (The comments refer to the lines that follow. Note that Docker is weird with quotes, so it tends to be better to omit them from your env file.)
```
# Database
DB_NAME=<your db name>
DB_USER=<your db user>
DB_PASSWORD=<your db user password>
DB_HOST=<your db host>
# General Django settings
DJANGO_SECRET_KEY=<your Django secret key; just a random string>
# Don't leak error details to visitors, very important
DEBUG=False
CELERY_TASK_ALWAYS_EAGER=True
# For better security, set this to your deployment's domain. Comma separated.
ALLOWED_HOSTS=*
# Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended)
SIGNUPS_ENABLED=False
# Change as required
TIME_ZONE=America/New_York
# Set to "False" if you will not be serving content over HTTPS
SCRIPT_USE_HTTPS=True
```
For more advanced deployments, you may consider adding the following settings to your environment file. **The following settings are optional, and not required for simple deployments.**
```env
# Email settings
EMAIL_HOST_USER=<your SMTP email user>
EMAIL_HOST_PASSWORD=<your SMTP email password>
EMAIL_HOST=<your SMTP email hostname>
SERVER_EMAIL=Shynet <noreply@shynet.example.com>
# Redis and queue settings; not necessary for single-instance deployments
REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
# If set, make sure CELERY_TASK_ALWAYS_EAGER is False
CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
```
4. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py migrate`.
5. Create your admin account by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py registeradmin <your email>`. The command will print a temporary password that you'll be able to use to log in.
6. Configure Shynet's hostname (e.g. `shynet.example.com` or `localhost:8000`) by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py hostname "<your hostname>"`. This doesn't affect Shynet's bind port; instead, it determines what hostname to inject into the tracking script. (So you'll want to use the "user-facing" hostname here.)
7. Name your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py whitelabel "<your instance name>"`. This could be something like "My Shynet Server" or "Acme Analytics"—whatever suits you.
8. Launch the Shynet server by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. You may need to bind Docker's port 8000 (where Shynet runs) to your local port 8000; this can be done using the flag `-p 8080:8080`.
9. Visit your service's homepage, and verify everything looks right! You should see a login prompt. Log in with the credentials from step 5. You'll probably be prompted to "confirm your email"—if you haven't set up an email server, the confirmation email will be printed to the console instead.
10. Create a service by clicking "+ Create Service" in the top right hand corner. Fill out the options as appropriate. Once you're done, press "create" and you'll be redirected to your new service's analytics page.
11. Finally, click on "Manage" in the top right of the service's page to get the tracking script code. Inject this script on all pages you'd like the service to track.
**Next steps:** while out of the scope of this short guide, next steps include setting up Shynet behind a reverse proxy (be it your own [Nginx server](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) or [Cloudflare](https://cloudflare.com)), making it run in the background, and integrating it on your sites. Integration instructions are available on each service's management page.
You can find instructions on getting started and usage in the [Usage Guide](GUIDE.md#installation). Out of the box, we support deploying via a simple Docker container, docker-compose, Heroku, or Kubernetes (see [kubernetes](/kubernetes)).
## FAQ
**Does Shynet respond to Do Not Track (DNT) signals?** Yes. While there isn't any standardized way to handle DNT requests, Shynet allows you to specify whether you want to collect any data from users with DNT enabled on a per-service basis. (By default, Shynet will _not_ collect any data from users who specify DNT.)
**Is this GDPR compliant?** I think so, but it also depends on how you use it. If you're worried about GDPR, you should talk to a lawyer about your particular data collection practices. I'm not a lawyer. (And this isn't legal advice.)
**Is this GDPR compliant?** It depends on how you use it. If you're worried about GDPR, you should talk to a lawyer about your particular data collection practices. I'm not a lawyer. (And this isn't legal advice.)
## Troubleshooting
Having trouble with Shynet? Check out the [troubleshooting guide](GUIDE.md#troubleshooting), or [create an issue](https://github.com/milesmcc/shynet/issues/new) if you think you found a bug in Shynet itself (or have a feature suggestion).
## Roadmap
The following features are planned:
* **Rollups** (aggregate old data to save space)
* **Anomaly detection** (get email alerts when you get a traffic spike or dip)
* **Interactive traffic heatmap** (see where in the world your visitors are coming from)
* **Better collaboration interface** (the current interface is... a draft)
* **Data deletion tool** (easily prune user data by specifying an ID or IP)
* **Differential privacy** (explore and share your data without revealing any personal information)
To see the upcoming planned features, check out the repository's [roadmap project](https://github.com/milesmcc/shynet/projects/1). Upcoming features include data aggregation through rollups, anomaly detection, detailed data exports, two-factor authentication, and a data deletion tool.
## In the Wild
@ -174,7 +115,7 @@ These sites use Shynet to monitor usage without violating visitors' privacy: [Po
## Contributing
Are you interested in contributing to Shynet? Just send a pull request! Maybe once the project matures there will be more detailed contribution guidelines, but for now just send the code this way. Just know that by contributing, you agree to share all of your contributions under the same license as the project (see [LICENSE](LICENSE)).
Are you interested in contributing to Shynet? Just send a pull request! Maybe once the project matures there will be more detailed contribution guidelines, but for now just send the code this way and we'll make sure it meets our standards together. Just know that by contributing, you agree to share all of your contributions under the same license as the project (see [LICENSE](LICENSE)). And always be sure to follow the [Code of Conduct](https://github.com/milesmcc/shynet/blob/master/CODE_OF_CONDUCT.md).
## License
@ -182,4 +123,4 @@ Shynet is made available under the [Apache License, version 2.0](LICENSE).
---
a17t was created by [Miles McCain](https://miles.land) at the [Recurse Center](https://recurse.com) using [a17t](https://a17t.miles.land).
Shynet was created by [Miles McCain](https://miles.land) ([@MilesMcCain](https://twitter.com/MilesMcCain)) at the [Recurse Center](https://recurse.com) using [a17t](https://a17t.miles.land).

112
TEMPLATE.env Normal file
View File

@ -0,0 +1,112 @@
# This file shows all of the environment variables you can
# set to configure Shynet, as well as information about their
# effects. Make a copy of this file to configure your deployment.
# Database settings (PostgreSQL)
DB_NAME=shynet_db
DB_USER=shynet_db_user
DB_PASSWORD=shynet_db_user_password
DB_HOST=db
DB_PORT=5432
# Database settings (SQLite) - comment PostgreSQL settings
# SQLITE=True
# DB_NAME=/var/local/shynet/db/db.sqlite3
# Email settings (optional)
EMAIL_HOST_USER=example
EMAIL_HOST_PASSWORD=example_password
EMAIL_HOST=smtp.example.com
EMAIL_PORT=465
EMAIL_USE_SSL=True
# Comment out EMAIL_USE_SSL & uncomment EMAIL_USE_TLS if your SMTP server uses TLS.
# EMAIL_USE_TLS=True
SERVER_EMAIL=Shynet <noreply@shynet.example.com>
# General Django settings - to generate run: python3 -c "import secrets; print(secrets.token_urlsafe())"
DJANGO_SECRET_KEY=random_string
# Set these to your deployment's domain. Both are comma separated, but CSRF_TRUSTED_ORIGINS also requires a scheme (e.g., `https://`).
ALLOWED_HOSTS=example.com
CSRF_TRUSTED_ORIGINS=https://example.com
# Localization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE=en-us
# Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended)
ACCOUNT_SIGNUPS_ENABLED=False
# Should user email addresses be verified? Only set this to `required` if you've setup the email settings and allow
# public sign-ups; otherwise, it's unnecessary.
ACCOUNT_EMAIL_VERIFICATION=none
# The timezone of the admin panel. Affects how dates are displayed.
# This must match a value from the IANA's tz database.
# Wikipedia has a list of valid strings: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
TIME_ZONE=America/New_York
# Set to "False" if you will not be serving content over HTTPS
SCRIPT_USE_HTTPS=True
# How frequently should the monitoring script "phone home" (in ms)?
SCRIPT_HEARTBEAT_FREQUENCY=5000
# How much time can elapse between requests from the same user before a new
# session is created, in seconds?
SESSION_MEMORY_TIMEOUT=1800
# Should only superusers (admins) be able to create services? This is helpful
# when you'd like to invite others to your Shynet instance but don't want
# them to be able to create services of their own.
ONLY_SUPERUSERS_CREATE=True
# Whether to perform checks and setup at startup, including applying unapplied
# migrations. For most setups, the recommended value is True. Defaults to True.
# Will skip only if value is False.
PERFORM_CHECKS_AND_SETUP=True
# The port that Shynet should bind to. Don't set this if you're deploying on Heroku.
PORT=8080
# Set to "False" if you do not want the version to be displayed on the frontend.
SHOW_SHYNET_VERSION=True
# Redis, queue, and parellization settings; not necessary for single-instance deployments.
# Don't uncomment these unless you know what you are doing!
# NUM_WORKERS=1
# Make sure you set a REDIS_CACHE_LOCATION if you have more than one frontend worker/instance.
# REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
# If CELERY_BROKER_URL is set, make sure CELERY_TASK_ALWAYS_EAGER is False and
# that you have a separate queue consumer running somewhere via `celeryworker.sh`.
# CELERY_TASK_ALWAYS_EAGER=False
# CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
# Should Shynet show third-party icons in the dashboard?
SHOW_THIRD_PARTY_ICONS=True
# Should Shynet block collection of IP addresses globally?
BLOCK_ALL_IPS=False
# Should Shynet include the date and site ID when hashing users?
# This will prevent any possibility of cross-site tracking provided
# that IP collection is also disabled, and external keys (primary
# keys) aren't supplied. It will also prevent sessions from spanning
# one day to another.
AGGRESSIVE_HASH_SALTING=True
# Custom location url to link to in frontend.
# $LATITUDE will get replaced by the latitude, $LONGITUDE will get
# replaced by the longitude.
# Examples:
# - https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE (default)
# - https://www.google.com/maps/search/?api=1&query=$LATITUDE,$LONGITUDE
# - https://www.mapquest.com/near-$LATITUDE,$LONGITUDE
LOCATION_URL=https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE
# How many services should be displayed on dashboard page?
# Set to big number if you don't want pagination at all.
DASHBOARD_PAGE_SIZE=5
# Should background bars be scaled to full width?
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION=True

142
app.json Normal file
View File

@ -0,0 +1,142 @@
{
"name": "Shynet",
"description": "Modern, privacy-friendly, and detailed web analytics that works without cookies or JS.",
"keywords": [
"app.json",
"shynet",
"heroku",
"analytics",
"privacy",
"friendly"
],
"website": "https://github.com/milesmcc/shynet",
"repository": "https://github.com/milesmcc/shynet",
"logo": "https://github.com/milesmcc/shynet/raw/master/images/slogo.png",
"success_url": "/",
"stack": "container",
"addons": [
"heroku-postgresql:hobby-dev"
],
"formation": {
"web": {
"quantity": 1,
"size": "free"
}
},
"env": {
"DB_NAME": {
"description": "Postgres database name (not required if using Postgres addon)",
"value": "shynet",
"required": false
},
"DB_USER": {
"description": "Postgres database username (not required if using Postgres addon)",
"value": "",
"required": false
},
"DB_PASSWORD": {
"description": "Postgres database password (not required if using Postgres addon)",
"value": "",
"required": false
},
"DB_HOST": {
"description": "Postgres database hostname (not required if using Postgres addon)",
"value": "",
"required": false
},
"DB_PORT": {
"description": "Postgres database port (not required if using Postgres addon)",
"value": "5432",
"required": false
},
"EMAIL_HOST": {
"description": "SMTP server hostname (for sending emails)",
"value": "smtp.gmail.com",
"required": false
},
"EMAIL_PORT": {
"description": "SMTP server port (for sending emails)",
"value": "465",
"required": false
},
"EMAIL_HOST_USER": {
"description": "SMTP server username (for sending emails)",
"value": "",
"required": false
},
"EMAIL_HOST_PASSWORD": {
"description": "SMTP server password (for sending emails)",
"value": "",
"required": false
},
"SERVER_EMAIL": {
"description": "Email address (for sending emails)",
"value": "<Shynet> noreply@shynet.example.com",
"required": false
},
"DJANGO_SECRET_KEY": {
"description": "Django secret key",
"generator": "secret"
},
"ALLOWED_HOSTS": {
"description": "For better security, set this to your deployment's domain. (Where you will actually host, not embed, Shynet.) Set to '*' to allow serving all domains.",
"value": "*",
"required": false
},
"ACCOUNT_SIGNUPS_ENABLED": {
"description": "Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended).",
"value": "False",
"required": false
},
"TIME_ZONE": {
"description": "The timezone of the admin panel. Affects how dates are displayed.",
"value": "America/New_York",
"required": false
},
"SCRIPT_USE_HTTPS": {
"description": "Set to 'False' if you will not be serving Shynet over HTTPS.",
"value": "True",
"required": false
},
"SCRIPT_HEARTBEAT_FREQUENCY": {
"description": "How frequently should the monitoring script 'phone home' (in ms)?",
"value": "5000",
"required": false
},
"SESSION_MEMORY_TIMEOUT": {
"description": "How much time can elapse between requests from the same user before a new session is created, in seconds?",
"value": "1800",
"required": false
},
"ONLY_SUPERUSERS_CREATE": {
"description": "Should only superusers (admins) be able to create tracked services?",
"value": "True",
"required": false
},
"PERFORM_CHECKS_AND_SETUP": {
"description": "Whether to perform checks and setup at startup. Recommended value is 'True' for Heroku users.",
"value": "True",
"required": false
},
"SHOW_SHYNET_VERSION": {
"description": "Set to 'False' if you do not want the version to be displayed on the frontend.",
"value": "True",
"required": false
},
"LOCATION_URL": {
"description": "Custom location url to link to in frontend.",
"value": "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE",
"required": false
},
"DASHBOARD_PAGE_SIZE": {
"description": "How many services should be displayed on dashboard page?",
"value": "5",
"required": false
},
"USE_RELATIVE_MAX_IN_BAR_VISUALIZATION": {
"description": "Should background bars be scaled to full width?",
"value": "True",
"required": false
}
}
}

46
docker-compose.yml Normal file
View File

@ -0,0 +1,46 @@
version: '3'
services:
shynet:
container_name: shynet_main
image: milesmcc/shynet:latest
restart: unless-stopped
expose:
- 8080
env_file:
# Create a file called '.env' if it doesn't already exist.
# You can use `TEMPLATE.env` as a guide.
- .env
environment:
- DB_HOST=db
networks:
- internal
depends_on:
- db
db:
container_name: shynet_database
image: postgres
restart: always
environment:
- "POSTGRES_USER=${DB_USER}"
- "POSTGRES_PASSWORD=${DB_PASSWORD}"
- "POSTGRES_DB=${DB_NAME}"
volumes:
- shynet_db:/var/lib/postgresql/data
networks:
- internal
webserver:
container_name: shynet_webserver
image: nginx
restart: always
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 8080:80
depends_on:
- shynet
networks:
- internal
volumes:
shynet_db:
networks:
internal:

3
heroku.yml Normal file
View File

@ -0,0 +1,3 @@
build:
docker:
web: Dockerfile

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 263 KiB

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
images/service.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

BIN
images/slogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

View File

@ -16,13 +16,12 @@ spec:
app: "shynet-webserver"
spec:
containers:
- name: "covideo-webserver"
image: "milesmcc/shynet:latest"
command: ["./webserver.sh"]
- name: "shynet-webserver"
image: "milesmcc/shynet:edge" # Change to the version appropriate for you (e.g., :latest)
imagePullPolicy: Always
envFrom:
- secretRef:
name: django-settings
name: shynet-settings
---
apiVersion: "apps/v1"
kind: "Deployment"
@ -42,45 +41,79 @@ spec:
app: "shynet-celeryworker"
spec:
containers:
- name: "covideo-celeryworker"
image: "milesmcc/shynet:latest"
- name: "shynet-celeryworker"
image: "milesmcc/shynet:edge" # Change to the version appropriate for you (e.g., :latest)
command: ["./celeryworker.sh"]
imagePullPolicy: Always
envFrom:
- secretRef:
name: django-settings
name: shynet-settings
---
apiVersion: v1
kind: Service
metadata:
name: redis
name: shynet-redis
spec:
ports:
- port: 6379
name: redis
clusterIP: None
selector:
app: redis
app: shynet-redis
---
apiVersion: apps/v1beta2
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
name: shynet-redis
spec:
selector:
matchLabels:
app: redis
serviceName: redis
app: shynet-redis
serviceName: shynet-redis
replicas: 1
template:
metadata:
labels:
app: redis
app: shynet-redis
spec:
containers:
- name: redis
- name: shynet-redis
image: redis:latest
imagePullPolicy: Always
ports:
- containerPort: 6379
name: redis
---
apiVersion: v1
kind: Service
metadata:
name: shynet-webserver-service
spec:
type: ClusterIP
ports:
- port: 8080
selector:
app: shynet-webserver
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shynet-webserver-ingress
annotations:
kubernetes.io/ingress.class: addon-http-application-routing
spec:
rules:
- host: shynet.rmrm.io
http:
paths:
- backend:
serviceName: shynet-webserver-service
servicePort: 8080
path: /
- host: shynet-beta.rmrm.io
http:
paths:
- backend:
serviceName: shynet-webserver-service
servicePort: 8080
path: /

View File

@ -1,19 +1,19 @@
apiVersion: v1
kind: Secret
metadata:
name: django-settings
name: shynet-settings
type: Opaque
stringData:
# Django settings
DEBUG: "False"
ALLOWED_HOSTS: "*" # For better security, set this to your deployment's domain. Comma separated.
DJANGO_SECRET_KEY: ""
SIGNUPS_ENABLED: "False"
ACCOUNT_SIGNUPS_ENABLED: "False"
TIME_ZONE: "America/New_York"
# Redis configuration (if you use the default Kubernetes config, this will work)
REDIS_CACHE_LOCATION: "redis://redis.default.svc.cluster.local/0"
CELERY_BROKER_URL: "redis://redis.default.svc.cluster.local/1"
REDIS_CACHE_LOCATION: "redis://shynet-redis.default.svc.cluster.local/0"
CELERY_BROKER_URL: "redis://shynet-redis.default.svc.cluster.local/1"
# PostgreSQL settings
DB_NAME: ""

19
nginx.conf Normal file
View File

@ -0,0 +1,19 @@
server {
server_name example.com;
access_log /var/log/nginx/bin.access.log;
error_log /var/log/nginx/bin.error.log error;
location / {
proxy_pass http://shynet:8080;
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Url-Scheme $scheme;
}
listen 80;
}

1261
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "shynet",
"description": "Modern, privacy-friendly, and cookie-free web analytics.",
"repository": {
"type": "git",
"url": "git+https://github.com/milesmcc/shynet.git"
},
"keywords": [
"privacy",
"analytics",
"self-host"
],
"author": "R. Miles McCain <shynet@sendmiles.email>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/milesmcc/shynet/issues"
},
"homepage": "https://github.com/milesmcc/shynet#readme",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"a17t": "^0.5.1",
"apexcharts": "^3.24.0",
"datamaps": "^0.5.9",
"flag-icon-css": "^3.5.0",
"inter-ui": "^3.15.0",
"litepicker": "^2.0.11"
}
}

1752
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

44
pyproject.toml Normal file
View File

@ -0,0 +1,44 @@
[tool.poetry]
name = "shynet"
version = "0.13.1"
description = "Modern, privacy-friendly, and cookie-free web analytics."
authors = ["R. Miles McCain <github@sendmiles.email>"]
license = "Apache-2.0"
[tool.poetry.dependencies]
python = "^3.8"
Django = "^4"
django-allauth = "^0.45.0"
geoip2 = "^4.2.0"
whitenoise = "^5.3.0"
celery = "^5.2.2"
django-ipware = "^4.0.2"
PyYAML = "^5.4.1"
user-agents = "^2.2.0"
rules = "^3.0"
gunicorn = "^20.1.0"
psycopg2-binary = "^2.9.2"
redis = "^3.5.3"
django-redis-cache = "^3.0.0"
pycountry = "^20.7.3"
html2text = "^2020.1.16"
django-health-check = "^3.16.4"
django-npm = "^1.0.0"
python-dotenv = "^0.18.0"
django-debug-toolbar = "^3.2.1"
django-cors-headers = "^3.11.0"
[tool.poetry.dev-dependencies]
pytest-sugar = "^0.9.4"
factory-boy = "^3.2.0"
pytest-django = "^4.4.0"
django-coverage-plugin = "^2.0.0"
django-stubs = "^1.8.0"
mypy = "^0.910"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 88

View File

@ -0,0 +1,29 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: a17t/templates/a17t/includes/pagination.html:5
#: a17t/templates/a17t/includes/pagination.html:7
msgid "Previous"
msgstr "Zurück"
#: a17t/templates/a17t/includes/pagination.html:10
#: a17t/templates/a17t/includes/pagination.html:12
msgid "Next"
msgstr "Vor"

View File

@ -0,0 +1,29 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: a17t/templates/a17t/includes/pagination.html:5
#: a17t/templates/a17t/includes/pagination.html:7
msgid "Previous"
msgstr "上一頁"
#: a17t/templates/a17t/includes/pagination.html:10
#: a17t/templates/a17t/includes/pagination.html:12
msgid "Next"
msgstr "下一頁"

View File

@ -1,3 +0,0 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@0.1.3/dist/a17t.css">
<script async src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">

View File

@ -0,0 +1,12 @@
{% load static %}
<link rel="stylesheet" href="{% static 'a17t/dist/a17t.css' %}">
<script async src="{% static '@fortawesome/fontawesome-free/js/all.min.js' %}" data-mutate-approach="sync"></script>
<link href="{% static 'a17t/dist/tailwind.css' %}" rel="stylesheet">
<link href="{% static 'inter-ui/Inter (web)/inter.css' %}" rel="stylesheet">
<style>
:root {
--family-primary: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--family-secondary: var(--family-primary);
}
</style>

View File

@ -1 +1,2 @@
<label class="label" for="{{field.auto_id}}">{{ field.label }} {% if not field.field.required %}<span class="badge ~neutral">Optional</span>{% endif %}</label>
{% load i18n %}
<label class="label" for="{{field.auto_id}}">{% trans field.label %} {% if not field.field.required %}<span class="badge ~neutral">Optional</span>{% endif %}</label>

View File

@ -1,45 +1,46 @@
{% load i18n %}
<nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination">
<div class="w-full md:w-auto mb-2">
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}{{url_parameters}}" class="button field w-auto mr-1">Previous</a>
<a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto mr-1">{% trans 'Previous' %}</a>
{% else %}
<a class="button field w-auto mr-1" disabled>Previous</a>
<a class="button field !low bg-neutral-000 w-auto mr-1" disabled>{% trans 'Previous' %}</a>
{% endif %}
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}{{url_parameters}}" class="button field w-auto">Next</a>
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto">{% trans 'Next' %}</a>
{% else %}
<a class="button field w-auto" disabled>Next</a>
<a class="button field !low bg-neutral-000 w-auto" disabled>{% trans 'Next' %}</a>
{% endif %}
</div>
<ul class="pagination-list w-full md:w-auto mb-2 flex">
{% for pnum in begin %}
{% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li>
{% if page.number == pnum %}
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %}
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endif %}
{% endfor %}
{% if middle %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in middle %}
{% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li>
{% if page.number == pnum %}
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %}
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endif %}
{% endfor %}
{% endif %}
{% if end %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in end %}
{% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li>
{% if page.number == pnum %}
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %}
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endif %}
{% endfor %}
{% endif %}
</ul>

View File

@ -92,4 +92,6 @@ def is_file(field):
def add_class(field, css_class):
if len(field.errors) > 0:
css_class += " ~critical"
if field.field.widget.attrs.get("class") != None:
css_class += " " + field.field.widget.attrs["class"]
return field.as_widget(attrs={"class": field.css_classes(extra_classes=css_class)})

View File

@ -15,12 +15,8 @@ def pagination(
before_current_pages=4,
after_current_pages=4,
):
url_parameters = "".join(
[
f"&{urlencode(key)}={urlencode(value)}"
for key, value in request.GET.items()
if key != "page"
]
url_parameters = urlencode(
[(key, value) for key, value in request.GET.items() if key != "page"]
)
before = max(page.number - before_current_pages - 1, 0)

View File

@ -0,0 +1,118 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: analytics/models.py:18
msgid "Service"
msgstr "Dienst"
#: analytics/models.py:24
msgid "Identifier"
msgstr "Kennung"
#: analytics/models.py:29
msgid "Start time"
msgstr "Startzeit"
#: analytics/models.py:32
msgid "Last seen"
msgstr "Zuletzt gesehen"
#: analytics/models.py:36
msgid "User agent"
msgstr ""
#: analytics/models.py:37
msgid "Browser"
msgstr ""
#: analytics/models.py:38
msgid "Device"
msgstr "Gerät"
#: analytics/models.py:42
msgid "Phone"
msgstr ""
#: analytics/models.py:43
msgid "Tablet"
msgstr ""
#: analytics/models.py:44
msgid "Desktop"
msgstr ""
#: analytics/models.py:45
msgid "Robot"
msgstr ""
#: analytics/models.py:46
msgid "Other"
msgstr "Andere"
#: analytics/models.py:49
msgid "Device type"
msgstr "Gerätetyp"
#: analytics/models.py:51
msgid "OS"
msgstr "Betriessystem"
#: analytics/models.py:52
msgid "IP"
msgstr ""
#: analytics/models.py:55
msgid "Asn"
msgstr ""
#: analytics/models.py:56
msgid "Country"
msgstr "Land"
#: analytics/models.py:57
msgid "Longitude"
msgstr "Längengrad"
#: analytics/models.py:58
msgid "Latitude"
msgstr "Breitengrad"
#: analytics/models.py:59
msgid "Time zone"
msgstr "Zeitzone"
#: analytics/models.py:61
msgid "Is bounce"
msgstr "Absprung"
#: analytics/models.py:64 analytics/models.py:100
msgid "Session"
msgstr "Sitzung"
#: analytics/models.py:65
msgid "Sessions"
msgstr "Sitzungen"
#: analytics/models.py:122
msgid "Hit"
msgstr "Besuch"
#: analytics/models.py:123
msgid "Hits"
msgstr "Besuche"

View File

@ -0,0 +1,118 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: analytics/models.py:18
msgid "Service"
msgstr "服務"
#: analytics/models.py:24
msgid "Identifier"
msgstr "識別碼"
#: analytics/models.py:29
msgid "Start time"
msgstr "開始時間"
#: analytics/models.py:32
msgid "Last seen"
msgstr "最後瀏覽"
#: analytics/models.py:36
msgid "User agent"
msgstr "使用者代理程式"
#: analytics/models.py:37
msgid "Browser"
msgstr "瀏覽器"
#: analytics/models.py:38
msgid "Device"
msgstr "裝置"
#: analytics/models.py:42
msgid "Phone"
msgstr "手機"
#: analytics/models.py:43
msgid "Tablet"
msgstr "平板"
#: analytics/models.py:44
msgid "Desktop"
msgstr "桌上型電腦"
#: analytics/models.py:45
msgid "Robot"
msgstr "機器人"
#: analytics/models.py:46
msgid "Other"
msgstr "其他"
#: analytics/models.py:49
msgid "Device type"
msgstr "裝置類型"
#: analytics/models.py:51
msgid "OS"
msgstr "作業系統"
#: analytics/models.py:52
msgid "IP"
msgstr "IP"
#: analytics/models.py:55
msgid "Asn"
msgstr "ASN"
#: analytics/models.py:56
msgid "Country"
msgstr "國家"
#: analytics/models.py:57
msgid "Longitude"
msgstr "經度"
#: analytics/models.py:58
msgid "Latitude"
msgstr "緯度"
#: analytics/models.py:59
msgid "Time zone"
msgstr "時區"
#: analytics/models.py:61
msgid "Is bounce"
msgstr "是否為跳出"
#: analytics/models.py:64 analytics/models.py:100
msgid "Session"
msgstr "工作階段"
#: analytics/models.py:65
msgid "Sessions"
msgstr "工作階段次數"
#: analytics/models.py:122
msgid "Hit"
msgstr "點選"
#: analytics/models.py:123
msgid "Hits"
msgstr "點選次數"

View File

@ -60,7 +60,9 @@ class Migration(migrations.Migration):
),
),
],
options={"ordering": ["-start_time"],},
options={
"ordering": ["-start_time"],
},
),
migrations.CreateModel(
name="Hit",
@ -90,7 +92,9 @@ class Migration(migrations.Migration):
),
),
],
options={"ordering": ["-start_time"],},
options={
"ordering": ["-start_time"],
},
),
migrations.AddIndex(
model_name="session",

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-05-02 16:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("analytics", "0002_auto_20200415_1742"),
]
operations = [
migrations.AlterField(
model_name="session",
name="ip",
field=models.GenericIPAddressField(db_index=True, null=True),
),
]

View File

@ -0,0 +1,46 @@
# Generated by Django 3.1.7 on 2021-03-28 19:14
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("analytics", "0003_auto_20200502_1227"),
]
operations = [
migrations.AlterField(
model_name="hit",
name="last_seen",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AlterField(
model_name="hit",
name="start_time",
field=models.DateTimeField(
db_index=True, default=django.utils.timezone.now
),
),
migrations.AlterField(
model_name="session",
name="last_seen",
field=models.DateTimeField(
db_index=True, default=django.utils.timezone.now
),
),
migrations.AlterField(
model_name="session",
name="start_time",
field=models.DateTimeField(
db_index=True, default=django.utils.timezone.now
),
),
migrations.AddIndex(
model_name="session",
index=models.Index(
fields=["service", "-last_seen"], name="analytics_s_service_10bb96_idx"
),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.1.7 on 2021-03-28 19:18
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("analytics", "0004_auto_20210328_1514"),
]
operations = [
migrations.AlterField(
model_name="hit",
name="last_seen",
field=models.DateTimeField(
db_index=True, default=django.utils.timezone.now
),
),
migrations.AlterField(
model_name="hit",
name="load_time",
field=models.FloatField(db_index=True, null=True),
),
]

View File

@ -0,0 +1,40 @@
# Generated by Django 3.1.7 on 2021-03-28 19:36
from ..models import Hit, Session
from django.db import migrations, models
import django.db.models.deletion
from django.db.models import Subquery, OuterRef
def add_service_to_hits(_a, _b):
service = Session.objects.filter(pk=OuterRef("session")).values_list("service")[:1]
Hit.objects.update(service=Subquery(service))
class Migration(migrations.Migration):
dependencies = [
("core", "0008_auto_20200628_1403"),
("analytics", "0005_auto_20210328_1518"),
]
operations = [
migrations.AddField(
model_name="hit",
name="service",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="core.service",
),
),
migrations.RunPython(add_service_to_hits, lambda: ()),
migrations.AlterField(
model_name="hit",
name="service",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="core.service"
),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.1.7 on 2021-03-28 20:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("analytics", "0006_hit_service"),
]
operations = [
migrations.AddIndex(
model_name="hit",
index=models.Index(
fields=["service", "-start_time"], name="analytics_h_service_f4f41e_idx"
),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-28 21:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("analytics", "0007_auto_20210328_1634"),
]
operations = [
migrations.AddField(
model_name="session",
name="is_bounce",
field=models.BooleanField(db_index=True, default=True),
)
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.1.7 on 2021-03-29 15:00
from django.db import migrations, models
from ..models import Session
def update_bounce_stats(_a, _b):
Session.objects.all().annotate(hit_count=models.Count("hit")).filter(
hit_count__gt=1
).update(is_bounce=False)
class Migration(migrations.Migration):
dependencies = [
("analytics", "0008_session_is_bounce"),
]
operations = [
migrations.RunPython(update_bounce_stats, lambda: ()),
]

View File

@ -0,0 +1,114 @@
# Generated by Django 3.2.12 on 2022-06-24 11:44
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('core', '0009_auto_20211117_0217'),
('analytics', '0009_auto_20210329_1100'),
]
operations = [
migrations.AlterModelOptions(
name='hit',
options={'ordering': ['-start_time'], 'verbose_name': 'Hit', 'verbose_name_plural': 'Hits'},
),
migrations.AlterModelOptions(
name='session',
options={'ordering': ['-start_time'], 'verbose_name': 'Session', 'verbose_name_plural': 'Sessions'},
),
migrations.AlterField(
model_name='hit',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='hit',
name='session',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='analytics.session', verbose_name='Session'),
),
migrations.AlterField(
model_name='session',
name='asn',
field=models.TextField(blank=True, verbose_name='Asn'),
),
migrations.AlterField(
model_name='session',
name='browser',
field=models.TextField(verbose_name='Browser'),
),
migrations.AlterField(
model_name='session',
name='country',
field=models.TextField(blank=True, verbose_name='Country'),
),
migrations.AlterField(
model_name='session',
name='device',
field=models.TextField(verbose_name='Device'),
),
migrations.AlterField(
model_name='session',
name='device_type',
field=models.CharField(choices=[('PHONE', 'Phone'), ('TABLET', 'Tablet'), ('DESKTOP', 'Desktop'), ('ROBOT', 'Robot'), ('OTHER', 'Other')], default='OTHER', max_length=7, verbose_name='Device type'),
),
migrations.AlterField(
model_name='session',
name='identifier',
field=models.TextField(blank=True, db_index=True, verbose_name='Identifier'),
),
migrations.AlterField(
model_name='session',
name='ip',
field=models.GenericIPAddressField(db_index=True, null=True, verbose_name='IP'),
),
migrations.AlterField(
model_name='session',
name='is_bounce',
field=models.BooleanField(db_index=True, default=True, verbose_name='Is bounce'),
),
migrations.AlterField(
model_name='session',
name='last_seen',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Last seen'),
),
migrations.AlterField(
model_name='session',
name='latitude',
field=models.FloatField(null=True, verbose_name='Latitude'),
),
migrations.AlterField(
model_name='session',
name='longitude',
field=models.FloatField(null=True, verbose_name='Longitude'),
),
migrations.AlterField(
model_name='session',
name='os',
field=models.TextField(verbose_name='OS'),
),
migrations.AlterField(
model_name='session',
name='service',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.service', verbose_name='Service'),
),
migrations.AlterField(
model_name='session',
name='start_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Start time'),
),
migrations.AlterField(
model_name='session',
name='time_zone',
field=models.TextField(blank=True, verbose_name='Time zone'),
),
migrations.AlterField(
model_name='session',
name='user_agent',
field=models.TextField(verbose_name='User agent'),
),
]

View File

@ -1,11 +1,11 @@
import json
import uuid
from django.db import models
from django.shortcuts import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import Service
from core.models import Service, ACTIVE_USER_TIMEDELTA
def _default_uuid():
@ -14,50 +14,66 @@ def _default_uuid():
class Session(models.Model):
uuid = models.UUIDField(default=_default_uuid, primary_key=True)
service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True)
service = models.ForeignKey(
Service, verbose_name=_("Service"), on_delete=models.CASCADE, db_index=True
)
# Cross-session identification; optional, and provided by the service
identifier = models.TextField(blank=True, db_index=True)
identifier = models.TextField(
blank=True, db_index=True, verbose_name=_("Identifier")
)
# Time
start_time = models.DateTimeField(auto_now_add=True, db_index=True)
last_seen = models.DateTimeField(auto_now_add=True)
start_time = models.DateTimeField(
default=timezone.now, db_index=True, verbose_name=_("Start time")
)
last_seen = models.DateTimeField(
default=timezone.now, db_index=True, verbose_name=_("Last seen")
)
# Core request information
user_agent = models.TextField()
browser = models.TextField()
device = models.TextField()
user_agent = models.TextField(verbose_name=_("User agent"))
browser = models.TextField(verbose_name=_("Browser"))
device = models.TextField(verbose_name=_("Device"))
device_type = models.CharField(
max_length=7,
choices=[
("PHONE", "Phone"),
("TABLET", "Tablet"),
("DESKTOP", "Desktop"),
("ROBOT", "Robot"),
("OTHER", "Other"),
("PHONE", _("Phone")),
("TABLET", _("Tablet")),
("DESKTOP", _("Desktop")),
("ROBOT", _("Robot")),
("OTHER", _("Other")),
],
default="OTHER",
verbose_name=_("Device type"),
)
os = models.TextField()
ip = models.GenericIPAddressField(db_index=True)
os = models.TextField(verbose_name=_("OS"))
ip = models.GenericIPAddressField(db_index=True, null=True, verbose_name=_("IP"))
# GeoIP data
asn = models.TextField(blank=True)
country = models.TextField(blank=True)
longitude = models.FloatField(null=True)
latitude = models.FloatField(null=True)
time_zone = models.TextField(blank=True)
asn = models.TextField(blank=True, verbose_name=_("Asn"))
country = models.TextField(blank=True, verbose_name=_("Country"))
longitude = models.FloatField(null=True, verbose_name=_("Longitude"))
latitude = models.FloatField(null=True, verbose_name=_("Latitude"))
time_zone = models.TextField(blank=True, verbose_name=_("Time zone"))
is_bounce = models.BooleanField(
default=True, db_index=True, verbose_name=_("Is bounce")
)
class Meta:
verbose_name = _("Session")
verbose_name_plural = _("Sessions")
ordering = ["-start_time"]
indexes = [
models.Index(fields=["service", "-start_time"]),
models.Index(fields=["service", "-last_seen"]),
models.Index(fields=["service", "identifier"]),
]
@property
def is_currently_active(self):
return timezone.now() - self.last_seen < timezone.timedelta(seconds=10)
return timezone.now() - self.last_seen < ACTIVE_USER_TIMEDELTA
@property
def duration(self):
@ -72,14 +88,22 @@ class Session(models.Model):
kwargs={"pk": self.service.pk, "session_pk": self.uuid},
)
def recalculate_bounce(self):
bounce = self.hit_set.count() == 1
if bounce != self.is_bounce:
self.is_bounce = bounce
self.save()
class Hit(models.Model):
session = models.ForeignKey(Session, on_delete=models.CASCADE, db_index=True)
session = models.ForeignKey(
Session, on_delete=models.CASCADE, db_index=True, verbose_name=_("Session")
)
initial = models.BooleanField(default=True, db_index=True)
# Base request information
start_time = models.DateTimeField(auto_now_add=True, db_index=True)
last_seen = models.DateTimeField(auto_now_add=True)
start_time = models.DateTimeField(default=timezone.now, db_index=True)
last_seen = models.DateTimeField(default=timezone.now, db_index=True)
heartbeats = models.IntegerField(default=0)
tracker = models.TextField(
choices=[("JS", "JavaScript"), ("PIXEL", "Pixel (noscript)")]
@ -88,12 +112,19 @@ class Hit(models.Model):
# Advanced page information
location = models.TextField(blank=True, db_index=True)
referrer = models.TextField(blank=True, db_index=True)
load_time = models.FloatField(null=True)
load_time = models.FloatField(null=True, db_index=True)
# While not necessary, we store the root service directly for performance.
# It makes querying much easier; no need for inner joins.
service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True)
class Meta:
verbose_name = _("Hit")
verbose_name_plural = _("Hits")
ordering = ["-start_time"]
indexes = [
models.Index(fields=["session", "-start_time"]),
models.Index(fields=["service", "-start_time"]),
models.Index(fields=["session", "location"]),
models.Index(fields=["session", "referrer"]),
]
@ -105,5 +136,5 @@ class Hit(models.Model):
def get_absolute_url(self):
return reverse(
"dashboard:service_session",
kwargs={"pk": self.session.service.pk, "session_pk": self.session.pk},
kwargs={"pk": self.service.pk, "session_pk": self.session.pk},
)

View File

@ -1,5 +1,6 @@
import json
import ipaddress
import logging
from hashlib import sha256
import geoip2.database
import user_agents
@ -38,47 +39,81 @@ def _geoip2_lookup(ip):
}
except geoip2.errors.AddressNotFoundError:
return {}
except FileNotFoundError as e:
log.exception("Unable to perform GeoIP lookup: %s", e)
return {}
@shared_task
def ingress_request(
service_uuid, tracker, time, payload, ip, location, user_agent, dnt=False, identifier=""
service_uuid,
tracker,
time,
payload,
ip,
location,
user_agent,
dnt=False,
identifier="",
):
try:
service = Service.objects.get(pk=service_uuid, status=Service.ACTIVE)
log.debug(f"Linked to service {service}")
if dnt and service.respect_dnt:
log.debug("Ignoring because of DNT or GPC")
return
ip_data = _geoip2_lookup(ip)
log.debug(f"Found geoip2 data")
try:
remote_ip = ipaddress.ip_network(ip)
for ignored_network in service.get_ignored_networks():
if (
ignored_network.version == remote_ip.version
and ignored_network.supernet_of(remote_ip)
):
log.debug("Ignoring because of ignored IP")
return
except ValueError as e:
log.exception(e)
# Validate payload
if payload.get("loadTime", 1) <= 0:
payload["loadTime"] = None
# Create or update session
session = (
Session.objects.filter(
service=service,
last_seen__gt=timezone.now() - timezone.timedelta(minutes=10),
ip=ip,
user_agent=user_agent,
).first()
# We used to check for identifiers, but that can cause issues when people
# re-open the page in a new tab, for example. It's better to match sessions
# solely based on IP and user agent.
association_id_hash = sha256()
association_id_hash.update(str(ip).encode("utf-8"))
association_id_hash.update(str(user_agent).encode("utf-8"))
if settings.AGGRESSIVE_HASH_SALTING:
association_id_hash.update(str(service.pk).encode("utf-8"))
association_id_hash.update(
str(timezone.now().date().isoformat()).encode("utf-8")
)
session_cache_path = (
f"session_association_{service.pk}_{association_id_hash.hexdigest()}"
)
# Create or update session
session = None
if cache.get(session_cache_path) is not None:
cache.touch(session_cache_path, settings.SESSION_MEMORY_TIMEOUT)
session = Session.objects.filter(
pk=cache.get(session_cache_path), service=service
).first()
if session is None:
log.debug("Cannot link to existing session; creating a new one...")
ua = user_agents.parse(user_agent)
initial = True
log.debug("Cannot link to existing session; creating a new one...")
ip_data = _geoip2_lookup(ip)
log.debug(f"Found geoip2 data...")
ua = user_agents.parse(user_agent)
device_type = "OTHER"
if (
ua.is_bot
or (ua.browser.family or "").strip().lower() == "googlebot"
or (ua.device.family or ua.device.model or "").strip().lower() == "spider"
or (ua.device.family or ua.device.model or "").strip().lower()
== "spider"
):
device_type = "ROBOT"
elif ua.is_mobile:
@ -87,26 +122,35 @@ def ingress_request(
device_type = "TABLET"
elif ua.is_pc:
device_type = "DESKTOP"
if device_type == "ROBOT" and service.ignore_robots:
return
session = Session.objects.create(
service=service,
ip=ip,
ip=ip if service.collect_ips and not settings.BLOCK_ALL_IPS else None,
user_agent=user_agent,
identifier=identifier.strip(),
browser=ua.browser.family or "",
device=ua.device.family or ua.device.model or "",
device_type=device_type,
start_time=time,
last_seen=time,
os=ua.os.family or "",
asn=ip_data.get("asn", ""),
country=ip_data.get("country", ""),
asn=ip_data.get("asn") or "",
country=ip_data.get("country") or "",
longitude=ip_data.get("longitude"),
latitude=ip_data.get("latitude"),
time_zone=ip_data.get("time_zone", ""),
time_zone=ip_data.get("time_zone") or "",
)
cache.set(
session_cache_path, session.pk, timeout=settings.SESSION_MEMORY_TIMEOUT
)
else:
log.debug("Updating old session with new data...")
initial = False
log.debug("Updating old session with new data...")
# Update last seen time
session.last_seen = timezone.now()
session.last_seen = time
if session.identifier == "" and identifier.strip() != "":
session.identifier = identifier.strip()
session.save()
@ -115,9 +159,10 @@ def ingress_request(
idempotency = payload.get("idempotency")
idempotency_path = f"hit_idempotency_{idempotency}"
hit = None
if idempotency is not None:
if cache.get(idempotency_path) is not None:
cache.touch(idempotency_path, 10 * 60)
cache.touch(idempotency_path, settings.SESSION_MEMORY_TIMEOUT)
hit = Hit.objects.filter(
pk=cache.get(idempotency_path), session=session
).first()
@ -126,8 +171,9 @@ def ingress_request(
# this is a heartbeat.
log.debug("Hit is a heartbeat; updating old hit with new data...")
hit.heartbeats += 1
hit.last_seen = timezone.now()
hit.last_seen = time
hit.save()
if hit is None:
log.debug("Hit is a page load; creating new hit...")
# There is no existing hit; create a new one
@ -141,10 +187,20 @@ def ingress_request(
location=payload.get("location", location),
referrer=payload.get("referrer", ""),
load_time=payload.get("loadTime"),
start_time=time,
last_seen=time,
service=service,
)
# Recalculate whether the session is a bounce
session.recalculate_bounce()
# Set idempotency (if applicable)
if idempotency is not None:
cache.set(idempotency_path, hit.pk, timeout=10 * 60)
cache.set(
idempotency_path, hit.pk, timeout=settings.SESSION_MEMORY_TIMEOUT
)
except Exception as e:
log.exception(e)
print(e)
raise e

View File

@ -1,19 +1,43 @@
window.onload = function () {
var idempotency =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
function sendUpdate() {
// This is a lightweight and privacy-friendly analytics script from Shynet, a self-hosted
// analytics tool. To give you full visibility into how your data is being monitored, this
// file is intentionally not minified or obfuscated. To learn more about Shynet (and to view
// its source code), visit <https://github.com/milesmcc/shynet>.
//
// This script only sends the current URL, the referrer URL, and the page load time. That's it!
{% if dnt %}
var Shynet = {
dnt: true
};
{% else %}
var Shynet = {
dnt: false,
idempotency: null,
heartbeatTaskId: null,
skipHeartbeat: false,
sendHeartbeat: function () {
try {
if (document.hidden || Shynet.skipHeartbeat) {
return;
}
Shynet.skipHeartbeat = true;
var xhr = new XMLHttpRequest();
xhr.open(
"POST",
"{{protocol}}://{{request.site.domain|default:request.META.HTTP_HOST}}{{endpoint}}",
"{{protocol}}://{{request.get_host}}{{endpoint}}",
true
);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = function () {
Shynet.skipHeartbeat = false;
};
xhr.onerror = function () {
Shynet.skipHeartbeat = false;
};
xhr.send(
JSON.stringify({
idempotency: idempotency,
idempotency: Shynet.idempotency,
referrer: document.referrer,
location: window.location.href,
loadTime:
@ -21,8 +45,28 @@ window.onload = function () {
window.performance.timing.navigationStart,
})
);
} catch {}
} catch (e) {}
},
newPageLoad: function () {
if (Shynet.heartbeatTaskId != null) {
clearInterval(Shynet.heartbeatTaskId);
}
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
Shynet.skipHeartbeat = false;
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
Shynet.sendHeartbeat();
}
setInterval(sendUpdate, 5000);
sendUpdate();
};
window.addEventListener("load", Shynet.newPageLoad);
{% endif %}
{% if script_inject %}
// The following is script is not part of Shynet, and was instead
// provided by this site's administrator.
//
// -- START --
{{script_inject|safe}}
// -- END --
{% endif %}

View File

@ -1,15 +1,25 @@
import base64
import json
from urllib.parse import urlparse
from django.conf import settings
from django.http import HttpResponse
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
)
from django.shortcuts import render, reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView, View
from django.views.generic import View
from ipware import get_client_ip
from core.models import Service
from ..tasks import ingress_request
@ -19,6 +29,9 @@ def ingress(request, service_uuid, identifier, tracker, payload):
location = request.META.get("HTTP_REFERER", "").strip()
user_agent = request.META.get("HTTP_USER_AGENT", "").strip()
dnt = request.META.get("HTTP_DNT", "0").strip() == "1"
gpc = request.META.get("HTTP_SEC_GPC", "0").strip() == "1"
if gpc or dnt:
dnt = True
ingress_request.delay(
service_uuid,
@ -33,13 +46,53 @@ def ingress(request, service_uuid, identifier, tracker, payload):
)
class PixelView(View):
class ValidateServiceOriginsMixin:
def dispatch(self, request, *args, **kwargs):
try:
service_uuid = self.kwargs.get("service_uuid")
origins = cache.get(f"service_origins_{service_uuid}")
if origins is None:
service = Service.objects.get(uuid=service_uuid)
origins = service.origins
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
allow_origin = "*"
if origins != "*":
remote_origin = request.META.get("HTTP_ORIGIN")
if (
remote_origin is None
and request.META.get("HTTP_REFERER") is not None
):
parsed = urlparse(request.META.get("HTTP_REFERER"))
remote_origin = f"{parsed.scheme}://{parsed.netloc}".lower()
origins = [origin.strip().lower() for origin in origins.split(",")]
if remote_origin in origins:
allow_origin = remote_origin
else:
return HttpResponseForbidden()
resp = super().dispatch(request, *args, **kwargs)
resp["Access-Control-Allow-Origin"] = allow_origin
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
resp[
"Access-Control-Allow-Headers"
] = "Origin, X-Requested-With, Content-Type, Accept, Authorization, Referer"
return resp
except Service.DoesNotExist:
raise Http404()
except ValidationError:
return HttpResponseBadRequest()
class PixelView(ValidateServiceOriginsMixin, View):
# Fallback view to serve an unobtrusive 1x1 transparent tracking pixel for browsers with
# JavaScript disabled.
def dispatch(self, request, *args, **kwargs):
def get(self, *args, **kwargs):
# Extract primary data
ingress(
request,
self.request,
self.kwargs.get("service_uuid"),
self.kwargs.get("identifier", ""),
"PIXEL",
@ -50,30 +103,23 @@ class PixelView(View):
"R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
)
resp = HttpResponse(data, content_type="image/gif")
resp["Cache-Control"] = "no-cache"
resp["Cache-Control"] = "no-cache, no-store, must-revalidate"
resp["Access-Control-Allow-Origin"] = "*"
return resp
@method_decorator(csrf_exempt, name="dispatch")
class ScriptView(View):
def dispatch(self, request, *args, **kwargs):
resp = super().dispatch(request, *args, **kwargs)
resp["Access-Control-Allow-Origin"] = "*"
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
resp[
"Access-Control-Allow-Headers"
] = "Origin, X-Requested-With, Content-Type, Accept, Authorization, Referer"
return resp
class ScriptView(ValidateServiceOriginsMixin, View):
def get(self, *args, **kwargs):
protocol = "https" if settings.SCRIPT_USE_HTTPS else "http"
endpoint = (
reverse(
"ingress:endpoint_script",
kwargs={"service_uuid": self.kwargs.get("service_uuid"),},
kwargs={
"service_uuid": self.kwargs.get("service_uuid"),
},
)
if self.kwargs.get("identifier") == None
if self.kwargs.get("identifier") is None
else reverse(
"ingress:endpoint_script_id",
kwargs={
@ -82,10 +128,22 @@ class ScriptView(View):
},
)
)
heartbeat_frequency = settings.SCRIPT_HEARTBEAT_FREQUENCY
dnt = self.request.META.get("HTTP_DNT", "0").strip() == "1"
service_uuid = self.kwargs.get("service_uuid")
service = Service.objects.get(pk=service_uuid, status=Service.ACTIVE)
return render(
self.request,
"analytics/scripts/page.js",
context={"endpoint": endpoint, "protocol": protocol},
context=dict(
{
"endpoint": endpoint,
"protocol": protocol,
"heartbeat_frequency": heartbeat_frequency,
"script_inject": self.get_script_inject(),
"dnt": dnt and service.respect_dnt,
}
),
content_type="application/javascript",
)
@ -101,3 +159,12 @@ class ScriptView(View):
return HttpResponse(
json.dumps({"status": "OK"}), content_type="application/json"
)
def get_script_inject(self):
service_uuid = self.kwargs.get("service_uuid")
script_inject = cache.get(f"script_inject_{service_uuid}")
if script_inject is None:
service = Service.objects.get(uuid=service_uuid)
script_inject = service.script_inject
cache.set(f"script_inject_{service_uuid}", script_inject, timeout=3600)
return script_inject

0
shynet/api/__init__.py Normal file
View File

1
shynet/api/admin.py Normal file
View File

@ -0,0 +1 @@
# from django.contrib import admin

6
shynet/api/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api"

View File

26
shynet/api/mixins.py Normal file
View File

@ -0,0 +1,26 @@
from http import HTTPStatus
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.http import JsonResponse
User = get_user_model()
class ApiTokenRequiredMixin:
def _get_user_by_token(self, request):
token = request.headers.get("Authorization")
if not token or not token.startswith("Token "):
return AnonymousUser()
token = token.split(" ")[1]
user: User = User.objects.filter(api_token=token).first()
return user or AnonymousUser()
def dispatch(self, request, *args, **kwargs):
request.user = self._get_user_by_token(request)
return (
super().dispatch(request, *args, **kwargs)
if request.user.is_authenticated
else JsonResponse(data={}, status=HTTPStatus.FORBIDDEN)
)

1
shynet/api/models.py Normal file
View File

@ -0,0 +1 @@
# from django.db import models

View File

View File

@ -0,0 +1,77 @@
from http import HTTPStatus
from django.test import TestCase, RequestFactory
from django.views import View
from api.mixins import ApiTokenRequiredMixin
from core.factories import UserFactory
from core.models import _default_api_token, Service
class TestApiTokenRequiredMixin(TestCase):
class DummyView(ApiTokenRequiredMixin, View):
model = Service
template_name = "dashboard/pages/service.html"
def setUp(self):
super().setUp()
self.user = UserFactory()
self.request = RequestFactory().get("/fake-path")
# Setup request and view.
self.factory = RequestFactory()
self.view = self.DummyView()
def test_get_user_by_token_without_authorization_token(self):
"""
GIVEN: A request without Authorization header
WHEN: get_user_by_token is called
THEN: It should return AnonymousUser
"""
user = self.view._get_user_by_token(self.request)
self.assertEqual(user.is_anonymous, True)
def test_get_user_by_token_with_invalid_authorization_token(self):
"""
GIVEN: A request with invalid Authorization header
WHEN: get_user_by_token is called
THEN: It should return AnonymousUser
"""
self.request.META["HTTP_AUTHORIZATION"] = "Bearer invalid-token"
user = self.view._get_user_by_token(self.request)
self.assertEqual(user.is_anonymous, True)
def test_get_user_by_token_with_invalid_token(self):
"""
GIVEN: A request with invalid token
WHEN: get_user_by_token is called
THEN: It should return AnonymousUser
"""
self.request.META["HTTP_AUTHORIZATION"] = f"Token {_default_api_token()}"
user = self.view._get_user_by_token(self.request)
self.assertEqual(user.is_anonymous, True)
def test_get_user_by_token_with_valid_token(self):
"""
GIVEN: A request with valid token
WHEN: get_user_by_token is called
THEN: It should return the user
"""
self.request.META["HTTP_AUTHORIZATION"] = f"Token {self.user.api_token}"
user = self.view._get_user_by_token(self.request)
self.assertEqual(user, self.user)
def test_dispatch_with_unauthenticated_user(self):
"""
GIVEN: A request with unauthenticated user
WHEN: dispatch is called
THEN: It should return 403
"""
self.request.META["HTTP_AUTHORIZATION"] = f"Token {_default_api_token()}"
response = self.view.dispatch(self.request)
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)

View File

@ -0,0 +1,79 @@
import json
from http import HTTPStatus
from django.contrib.auth import get_user_model
from django.test import TestCase, RequestFactory
from django.urls import reverse
from api.views import DashboardApiView
from core.factories import UserFactory, ServiceFactory
from core.models import Service
User = get_user_model()
class TestDashboardApiView(TestCase):
def setUp(self) -> None:
super().setUp()
self.user: User = UserFactory()
self.service_1: Service = ServiceFactory(owner=self.user)
self.service_2: Service = ServiceFactory(owner=self.user)
self.url = reverse("api:services")
self.factory = RequestFactory()
def test_get_with_unauthenticated_user(self):
"""
GIVEN: An unauthenticated user
WHEN: The user makes a GET request to the dashboard API view
THEN: It should return 403
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
def test_get_returns_400(self):
"""
GIVEN: An authenticated user
WHEN: The user makes a GET request to the dashboard API view with an invalid date format
THEN: It should return 400
"""
request = self.factory.get(self.url, {"startDate": "01/01/2000"})
request.META["HTTP_AUTHORIZATION"] = f"Token {self.user.api_token}"
response = DashboardApiView.as_view()(request)
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
data = json.loads(response.content)
self.assertEqual(data["error"], "Invalid date format. Use YYYY-MM-DD.")
def test_get_with_authenticated_user(self):
"""
GIVEN: An authenticated user
WHEN: The user makes a GET request to the dashboard API view
THEN: It should return 200
"""
request = self.factory.get(self.url)
request.META["HTTP_AUTHORIZATION"] = f"Token {self.user.api_token}"
response = DashboardApiView.as_view()(request)
self.assertEqual(response.status_code, HTTPStatus.OK)
data = json.loads(response.content)
self.assertEqual(len(data["services"]), 2)
def test_get_with_service_uuid(self):
"""
GIVEN: An authenticated user
WHEN: The user makes a GET request to the dashboard API view with a service UUID
THEN: It should return 200 and a single service
"""
request = self.factory.get(self.url, {"uuid": str(self.service_1.uuid)})
request.META["HTTP_AUTHORIZATION"] = f"Token {self.user.api_token}"
response = DashboardApiView.as_view()(request)
self.assertEqual(response.status_code, HTTPStatus.OK)
data = json.loads(response.content)
self.assertEqual(len(data["services"]), 1)
self.assertEqual(data["services"][0]["uuid"], str(self.service_1.uuid))
self.assertEqual(data["services"][0]["name"], str(self.service_1.name))

7
shynet/api/urls.py Normal file
View File

@ -0,0 +1,7 @@
from django.urls import path
from . import views
urlpatterns = [
path("dashboard/", views.DashboardApiView.as_view(), name="services"),
]

52
shynet/api/views.py Normal file
View File

@ -0,0 +1,52 @@
from http import HTTPStatus
from django.db.models import Q
from django.db.models.query import QuerySet
from django.http import JsonResponse
from django.views.generic import View
from core.models import Service
from core.utils import is_valid_uuid
from dashboard.mixins import DateRangeMixin
from .mixins import ApiTokenRequiredMixin
class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
def get(self, request, *args, **kwargs):
services = Service.objects.filter(Q(owner=request.user) | Q(collaborators__in=[request.user])).distinct()
uuid_ = request.GET.get("uuid")
if uuid_ and is_valid_uuid(uuid_):
services = services.filter(uuid=uuid_)
try:
start = self.get_start_date()
end = self.get_end_date()
except ValueError:
return JsonResponse(status=HTTPStatus.BAD_REQUEST, data={"error": "Invalid date format. Use YYYY-MM-DD."})
service: Service
services_data = [
{
"name": service.name,
"uuid": service.uuid,
"link": service.link,
"stats": service.get_core_stats(start, end),
}
for service in services
]
services_data = self._convert_querysets_to_lists(services_data)
return JsonResponse(data={"services": services_data})
def _convert_querysets_to_lists(self, services_data: list[dict]) -> list[dict]:
for service_data in services_data:
for key, value in service_data["stats"].items():
if isinstance(value, QuerySet):
service_data["stats"][key] = list(value)
for key, value in service_data["stats"]["compare"].items():
if isinstance(value, QuerySet):
service_data["stats"]["compare"][key] = list(value)
return services_data

View File

@ -1,8 +1,15 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import Service, User
class UserAdmin(BaseUserAdmin):
fieldsets = BaseUserAdmin.fieldsets + (
("Extra fields", {"fields": ("api_token",)}),
)
admin.site.register(User, UserAdmin)

39
shynet/core/factories.py Normal file
View File

@ -0,0 +1,39 @@
import factory
from django.contrib.auth import get_user_model
from factory import post_generation
from factory.django import DjangoModelFactory
from .models import Service
class UserFactory(DjangoModelFactory):
username = factory.Faker("user_name")
email = factory.Faker("email")
first_name = factory.Faker("name")
@post_generation
def password(self, create, extracted, **kwargs):
password = (
extracted
or factory.Faker(
"password",
length=42,
special_chars=True,
digits=True,
upper_case=True,
lower_case=True,
).evaluate(None, None, extra={"locale": None})
)
self.set_password(password)
class Meta:
model = get_user_model()
django_get_or_create = ["username"]
class ServiceFactory(DjangoModelFactory):
class Meta:
model = Service
name = factory.Faker("company")

View File

@ -0,0 +1,87 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: core/models.py:58
msgid "Active"
msgstr "Aktiv"
#: core/models.py:58
msgid "Archived"
msgstr "Archiviert"
#: core/models.py:61
msgid "Name"
msgstr "Name"
#: core/models.py:63
msgid "Owner"
msgstr "Eigentümer"
#: core/models.py:67
msgid "Collaborators"
msgstr "Mitarbeiter"
#: core/models.py:70
msgid "created"
msgstr "Erstellt"
#: core/models.py:71
msgid "link"
msgstr "Verweis"
#: core/models.py:72
msgid "origins"
msgstr ""
#: core/models.py:75
msgid "status"
msgstr "Status"
#: core/models.py:77
msgid "Respect dnt"
msgstr "DNT beachten"
#: core/models.py:78
msgid "Ignore robots"
msgstr "Robots ignorieren"
#: core/models.py:79
msgid "Collect ips"
msgstr "IPs erfassen"
#: core/models.py:82
msgid "Igored ips"
msgstr "IPs ignorieren"
#: core/models.py:86
msgid "Hide referrer regex"
msgstr "Referrer Regex ausblenden"
#: core/models.py:88
msgid "Script inject"
msgstr ""
#: core/models.py:91
msgid "Service"
msgstr "Dienst"
#: core/models.py:92
msgid "Services"
msgstr "Dienste"

View File

@ -1,247 +0,0 @@
#: account/adapter.py:51
msgid "A user is already registered with this e-mail address."
msgstr "A user is already registered with this email address."
#: account/adapter.py:294
#, python-brace-format
msgid "Password must be a minimum of {0} characters."
msgstr ""
#: account/forms.py:92
msgid "Remember Me"
msgstr "Remember me"
#: account/forms.py:101
msgid "The e-mail address and/or password you specified are not correct."
msgstr "The email address and/or password are not correct."
#: account/forms.py:113 account/forms.py:268 account/forms.py:426
#: account/forms.py:495
msgid "E-mail address"
msgstr "Email address"
#: account/forms.py:115 account/forms.py:301 account/forms.py:421
#: account/forms.py:490
msgid "E-mail"
msgstr "Email"
#: account/forms.py:130
msgid "Username or e-mail"
msgstr "Username or email"
#: account/forms.py:292
msgid "E-mail (again)"
msgstr "Email (again)"
#: account/forms.py:296
msgid "E-mail address confirmation"
msgstr "Email address confirmation"
#: account/forms.py:304
msgid "E-mail (optional)"
msgstr "Email (optional)"
#: account/forms.py:432
msgid "This e-mail address is already associated with this account."
msgstr "This email address is already associated with this account."
#: account/forms.py:434
msgid "This e-mail address is already associated with another account."
msgstr "This email address is already associated with another account."
#: account/forms.py:504
msgid "The e-mail address is not assigned to any user account"
msgstr "The email address is not assigned to any user account."
#: account/models.py:25 account/models.py:78
msgid "e-mail address"
msgstr "email address"
#: socialaccount/adapter.py:26
#, python-format
msgid ""
"An account already exists with this e-mail address. Please sign in to that "
"account first, then connect your %s account."
msgstr ""
"An account already exists with this email address. Please sign in to that "
"account first, then connect your %s account."
#: socialaccount/adapter.py:138
msgid "Your account has no verified e-mail address."
msgstr "Your account has no verified email address."
#: templates/account/email.html:8
msgid "E-mail Addresses"
msgstr "Email Addresses"
#: templates/account/email.html:10
msgid "The following e-mail addresses are associated with your account:"
msgstr "The following email addresses are associated with your account:"
#: templates/account/email.html:43
msgid ""
"You currently do not have any e-mail address set up. You should really add "
"an e-mail address so you can receive notifications, reset your password, etc."
msgstr ""
"You currently do not have any email address set up. You should really add "
"an email address so you can receive notifications, reset your password, etc."
#: templates/account/email.html:48
msgid "Add E-mail Address"
msgstr "Add Email Address"
#: templates/account/email.html:53
msgid "Add E-mail"
msgstr "Add Email"
#: templates/account/email.html:62
msgid "Do you really want to remove the selected e-mail address?"
msgstr "Do you really want to remove the selected email address?"
#: templates/account/email/email_confirmation_message.txt:1
#, python-format
msgid ""
"Hello from %(site_name)s!\n"
"\n"
"You're receiving this e-mail because user %(user_display)s has given yours "
"as an e-mail address to connect their account.\n"
"\n"
"To confirm this is correct, go to %(activate_url)s\n"
msgstr ""
"Hello from %(site_name)s!\n"
"\n"
"You're receiving this email because user %(user_display)s has given yours "
"as an email address to connect their account.\n"
"\n"
"To confirm this is correct, go to %(activate_url)s\n"
#: templates/account/email/email_confirmation_message.txt:7
#, python-format
msgid ""
"Thank you from %(site_name)s!\n"
"%(site_domain)s"
msgstr ""
#: templates/account/email/email_confirmation_subject.txt:3
msgid "Please Confirm Your E-mail Address"
msgstr "Please Confirm Your Email Address"
#: templates/account/email/password_reset_key_message.txt:1
#, python-format
msgid ""
"Hello from %(site_name)s!\n"
"\n"
"You're receiving this e-mail because you or someone else has requested a "
"password for your user account.\n"
"It can be safely ignored if you did not request a password reset. Click the "
"link below to reset your password."
msgstr ""
"Hello from %(site_name)s!\n"
"\n"
"You're receiving this email because you or someone else has requested a "
"password for your user account.\n"
"It can be safely ignored if you did not request a password reset. Click the "
"link below to reset your password."
#: templates/account/email/password_reset_key_subject.txt:3
msgid "Password Reset E-mail"
msgstr "Password Reset Email"
#: templates/account/email_confirm.html:6
#: templates/account/email_confirm.html:10
msgid "Confirm E-mail Address"
msgstr "Confirm Email Address"
#: templates/account/email_confirm.html:16
#, python-format
msgid ""
"Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an e-mail "
"address for user %(user_display)s."
msgstr ""
"Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an email "
"address for user %(user_display)s."
#: templates/account/email_confirm.html:27
#, python-format
msgid ""
"This e-mail confirmation link expired or is invalid. Please <a href="
"\"%(email_url)s\">issue a new e-mail confirmation request</a>."
msgstr ""
"This email confirmation link expired or is invalid. Please <a href="
"\"%(email_url)s\">issue a new email confirmation request</a>."
#: templates/account/messages/cannot_delete_primary_email.txt:2
#, python-format
msgid "You cannot remove your primary e-mail address (%(email)s)."
msgstr "You cannot remove your primary email address (%(email)s)."
#: templates/account/messages/email_confirmation_sent.txt:2
#, python-format
msgid "Confirmation e-mail sent to %(email)s."
msgstr "Confirmation email sent to %(email)s."
#: templates/account/messages/email_deleted.txt:2
#, python-format
msgid "Removed e-mail address %(email)s."
msgstr "Removed email address %(email)s."
#: templates/account/messages/primary_email_set.txt:2
msgid "Primary e-mail address set."
msgstr "Primary email address set."
#: templates/account/messages/unverified_primary_email.txt:2
msgid "Your primary e-mail address must be verified."
msgstr "Your primary email address must be verified."
#: templates/account/password_reset.html:15
msgid ""
"Forgotten your password? Enter your e-mail address below, and we'll send you "
"an e-mail allowing you to reset it."
msgstr ""
"Forgotten your password? Enter your email address below, and we'll send you "
"an email allowing you to reset it."
#: templates/account/password_reset_done.html:15
msgid ""
"We have sent you an e-mail. Please contact us if you do not receive it "
"within a few minutes."
msgstr ""
"We have sent you an email. Please contact us if you do not receive it "
"within a few minutes."
#: templates/account/verification_sent.html:5
#: templates/account/verification_sent.html:8
#: templates/account/verified_email_required.html:5
#: templates/account/verified_email_required.html:8
msgid "Verify Your E-mail Address"
msgstr "Verify Your Email Address"
#: templates/account/verification_sent.html:10
msgid ""
"We have sent an e-mail to you for verification. Follow the link provided to "
"finalize the signup process. Please contact us if you do not receive it "
"within a few minutes."
msgstr ""
"We have sent an email to you for verification. Follow the link provided to "
"finalize the signup process. Please contact us if you do not receive it "
"within a few minutes."
#: templates/account/verified_email_required.html:12
msgid ""
"This part of the site requires us to verify that\n"
"you are who you claim to be. For this purpose, we require that you\n"
"verify ownership of your e-mail address. "
msgstr ""
"This part of the site requires us to verify that\n"
"you are who you claim to be. For this purpose, we require that you\n"
"verify ownership of your email address. "
#: templates/account/verified_email_required.html:16
msgid ""
"We have sent an e-mail to you for\n"
"verification. Please click on the link inside this e-mail. Please\n"
"contact us if you do not receive it within a few minutes."
msgstr ""
"We have sent an email to you for\n"
"verification. Please click on the link inside this email. Please\n"
"contact us if you do not receive it within a few minutes."

View File

@ -0,0 +1,87 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: core/models.py:58
msgid "Active"
msgstr "啟用"
#: core/models.py:58
msgid "Archived"
msgstr "已封存"
#: core/models.py:61
msgid "Name"
msgstr "名稱"
#: core/models.py:63
msgid "Owner"
msgstr "擁有者"
#: core/models.py:67
msgid "Collaborators"
msgstr "協作者"
#: core/models.py:70
msgid "created"
msgstr "已建立"
#: core/models.py:71
msgid "link"
msgstr "連結"
#: core/models.py:72
msgid "origins"
msgstr "來源"
#: core/models.py:75
msgid "status"
msgstr "狀態"
#: core/models.py:77
msgid "Respect dnt"
msgstr "尊重停止追蹤 (Do Not Track) 設定"
#: core/models.py:78
msgid "Ignore robots"
msgstr "忽略機器人"
#: core/models.py:79
msgid "Collect ips"
msgstr "收集 IP"
#: core/models.py:82
msgid "Igored ips"
msgstr "忽略的 IP"
#: core/models.py:86
msgid "Hide referrer regex"
msgstr "隱藏來源參照正規表達式"
#: core/models.py:88
msgid "Script inject"
msgstr "插入指令碼"
#: core/models.py:91
msgid "Service"
msgstr "服務"
#: core/models.py:92
msgid "Services"
msgstr "服務"

View File

@ -0,0 +1,117 @@
import traceback
from django.utils.timezone import now
from django.utils.timezone import timedelta
import random
import uuid
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand, CommandError
from django.utils.crypto import get_random_string
import user_agents
from logging import info
from core.models import User, Service
from analytics.models import Session, Hit
from analytics.tasks import ingress_request
LOCATIONS = [
"/",
"/post/{rand}",
"/login",
"/me",
]
REFERRERS = [
"https://news.ycombinator.com/item?id=11116274",
"https://news.ycombinator.com/item?id=24872911",
"https://reddit.com",
"https://facebook.com",
"https://twitter.com/milesmccain",
"https://twitter.com",
"https://stanford.edu/~mccain/",
"https://tiktok.com",
"https://io.stanford.edu",
"https://en.wikipedia.org",
"https://stackoverflow.com",
"",
"",
"",
"",
]
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/43.4",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko)",
"Version/10.0 Mobile/14E304 Safari/602.1",
]
class Command(BaseCommand):
help = "Configures a Shynet demo service"
def add_arguments(self, parser):
parser.add_argument(
"name",
type=str,
)
parser.add_argument("owner_email", type=str)
parser.add_argument(
"avg",
type=int,
)
parser.add_argument("deviation", type=float, default=0.4)
parser.add_argument(
"days",
type=int,
)
parser.add_argument("load_time", type=float, default=1000)
def handle(self, *args, **options):
owner = User.objects.get(email=options.get("owner_email"))
service = Service.objects.create(name=options.get("name"), owner=owner)
print(
f"Created demo service `{service.name}` (uuid: `{service.uuid}`, owner: {owner})"
)
# Go through each day requested, creating sessions and hits
for days in range(options.get("days")):
day = (now() - timedelta(days=days)).replace(hour=0, minute=0, second=0)
print(f"Populating info for {day}...")
avg = options.get("avg")
deviation = options.get("deviation")
ips = [
".".join(map(str, (random.randint(0, 255) for _ in range(4))))
for _ in range(avg)
]
n = avg + random.randrange(-1 * deviation * avg, deviation * avg)
for _ in range(n):
time = day + timedelta(
hours=random.randrange(0, 23),
minutes=random.randrange(0, 59),
seconds=random.randrange(0, 59),
)
ip = random.choice(ips)
load_time = random.normalvariate(options.get("load_time"), 500)
referrer = random.choice(REFERRERS)
location = "https://example.com" + random.choice(LOCATIONS).replace(
"{rand}", str(random.randint(0, n))
)
user_agent = random.choice(USER_AGENTS)
ingress_request(
service.uuid,
"JS",
time,
{"loadTime": load_time, "referrer": referrer},
ip,
location,
user_agent,
)
print(f"Created {n} demo hits on {day}!")
self.stdout.write(self.style.SUCCESS(f"Successfully created demo data!"))

View File

@ -1,27 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from django.contrib.sites.models import Site
from core.models import User
from django.utils.crypto import get_random_string
import uuid
import traceback
class Command(BaseCommand):
help = "Configures the Shynet hostname"
def add_arguments(self, parser):
parser.add_argument(
"hostname",
type=str,
)
def handle(self, *args, **options):
site = Site.objects.get(pk=settings.SITE_ID)
site.domain = options.get("hostname")
site.save()
self.stdout.write(
self.style.SUCCESS(
f"Successfully set the hostname to '{options.get('hostname')}'"
)
)

View File

@ -1,10 +1,12 @@
from django.core.management.base import BaseCommand, CommandError
import traceback
import uuid
from django.conf import settings
from django.contrib.sites.models import Site
from core.models import User
from django.core.management.base import BaseCommand, CommandError
from django.utils.crypto import get_random_string
import uuid
import traceback
from core.models import User
class Command(BaseCommand):
@ -18,12 +20,8 @@ class Command(BaseCommand):
def handle(self, *args, **options):
email = options.get("email")
password = get_random_string()
User.objects.create_superuser(
str(uuid.uuid4()), email=email, password=password
)
self.stdout.write(
self.style.SUCCESS("Successfully created a Shynet superuser")
)
password = get_random_string(10)
User.objects.create_superuser(str(uuid.uuid4()), email=email, password=password)
self.stdout.write(self.style.SUCCESS("Successfully created a Shynet superuser"))
self.stdout.write(f"Email address: {email}")
self.stdout.write(f"Password: {password}")

View File

@ -0,0 +1,48 @@
import traceback
import uuid
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand, CommandError
from django.db import DEFAULT_DB_ALIAS, connections
from django.db.utils import ConnectionHandler, OperationalError
from django.utils.crypto import get_random_string
from core.models import User
class Command(BaseCommand):
help = "Internal command to perform startup checks."
def check_migrations(self):
from django.db.migrations.executor import MigrationExecutor
try:
executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
except OperationalError:
# DB_NAME database not found?
return True
except ImproperlyConfigured:
# No databases are configured (or the dummy one)
return True
if executor.migration_plan(executor.loader.graph.leaf_nodes()):
return True
return False
def handle(self, *args, **options):
migration = self.check_migrations()
admin, whitelabel = [True] * 2
if not migration:
admin = not User.objects.all().exists()
whitelabel = (
not Site.objects.filter(name__isnull=False)
.exclude(name__exact="")
.exclude(name__exact="example.com")
.exists()
)
self.stdout.write(self.style.SUCCESS(f"{migration} {admin} {whitelabel}"))

View File

@ -1,10 +1,12 @@
from django.core.management.base import BaseCommand, CommandError
import traceback
import uuid
from django.conf import settings
from django.contrib.sites.models import Site
from core.models import User
from django.core.management.base import BaseCommand, CommandError
from django.utils.crypto import get_random_string
import uuid
import traceback
from core.models import User
class Command(BaseCommand):

View File

@ -112,7 +112,9 @@ class Migration(migrations.Migration):
"verbose_name_plural": "users",
"abstract": False,
},
managers=[("objects", django.contrib.auth.models.UserManager()),],
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name="Service",

View File

@ -11,6 +11,7 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name="service", options={"ordering": ["name", "uuid"]},
name="service",
options={"ordering": ["name", "uuid"]},
),
]

View File

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_auto_20200415_1742'),
("core", "0002_auto_20200415_1742"),
]
operations = [
migrations.AddField(
model_name='service',
name='respect_dnt',
model_name="service",
name="respect_dnt",
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-05-02 16:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0003_service_respect_dnt"),
]
operations = [
migrations.AddField(
model_name="service",
name="collect_ips",
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.0.6 on 2020-05-07 20:28
from django.db import migrations, models
import core.models
class Migration(migrations.Migration):
dependencies = [
("core", "0004_service_collect_ips"),
]
operations = [
migrations.AddField(
model_name="service",
name="ignored_ips",
field=models.TextField(
blank=True, default="", validators=[core.models._validate_network_list]
),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.0.6 on 2020-05-07 21:23
from django.db import migrations, models
import core.models
class Migration(migrations.Migration):
dependencies = [
("core", "0005_service_ignored_ips"),
]
operations = [
migrations.AddField(
model_name="service",
name="hide_referrer_regex",
field=models.TextField(
blank=True, default="", validators=[core.models._validate_regex]
),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-06-15 16:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0006_service_hide_referrer_regex"),
]
operations = [
migrations.AddField(
model_name="service",
name="ignore_robots",
field=models.BooleanField(default=False),
)
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.1b1 on 2020-06-28 18:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0007_service_ignore_robots"),
]
operations = [
migrations.AddField(
model_name="service",
name="script_inject",
field=models.TextField(blank=True, default=""),
),
migrations.AlterField(
model_name="user",
name="first_name",
field=models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.5 on 2021-11-17 07:17
import core.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_auto_20200628_1403'),
]
operations = [
migrations.AddField(
model_name='user',
name='api_token',
field=models.TextField(null=True, unique=True),
),
migrations.AlterField(
model_name='user',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -0,0 +1,89 @@
# Generated by Django 3.2.12 on 2022-06-24 11:44
import core.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0009_auto_20211117_0217'),
]
operations = [
migrations.AlterModelOptions(
name='service',
options={'ordering': ['name', 'uuid'], 'verbose_name': 'Service', 'verbose_name_plural': 'Services'},
),
migrations.AlterField(
model_name='service',
name='collaborators',
field=models.ManyToManyField(blank=True, related_name='collaborating_services', to=settings.AUTH_USER_MODEL, verbose_name='Collaborators'),
),
migrations.AlterField(
model_name='service',
name='collect_ips',
field=models.BooleanField(default=True, verbose_name='Collect ips'),
),
migrations.AlterField(
model_name='service',
name='created',
field=models.DateTimeField(auto_now_add=True, verbose_name='created'),
),
migrations.AlterField(
model_name='service',
name='hide_referrer_regex',
field=models.TextField(blank=True, default='', validators=[core.models._validate_regex], verbose_name='Hide referrer regex'),
),
migrations.AlterField(
model_name='service',
name='ignore_robots',
field=models.BooleanField(default=False, verbose_name='Ignore robots'),
),
migrations.AlterField(
model_name='service',
name='ignored_ips',
field=models.TextField(blank=True, default='', validators=[core.models._validate_network_list], verbose_name='Igored ips'),
),
migrations.AlterField(
model_name='service',
name='link',
field=models.URLField(blank=True, verbose_name='link'),
),
migrations.AlterField(
model_name='service',
name='name',
field=models.TextField(max_length=64, verbose_name='Name'),
),
migrations.AlterField(
model_name='service',
name='origins',
field=models.TextField(default='*', verbose_name='origins'),
),
migrations.AlterField(
model_name='service',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owning_services', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='service',
name='respect_dnt',
field=models.BooleanField(default=True, verbose_name='Respect dnt'),
),
migrations.AlterField(
model_name='service',
name='script_inject',
field=models.TextField(blank=True, default='', verbose_name='Script inject'),
),
migrations.AlterField(
model_name='service',
name='status',
field=models.CharField(choices=[('AC', 'Active'), ('AR', 'Archived')], db_index=True, default='AC', max_length=2, verbose_name='status'),
),
migrations.AlterField(
model_name='user',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -1,22 +1,59 @@
import json
import ipaddress
import re
import uuid
from secrets import token_urlsafe
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.functions import TruncDate
from django.db.models.functions import TruncDate, TruncHour
from django.db.utils import NotSupportedError
from django.shortcuts import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
# How long a session a needs to go without an update to no longer be considered 'active' (i.e., currently online)
ACTIVE_USER_TIMEDELTA = timezone.timedelta(
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
)
RESULTS_LIMIT = 300
def _default_uuid():
return str(uuid.uuid4())
def _validate_network_list(networks: str):
try:
_parse_network_list(networks)
except ValueError as e:
raise ValidationError(str(e))
def _validate_regex(regex: str):
try:
re.compile(regex)
except re.error:
raise ValidationError(f"'{regex}' is not valid RegEx")
def _parse_network_list(networks: str):
if len(networks.strip()) == 0:
return []
return [ipaddress.ip_network(network.strip()) for network in networks.split(",")]
def _default_api_token():
return token_urlsafe(32)
class User(AbstractUser):
username = models.TextField(default=_default_uuid, unique=True)
email = models.EmailField(unique=True)
api_token = models.TextField(default=_default_api_token, unique=True)
def __str__(self):
return self.email
@ -25,30 +62,74 @@ class User(AbstractUser):
class Service(models.Model):
ACTIVE = "AC"
ARCHIVED = "AR"
SERVICE_STATUSES = [(ACTIVE, "Active"), (ARCHIVED, "Archived")]
SERVICE_STATUSES = [(ACTIVE, _("Active")), (ARCHIVED, _("Archived"))]
uuid = models.UUIDField(default=_default_uuid, primary_key=True)
name = models.TextField(max_length=64)
name = models.TextField(max_length=64, verbose_name=_("Name"))
owner = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="owning_services"
User,
verbose_name=_("Owner"),
on_delete=models.CASCADE,
related_name="owning_services",
)
collaborators = models.ManyToManyField(
User, related_name="collaborating_services", blank=True
User,
verbose_name=_("Collaborators"),
related_name="collaborating_services",
blank=True,
)
created = models.DateTimeField(auto_now_add=True)
link = models.URLField(blank=True)
origins = models.TextField(default="*")
created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))
link = models.URLField(blank=True, verbose_name=_("link"))
origins = models.TextField(default="*", verbose_name=_("origins"))
status = models.CharField(
max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True
max_length=2,
choices=SERVICE_STATUSES,
default=ACTIVE,
db_index=True,
verbose_name=_("status"),
)
respect_dnt = models.BooleanField(default=True, verbose_name=_("Respect dnt"))
ignore_robots = models.BooleanField(default=False, verbose_name=_("Ignore robots"))
collect_ips = models.BooleanField(default=True, verbose_name=_("Collect ips"))
ignored_ips = models.TextField(
default="",
blank=True,
validators=[_validate_network_list],
verbose_name=_("Igored ips"),
)
hide_referrer_regex = models.TextField(
default="",
blank=True,
validators=[_validate_regex],
verbose_name=_("Hide referrer regex"),
)
script_inject = models.TextField(
default="", blank=True, verbose_name=_("Script inject")
)
respect_dnt = models.BooleanField(default=True)
class Meta:
verbose_name = _("Service")
verbose_name_plural = _("Services")
ordering = ["name", "uuid"]
def __str__(self):
return self.name
def get_ignored_networks(self):
return _parse_network_list(self.ignored_ips)
def get_ignored_referrer_regex(self):
if len(self.hide_referrer_regex.strip()) == 0:
return re.compile(r".^") # matches nothing
else:
try:
return re.compile(self.hide_referrer_regex)
except re.error:
# Regexes are validated in the form, but this is an important
# fallback to prevent form validation and malformed source
# data from causing all service pages to error
return re.compile(r".^")
def get_daily_stats(self):
return self.get_core_stats(
start_time=timezone.now() - timezone.timedelta(days=1)
@ -72,8 +153,10 @@ class Service(models.Model):
Session = apps.get_model("analytics", "Session")
Hit = apps.get_model("analytics", "Hit")
tz_now = timezone.now()
currently_online = Session.objects.filter(
service=self, last_seen__gt=timezone.now() - timezone.timedelta(seconds=10)
service=self, last_seen__gt=tz_now - ACTIVE_USER_TIMEDELTA
).count()
sessions = Session.objects.filter(
@ -82,58 +165,61 @@ class Service(models.Model):
session_count = sessions.count()
hits = Hit.objects.filter(
session__service=self, start_time__lt=end_time, start_time__gt=start_time
service=self, start_time__lt=end_time, start_time__gt=start_time
)
hit_count = hits.count()
bounces = sessions.annotate(hit_count=models.Count("hit")).filter(hit_count=1)
has_hits = Hit.objects.filter(service=self).exists()
bounces = sessions.filter(is_bounce=True)
bounce_count = bounces.count()
locations = (
hits.values("location")
.annotate(count=models.Count("location"))
.order_by("-count")
.order_by("-count")[:RESULTS_LIMIT]
)
referrers = (
referrer_ignore = self.get_ignored_referrer_regex()
referrers = [
referrer
for referrer in (
hits.filter(initial=True)
.values("referrer")
.annotate(count=models.Count("referrer"))
.order_by("-count")
.order_by("-count")[:RESULTS_LIMIT]
)
if not referrer_ignore.match(referrer["referrer"])
]
countries = (
sessions.values("country")
.annotate(count=models.Count("country"))
.order_by("-count")
.order_by("-count")[:RESULTS_LIMIT]
)
operating_systems = (
sessions.values("os").annotate(count=models.Count("os")).order_by("-count")
sessions.values("os")
.annotate(count=models.Count("os"))
.order_by("-count")[:RESULTS_LIMIT]
)
browsers = (
sessions.values("browser")
.annotate(count=models.Count("browser"))
.order_by("-count")
.order_by("-count")[:RESULTS_LIMIT]
)
device_types = (
sessions.values("device_type")
.annotate(count=models.Count("device_type"))
.order_by("-count")
.order_by("-count")[:RESULTS_LIMIT]
)
devices = (
sessions.values("device")
.annotate(count=models.Count("device"))
.order_by("-count")
)
device_types = (
sessions.values("device_type")
.annotate(count=models.Count("device_type"))
.order_by("-count")
.order_by("-count")[:RESULTS_LIMIT]
)
avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
@ -142,6 +228,37 @@ class Service(models.Model):
avg_hits_per_session = hit_count / session_count if session_count > 0 else None
avg_session_duration = self._get_avg_session_duration(sessions, session_count)
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
sessions, hits, start_time, end_time, tz_now
)
return {
"currently_online": currently_online,
"session_count": session_count,
"hit_count": hit_count,
"has_hits": has_hits,
"bounce_rate_pct": bounce_count * 100 / session_count
if session_count > 0
else None,
"avg_session_duration": avg_session_duration,
"avg_load_time": avg_load_time,
"avg_hits_per_session": avg_hits_per_session,
"locations": locations,
"referrers": referrers,
"countries": countries,
"operating_systems": operating_systems,
"browsers": browsers,
"devices": devices,
"device_types": device_types,
"chart_data": chart_data,
"chart_tooltip_format": chart_tooltip_format,
"chart_granularity": chart_granularity,
"online": True,
}
def _get_avg_session_duration(self, sessions, session_count):
try:
avg_session_duration = sessions.annotate(
duration=models.F("last_seen") - models.F("start_time")
@ -156,44 +273,80 @@ class Service(models.Model):
if session_count == 0:
avg_session_duration = None
session_chart_data = {
k["date"]: k["count"]
for k in sessions.annotate(date=TruncDate("start_time"))
return avg_session_duration
def _get_chart_data(self, sessions, hits, start_time, end_time, tz_now):
# Show hourly chart for date ranges of 3 days or less, otherwise daily chart
if (end_time - start_time).days < 3:
chart_tooltip_format = "MM/dd HH:mm"
chart_granularity = "hourly"
sessions_per_hour = (
sessions.annotate(hour=TruncHour("start_time"))
.values("hour")
.annotate(count=models.Count("uuid"))
.order_by("hour")
)
chart_data = {
k["hour"]: {"sessions": k["count"], "hits": 0}
for k in sessions_per_hour
}
hits_per_hour = (
hits.annotate(hour=TruncHour("start_time"))
.values("hour")
.annotate(count=models.Count("id"))
.order_by("hour")
)
for k in hits_per_hour:
if k["hour"] not in chart_data:
chart_data[k["hour"]] = {"hits": k["count"], "sessions": 0}
else:
chart_data[k["hour"]]["hits"] = k["count"]
hours_range = range(int((end_time - start_time).total_seconds() / 3600) + 1)
for hour_offset in hours_range:
hour = start_time + timezone.timedelta(hours=hour_offset)
if hour not in chart_data and hour <= tz_now:
chart_data[hour] = {"sessions": 0, "hits": 0}
else:
chart_tooltip_format = "MMM d"
chart_granularity = "daily"
sessions_per_day = (
sessions.annotate(date=TruncDate("start_time"))
.values("date")
.annotate(count=models.Count("uuid"))
.order_by("date")
)
chart_data = {
k["date"]: {"sessions": k["count"], "hits": 0} for k in sessions_per_day
}
hits_per_day = (
hits.annotate(date=TruncDate("start_time"))
.values("date")
.annotate(count=models.Count("id"))
.order_by("date")
)
for k in hits_per_day:
if k["date"] not in chart_data:
chart_data[k["date"]] = {"hits": k["count"], "sessions": 0}
else:
chart_data[k["date"]]["hits"] = k["count"]
for day_offset in range((end_time - start_time).days + 1):
day = (start_time + timezone.timedelta(days=day_offset)).date()
if day not in session_chart_data:
session_chart_data[day] = 0
if day not in chart_data and day <= tz_now.date():
chart_data[day] = {"sessions": 0, "hits": 0}
return {
"currently_online": currently_online,
"session_count": session_count,
"hit_count": hit_count,
"avg_hits_per_session": hit_count / (max(session_count, 1)),
"bounce_rate_pct": bounce_count * 100 / session_count
if session_count > 0
else None,
"avg_session_duration": avg_session_duration,
"avg_load_time": avg_load_time,
"avg_hits_per_session": avg_hits_per_session,
"locations": locations,
"referrers": referrers,
"countries": countries,
"operating_systems": operating_systems,
"browsers": browsers,
"devices": devices,
"device_types": device_types,
"session_chart_data": json.dumps(
[
{"x": str(key), "y": value}
for key, value in session_chart_data.items()
]
),
"online": True,
chart_data = sorted(chart_data.items(), key=lambda k: k[0])
chart_data = {
"sessions": [v["sessions"] for k, v in chart_data],
"hits": [v["hits"] for k, v in chart_data],
"labels": [str(k) for k, v in chart_data],
}
return chart_data, chart_tooltip_format, chart_granularity
def get_absolute_url(self):
return reverse("dashboard:service", kwargs={"pk": self.pk},)
return reverse(
"dashboard:service",
kwargs={"pk": self.pk},
)

View File

@ -0,0 +1 @@

10
shynet/core/utils.py Normal file
View File

@ -0,0 +1,10 @@
import uuid
def is_valid_uuid(value: str) -> bool:
"""Check if a string is a valid UUID."""
try:
uuid.UUID(value)
return True
except ValueError:
return False

View File

@ -1,44 +1,117 @@
from allauth.account.admin import EmailAddress
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from core.models import Service, User
from allauth.account.admin import EmailAddress
class ServiceForm(forms.ModelForm):
class Meta:
model = Service
fields = ["name", "link", "respect_dnt", "collaborators"]
fields = [
"name",
"link",
"respect_dnt",
"collect_ips",
"ignored_ips",
"ignore_robots",
"hide_referrer_regex",
"origins",
"collaborators",
"script_inject",
]
widgets = {
"name": forms.TextInput(),
"origins": forms.TextInput(),
"respect_dnt": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
"ignored_ips": forms.TextInput(),
"respect_dnt": forms.RadioSelect(
choices=[(True, _("Yes")), (False, _("No"))]
),
"collect_ips": forms.RadioSelect(
choices=[(True, _("Yes")), (False, _("No"))]
),
"ignore_robots": forms.RadioSelect(
choices=[(True, _("Yes")), (False, _("No"))]
),
"hide_referrer_regex": forms.TextInput(),
"script_inject": forms.Textarea(attrs={"class": "font-mono", "rows": 5}),
}
labels = {
"origins": "Allowed Hostnames",
"respect_dnt": "Respect DNT",
"origins": _("Allowed origins"),
"respect_dnt": _("Respect DNT"),
"ignored_ips": _("Ignored IP addresses"),
"ignore_robots": _("Ignore robots"),
"hide_referrer_regex": _("Hide specific referrers"),
"script_inject": _("Additional injected JS"),
}
help_texts = {
"name": _("What should the service be called?"),
"link": _("What's the service's primary URL?"),
"origins": _(
"At what hostnames does the service operate? This sets CORS headers, so use '*' if you're not sure (or don't care)."
"At what origins does the service operate? Use commas to separate multiple values. This sets CORS headers, so use '*' if you're not sure (or don't care)."
),
"respect_dnt": _(
"Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?"
),
"ignored_ips": _(
"A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32')."
),
"ignore_robots": _(
"Should sessions generated by bots be excluded from tracking?"
),
"hide_referrer_regex": _(
"Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will not be listed in the referrer summary. Sessions will still be tracked normally. No effect if left blank."
),
"script_inject": _(
"Optional additional JavaScript to inject at the end of the Shynet script. This code will be injected on every page where this service is installed."
),
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?",
}
collaborators = forms.CharField(help_text="Which users should have read-only access to this service? (Comma separated list of emails.)", required=False)
collect_ips = forms.BooleanField(
help_text=_("IP address collection is disabled globally by your administrator.")
if settings.BLOCK_ALL_IPS
else _(
"Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected."
),
widget=forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]),
initial=False if settings.BLOCK_ALL_IPS else True,
required=False,
disabled=settings.BLOCK_ALL_IPS,
)
def clean_collect_ips(self):
collect_ips = self.cleaned_data["collect_ips"]
# Forces collect IPs to be false if it is disabled globally
return False if settings.BLOCK_ALL_IPS else collect_ips
collaborators = forms.CharField(
help_text=_(
"Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)"
),
required=False,
)
def clean_collaborators(self):
collaborators = []
users_to_emails = (
{}
) # maps users to the email they are listed under as a collaborator
for collaborator_email in self.cleaned_data["collaborators"].split(","):
email = collaborator_email.strip()
if email == "":
continue
collaborator_email_linked = EmailAddress.objects.filter(email__iexact=email).first()
collaborator_email_linked = EmailAddress.objects.filter(
email__iexact=email
).first()
if collaborator_email_linked is None:
raise forms.ValidationError(f"Email '{email}' is not registered")
user = collaborator_email_linked.user
if user in collaborators:
raise forms.ValidationError(
f"The emails '{email}' and '{users_to_emails[user]}' both correspond to the same user"
)
users_to_emails[user] = email
collaborators.append(collaborator_email_linked.user)
return collaborators

View File

@ -0,0 +1,698 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:43+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: forms.py:28 forms.py:29 forms.py:30 forms.py:59
msgid "Yes"
msgstr "Ja"
#: forms.py:28 forms.py:29 forms.py:30 forms.py:59
msgid "No"
msgstr "Nein"
#: forms.py:35
msgid "Allowed origins"
msgstr "Erlaubte origins"
#: forms.py:36
msgid "Respect DNT"
msgstr "DNT beachten"
#: forms.py:37
msgid "Ignored IP addresses"
msgstr "Ignorierte IP Adressen"
#: forms.py:38
msgid "Ignore robots"
msgstr "Robots ignorieren"
#: forms.py:39
msgid "Hide specific referrers"
msgstr "Bestimmte Referrer nicht zeigen"
#: forms.py:40
msgid "Additional injected JS"
msgstr "Zusätzlich injiziertes JS"
#: forms.py:43
msgid "What should the service be called?"
msgstr "Welchen Namen soll der Dienst haben?"
#: forms.py:44
msgid "What's the service's primary URL?"
msgstr "Was ist die primäre URL des Dienstes?"
#: forms.py:46
msgid ""
"At what origins does the service operate? Use commas to separate multiple "
"values. This sets CORS headers, so use '*' if you're not sure (or don't "
"care)."
msgstr ""
"Mit welchen origins arbeitet der Dienst? Verwenden Sie Kommas, um mehrere "
"Werte zu trennen. Dies setzt den CORS-Header. Verwenden Sie '*', wenn Sie "
"nicht sicher sind (oder es egal ist)."
#: forms.py:48
msgid ""
"Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/"
"Do_Not_Track'>Do Not Track</a> be excluded from all data?"
msgstr ""
"Sollen Besucher, die <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do "
"Not Track</a> aktiviert haben, von allen Daten ausgeschlossen werden?"
#: forms.py:49
msgid ""
"A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to "
"exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32')."
msgstr ""
"Eine Komma-separierte Liste von IP Adressen oder IP Bereichen (IPv4 and "
"IPv6), die vom Tracking ausgeschlossen werden sollen (z.B. '192.168.0.2, "
"127.0.0.1/32')."
#: forms.py:50
msgid "Should sessions generated by bots be excluded from tracking?"
msgstr ""
"Sollten von Bots generierte Sitzungen vom Tracking ausgeschlossen werden?"
#: forms.py:51
msgid ""
"Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will "
"not be listed in the referrer summary. Sessions will still be tracked "
"normally. No effect if left blank."
msgstr ""
"Alle Referrer, die diesem <a href='https://regexr.com/'>RegEx</a> "
"entsprechen, werden nicht in der Referrer-Zusammenfassung aufgeführt. Die "
"Sitzungen werden weiterhin normal verfolgt."
#: forms.py:52
msgid ""
"Optional additional JavaScript to inject at the end of the Shynet script. "
"This code will be injected on every page where this service is installed."
msgstr ""
"Optionales zusätzliches JavaScript, das am Ende des Shynet-Skripts eingefügt "
"wird. Dieser Code wird auf jeder Seite eingeschleust, auf der dieser Dienst "
"installiert ist."
#: forms.py:56
msgid "IP address collection is disabled globally by your administrator."
msgstr ""
"Die Erfassung von IP-Adressen wurde vom Administrator global deaktiviert."
#: forms.py:58
msgid ""
"Should individual IP addresses be collected? IP metadata (location, host, "
"etc) will still be collected."
msgstr ""
"Sollten einzelne IP-Adressen erfasst werden? IP-Metadaten (Standort, Host, "
"usw.) werden weiterhin erfasst."
#: forms.py:71
msgid ""
"Which users on this Shynet instance should have read-only access to this "
"service? (Comma separated list of emails.)"
msgstr ""
"Welche Benutzer dieser Shynet-Instanz sollen Lesezugriff auf diesen Dienst "
"haben? (Kommaseparierte Liste von E-Mails.)"
#: templates/account/account_inactive.html:5
#: templates/account/account_inactive.html:6
msgid "Account Inactive"
msgstr "Konto inaktiv"
#: templates/account/account_inactive.html:9
msgid "This account is inactive."
msgstr "Dieses Konto ist inaktiv"
#: templates/account/email.html:5 templates/account/email.html:6
msgid "Email Addresses"
msgstr "E-Mail Adressen"
#: templates/account/email.html:12
msgid "These are your known email addresses:"
msgstr "Ihre bekannten E-Mail Adressen:"
#: templates/account/email.html:26
msgid "Verified"
msgstr "Verifiziert"
#: templates/account/email.html:28
msgid "Unverified"
msgstr "Unverifiziert"
#: templates/account/email.html:30
msgid "Primary"
msgstr "Primär"
#: templates/account/email.html:36
msgid "Make Primary"
msgstr "Als primär kennzeichnen"
#: templates/account/email.html:37
msgid "Resend Verification"
msgstr "Verifikation erneut senden"
#: templates/account/email.html:38
msgid "Remove"
msgstr "Entfernen"
#: templates/account/email.html:46
msgid ""
"You currently do not have an email address associated with your account. "
"Without one, you won't be able to reset your password, receive "
"notifications, etc."
msgstr ""
"Sie haben noch keine E-Mail Adresse mit Ihrem Konto verknüpft. Ohne diese "
"können Sie Ihr Kennwort nicht zurücksetzen, keine Benachrichtigungen "
"erhalten etc."
#: templates/account/email.html:57
msgid "Add Address"
msgstr "Adresse hinzufügen"
#: templates/account/email.html:66
msgid "Do you really want to remove the selected email address?"
msgstr "Wollen Sie diese E-Mail Adresse wirkliche löschen?"
#: templates/account/email/email_confirmation_message.txt:1
#, python-format
msgid ""
"Hi there,\n"
"\n"
"You're receiving this email because %(user_display)s has listed this email "
"as a valid contact address for their account.\n"
"\n"
"To confirm this is correct, go to %(activate_url)s\n"
msgstr ""
"Hallo,\n"
"\n"
"Sie erhalten diese E-Mail, da der Benutzer %(user_display)s diese Adresse "
"als gültige Kontakt-Adressse für sein Konto angegeben hat.\n"
"\n"
"Um die E-Mail Adresse zu bestätigen, gehen Sie auf %(activate_url)s\n"
#: templates/account/email/email_confirmation_message.txt:7
#: templates/account/email/password_reset_key_message.txt:9
#, python-format
msgid ""
"Thank you,\n"
"%(site_name)s\n"
msgstr ""
#: templates/account/email/email_confirmation_subject.txt:3
#: templates/account/email_confirm.html:6
#: templates/account/email_confirm.html:7
msgid "Confirm Email Address"
msgstr "E-Mail Adresse bestätigen"
#: templates/account/email/password_reset_key_message.txt:1
msgid ""
"Hi there,\n"
"\n"
"You're receiving this email because you or someone else has requested a "
"password for your account.\n"
"\n"
"This message can be safely ignored if you did not request a password reset. "
"Click the link below to reset your password."
msgstr ""
"Hallo,\n"
"\n"
"Sie erhalten diese E-Mail, weil Sie oder jemand anderes ein Kennwort für Ihr "
"Konto angefordert hat.\n"
"\n"
"Sie können diese Nachricht ignorieren, wenn Sie kein Kennwort angefordert "
"haben. Klicken Sie auf den unten stehenden Link, um Ihr Kennwort "
"zurückzusetzen."
#: templates/account/email/password_reset_key_subject.txt:3
msgid "Password Reset Email"
msgstr "Kennwort zurücksetzen E-Mail"
#: templates/account/email_confirm.html:15
#, python-format
msgid ""
"Please confirm that <a\n"
" href=\"mailto:%(email)s\">%(email)s</a> is a valid email where we "
"can reach you."
msgstr ""
"Bitte bestätigen Sie, dass <a\n"
" href=\"mailto:%(email)s\">%(email)s</a> eine gültige E-Mail Adress "
"ist, unter der wie Sie erreichen können."
#: templates/account/email_confirm.html:21
msgid "Confirm"
msgstr "Bestätigen"
#: templates/account/email_confirm.html:28
#, python-format
msgid ""
"This email confirmation link expired or is invalid. Please <a href="
"\"%(email_url)s\">issue a new\n"
" email confirmation request</a>."
msgstr ""
"Diese Bestätigungs-E-Mail ist abgelaufen oder ungültig. Bitte <a href="
"\"%(email_url)s\">fordern Sie eine neue\n"
" Bestätigungs-E-Mail</a>. an"
#: templates/account/login.html:6 templates/account/login.html:7
#: templates/account/login.html:19
msgid "Sign In"
msgstr "Anmelden"
#: templates/account/login.html:20 templates/account/password_reset.html:21
msgid "Reset Password"
msgstr "Kennwort zurücksetzen"
#: templates/account/logout.html:5 templates/account/logout.html:6
#: templates/account/logout.html:16
msgid "Sign Out"
msgstr "Abmelden"
#: templates/account/logout.html:9
msgid "Are you sure you want to sign out?"
msgstr "Sind Sie sicher, dass Sie sich abmelden wollen?"
#: templates/account/messages/cannot_delete_primary_email.txt:2
#, python-format
msgid "You cannot remove your primary email address (%(email)s)."
msgstr "Sie können Ihre primäre E-Mail Adresse nicht entfernen (%(email)s)."
#: templates/account/messages/email_confirmation_sent.txt:2
#, python-format
msgid "Confirmation email sent to %(email)s."
msgstr "Bestätigungs-E-Mail gesendet an %(email)s."
#: templates/account/messages/email_confirmed.txt:2
#, python-format
msgid "Confirmed %(email)s."
msgstr "%(email)s bestätigt."
#: templates/account/messages/email_deleted.txt:2
#, python-format
msgid "Removed email address %(email)s."
msgstr "E-Mail-Adresse %(email)s entfernt."
#: templates/account/messages/logged_in.txt:4
#, python-format
msgid "Successfully signed in as %(name)s."
msgstr "Sid sind angemeldet als %(name)s"
#: templates/account/messages/logged_out.txt:2
msgid "You have signed out."
msgstr "Sie haben sich abgemeldet."
#: templates/account/messages/password_changed.txt:2
msgid "Password successfully changed."
msgstr "Ihr Kennwort wurde erfolgreich geändert."
#: templates/account/messages/password_set.txt:2
msgid "Password successfully set."
msgstr "Kennwort erfolgreich gesetzt."
#: templates/account/messages/primary_email_set.txt:2
msgid "New primary email address set."
msgstr "Eine neue primäre E-Mail Adresse wurde gesetzt."
#: templates/account/messages/unverified_primary_email.txt:2
msgid "Your primary email address must be verified."
msgstr "Ihre primäre E-Mail-Adresse muss verifiziert werden."
#: templates/account/password_change.html:5
#: templates/account/password_change.html:6
#: templates/account/password_change.html:12
#: templates/account/password_reset_from_key.html:4
#: templates/account/password_reset_from_key.html:5
#: templates/account/password_reset_from_key.html:16
#: templates/account/password_reset_from_key_done.html:4
#: templates/account/password_reset_from_key_done.html:5
msgid "Change Password"
msgstr "Kennwort ändern"
#: templates/account/password_reset.html:6
#: templates/account/password_reset.html:7
#: templates/account/password_reset_done.html:6
#: templates/account/password_reset_done.html:7
msgid "Password Reset"
msgstr "Kennwort zurücksetzen"
#: templates/account/password_reset.html:15
msgid ""
"Forgotten your password? Enter your email address below, and we'll send you "
"an email to reset it."
msgstr ""
"Haben Sie Ihr Kennwort vergessen? Geben Sie unten Ihre E-Mail-Adresse ein, "
"und wir senden Ihnen eine E-Mail, um es zurückzusetzen."
#: templates/account/password_reset_done.html:14
msgid ""
"We have sent you an email with a password reset link. Please try again if "
"you do not receive it within a few minutes."
msgstr ""
"Wir haben Ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts "
"geschickt. Bitte versuchen Sie es erneut, wenn Sie ihn nicht innerhalb "
"weniger Minuten erhalten."
#: templates/account/password_reset_from_key.html:10
#, python-format
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a <a href=\"%(passwd_reset_url)s\">new password reset</"
"a>."
msgstr ""
"Der Link zum Zurücksetzen des Kennworts war ungültig, möglicherweise weil er "
"bereits verwendet wurde. Bitte fordern Sie eine <a href="
"\"%(passwd_reset_url)s\">neue Kennwortrücksetzung</a> an."
#: templates/account/password_reset_from_key.html:19
#: templates/account/password_reset_from_key_done.html:8
msgid "Your password is now changed."
msgstr "Ihr Kennwort wurde geändert."
#: templates/account/password_set.html:5 templates/account/password_set.html:6
#: templates/account/password_set.html:12
msgid "Set Password"
msgstr "Kennwort setzen"
#: templates/account/signup.html:5 templates/account/signup.html:6
#: templates/account/signup.html:17
msgid "Sign Up"
msgstr "Registrieren"
#: templates/account/signup.html:9
#, python-format
msgid ""
"Already have an account? Then please <a href=\"%(login_url)s\">sign in</a> "
"instead."
msgstr ""
"Sie haben bereits ein Konto? Dann melden Sie sich bitt<a href=\"%(login_url)s"
"\">sign in</a> "
#: templates/account/signup_closed.html:5
#: templates/account/signup_closed.html:6
msgid "Sign Up Closed"
msgstr "Registrierung geschlossen"
#: templates/account/signup_closed.html:9
msgid "Public sign-ups are not allowed at this time."
msgstr "Öffentliche Registrierungen sind zur Zeit nicht erlaubt."
#: templates/account/snippets/already_logged_in.html:6
msgid "Note"
msgstr "Hinweis"
#: templates/account/snippets/already_logged_in.html:6
#, python-format
msgid "you are already logged in as %(user_display)s."
msgstr "Sie sind bereits als %(user_display)s angemeldet."
#: templates/account/verification_sent.html:5
#: templates/account/verification_sent.html:6
#: templates/account/verified_email_required.html:5
#: templates/account/verified_email_required.html:6
msgid "Verify Email Address"
msgstr "E-Mail Adresse verifizieren"
#: templates/account/verification_sent.html:9
msgid ""
"We have sent an email to you for verification. Follow the link provided to "
"finalize the signup process. Please try to log in again if you do not "
"receive it within a few minutes."
msgstr ""
"Wir haben Ihnen eine E-Mail zur Verifizierung gesendet. Folgen Sie dem "
"enthaltenen Link, um den Registrierungsprozess abzuschließen. Versuchen Sie "
"sich erneut anzumelden, falls Sie die E-Mail nicht innerhalb weniger Minuten "
"erhalten."
#: templates/account/verified_email_required.html:11
msgid ""
"This part of the site requires us to verify that\n"
"you are who you claim to be. For this purpose, we require that you\n"
"verify ownership of your email address. "
msgstr ""
"Dieser Teil der Website erfordert eine Überprüfung, dass Sie derjenige "
"sind,\n"
"der Sie vorgeben zu sein. Zu diesem Zweck verlangen wir, dass Sie\n"
"den Besitz Ihrer E-Mail-Adresse bestätigen. "
#: templates/account/verified_email_required.html:15
msgid ""
"We have sent an email to you for\n"
"verification. Please click on the link inside this email. Please\n"
"try again if you do not receive it within a few minutes."
msgstr ""
"Wir haben Ihnen eine E-Mail zur Überprüfung gesendet.\n"
"Bitte klicken Sie auf den Link in dieser E-Mail. Bitte\n"
"versuchen Sie es erneut, wenn Sie die E-Mail nicht innerhalb \n"
"weniger Minuten erhalten."
#: templates/account/verified_email_required.html:19
#, python-format
msgid ""
"<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your "
"email address</a>."
msgstr ""
"<strong>Hinweis:</strong> Sie können Ihre E-Mail-Adresse noch <a href="
"\"%(email_url)s\">ändern</a>."
#: templates/base.html:47
msgid "Services"
msgstr "Sitzungen"
#: templates/base.html:60
msgid "+ Create"
msgstr "+ Neu"
#: templates/base.html:69
msgid "Collaborations"
msgstr "Zusammenarbeit"
#: templates/base.html:81
msgid "Account"
msgstr "Konto"
#: templates/dashboard/includes/service_form.html:8
msgid "Advanced settings"
msgstr "Erweiterte Einstellungen"
#: templates/dashboard/includes/service_overview.html:15
#: templates/dashboard/pages/service_session_list.html:5
msgid "Sessions"
msgstr "Sitzungen"
#: templates/dashboard/includes/service_overview.html:22
#: templates/dashboard/includes/session_list.html:9
#: templates/dashboard/pages/service.html:38
#: templates/dashboard/pages/service.html:110
msgid "Hits"
msgstr "Besuche"
#: templates/dashboard/includes/service_overview.html:29
#: templates/dashboard/pages/service.html:60
msgid "Bounce Rate"
msgstr "Absprungrate"
#: templates/dashboard/includes/service_overview.html:40
msgid "Avg. Duration"
msgstr "Durchschn. Dauer"
#: templates/dashboard/includes/session_list.html:5
msgid "Session Start"
msgstr "Sitzungsstart"
#: templates/dashboard/includes/session_list.html:6
msgid "Identity"
msgstr "Kennung"
#: templates/dashboard/includes/session_list.html:7
#: templates/dashboard/pages/service_session.html:43
msgid "Network"
msgstr "Netzwerk"
#: templates/dashboard/includes/session_list.html:8
#: templates/dashboard/pages/service.html:73
#: templates/dashboard/pages/service_session.html:81
msgid "Duration"
msgstr "Dauer"
#: templates/dashboard/includes/session_list.html:36
msgid "No data yet"
msgstr "Noch keine Daten"
#: templates/dashboard/pages/dashboard.html:16
msgid "New Service"
msgstr "Neuer Dienst"
#: templates/dashboard/pages/index.html:6
#: templates/dashboard/pages/service_session_list.html:9
msgid "Analytics"
msgstr "Analytik"
#: templates/dashboard/pages/index.html:8
msgid "Log In"
msgstr "Anmelden"
#: templates/dashboard/pages/service.html:9
msgid "Manage"
msgstr "Verwalten"
#: templates/dashboard/pages/service.html:17
msgid ""
"This service hasn't collected any data yet. To get started, place the "
"following code snippet at the end of the <code>&lt;body&gt;</code> tag on "
"any page you'd like to track."
msgstr ""
#: templates/dashboard/pages/service.html:29
#: templates/dashboard/pages/service.html:158
#: templates/dashboard/pages/service.html:192
#: templates/dashboard/pages/service.html:226
#: templates/dashboard/pages/service.html:260
#: templates/dashboard/pages/service.html:295
msgid "sessions"
msgstr "Sitzungen"
#: templates/dashboard/pages/service.html:47
msgid "Load Time"
msgstr "Ladezeit"
#: templates/dashboard/pages/service.html:86
msgid "Hits/Session"
msgstr "Besuche/Sitzung"
#: templates/dashboard/pages/service.html:109
#: templates/dashboard/pages/service_session.html:51
msgid "Location"
msgstr "Ort"
#: templates/dashboard/pages/service.html:133
msgid "No data yet..."
msgstr "Noch keine Daten..."
#: templates/dashboard/pages/service.html:141
msgid "Sessions by Geography"
msgstr "Sitzungen nach Geograpie"
#: templates/dashboard/pages/service.html:143
msgid "view table"
msgstr "Tabellenansicht"
#: templates/dashboard/pages/service.html:153
#: templates/dashboard/pages/service_session.html:47
msgid "Country"
msgstr "Land"
#: templates/dashboard/pages/service.html:191
msgid "Referrer"
msgstr ""
#: templates/dashboard/pages/service.html:225
msgid "Operating System"
msgstr "Betriebssystem"
#: templates/dashboard/pages/service.html:259
#: templates/dashboard/pages/service_session.html:27
msgid "Browser"
msgstr ""
#: templates/dashboard/pages/service.html:294
#: templates/dashboard/pages/service_session.html:35
msgid "Device Type"
msgstr "Gerätetyp"
#: templates/dashboard/pages/service.html:329
msgid "View more sessions"
msgstr "Weitere Sitzungen"
#: templates/dashboard/pages/service_create.html:5
#: templates/dashboard/pages/service_create.html:8
msgid "Create Service"
msgstr "Neuer Dienst"
#: templates/dashboard/pages/service_create.html:16
msgid "Create"
msgstr "Neu"
#: templates/dashboard/pages/service_create.html:17
#: templates/dashboard/pages/service_update.html:30
msgid "Cancel"
msgstr "Abbrechen"
#: templates/dashboard/pages/service_delete.html:5
#: templates/dashboard/pages/service_update.html:33
msgid "Delete"
msgstr "Löschen"
#: templates/dashboard/pages/service_delete.html:12
msgid ""
"Are you sure you want to delete this service? All of its analytics and "
"associated data will be permanently deleted."
msgstr ""
"Sind Sie sicher, dass Sie diesen Dienst löschen wollen? Alle damit "
"verbundenen Daten werden unwiederruflich gelöscht."
#: templates/dashboard/pages/service_session.html:31
msgid "Device"
msgstr "Geräte"
#: templates/dashboard/pages/service_session.html:39
msgid "OS"
msgstr "Betriebssystem"
#: templates/dashboard/pages/service_session.html:54
msgid "Open in Maps"
msgstr "In Karte öffnen"
#: templates/dashboard/pages/service_session.html:56
msgid "Unknown"
msgstr "Unbekannt"
#: templates/dashboard/pages/service_session.html:85
msgid "Load"
msgstr "Laden"
#: templates/dashboard/pages/service_session.html:89
msgid "Tracker"
msgstr ""
#: templates/dashboard/pages/service_update.html:5
msgid "Management"
msgstr "Verwaltung"
#: templates/dashboard/pages/service_update.html:8
msgid "View"
msgstr "Ansicht"
#: templates/dashboard/pages/service_update.html:13
msgid "Installation"
msgstr ""
#: templates/dashboard/pages/service_update.html:15
msgid ""
"Place the following snippet at the end of the <code>&lt;body&gt;</code> tag "
"on any page you'd like to track."
msgstr ""
"Kopieren Sie den folgenden Code ans Ende des <code>&lt;body&gt;</code> Tags "
"in allen Seiten, die Sie tracken möchten."
#: templates/dashboard/pages/service_update.html:21
msgid "Settings"
msgstr "Einstellungen"
#: templates/dashboard/pages/service_update.html:29
msgid "Save"
msgstr "Speichern"

View File

@ -0,0 +1,659 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:43+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: forms.py:28 forms.py:29 forms.py:30 forms.py:59
msgid "Yes"
msgstr "是"
#: forms.py:28 forms.py:29 forms.py:30 forms.py:59
msgid "No"
msgstr "否"
#: forms.py:35
msgid "Allowed origins"
msgstr "允許的來源"
#: forms.py:36
msgid "Respect DNT"
msgstr "尊重停止追蹤 (Do Not Track) 設定"
#: forms.py:37
msgid "Ignored IP addresses"
msgstr "忽略的 IP"
#: forms.py:38
msgid "Ignore robots"
msgstr "忽略機器人"
#: forms.py:39
msgid "Hide specific referrers"
msgstr "隱藏特定的參照來源"
#: forms.py:40
msgid "Additional injected JS"
msgstr "額外插入的 JS"
#: forms.py:43
msgid "What should the service be called?"
msgstr "這項服務應該被稱為什麼?"
#: forms.py:44
msgid "What's the service's primary URL?"
msgstr "服務的主要 URL 是什麼?"
#: forms.py:46
msgid ""
"At what origins does the service operate? Use commas to separate multiple "
"values. This sets CORS headers, so use '*' if you're not sure (or don't "
"care)."
msgstr ""
"服務在哪些來源運作?使用逗號分隔多個值。這設定了 CORS 標頭,所以如果你不確定(或不在乎),請使用 '*'。"
#: forms.py:48
msgid ""
"Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/"
"Do_Not_Track'>Do Not Track</a> be excluded from all data?"
msgstr ""
"是否應排除已啟用 <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>停止追蹤 (Do Not Track)</a> 的訪客的所有資料?"
#: forms.py:49
msgid ""
"A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to "
"exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32')."
msgstr ""
"要從追蹤中排除的 IP 位址或 IP 範圍IPv4 和 IPv6的逗號分隔列表例如'192.168.0.2, 127.0.0.1/32')。"
#: forms.py:50
msgid "Should sessions generated by bots be excluded from tracking?"
msgstr "是否應排除由機器人產生的工作階段從追蹤中?"
#: forms.py:51
msgid ""
"Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will "
"not be listed in the referrer summary. Sessions will still be tracked "
"normally. No effect if left blank."
msgstr ""
"任何符合此 <a href='https://regexr.com/'>RegEx</a> 的參照來源將不會在參照來源摘要中列出。工作階段仍將正常追蹤。如果留空則無效果。"
#: forms.py:52
msgid ""
"Optional additional JavaScript to inject at the end of the Shynet script. "
"This code will be injected on every page where this service is installed."
msgstr ""
"選擇性的額外 JavaScript插入到 Shynet 指令碼的末端。此程式碼將插入到安裝此服務的每個頁面上。"
#: forms.py:56
msgid "IP address collection is disabled globally by your administrator."
msgstr "您的管理員已全域停用 IP 收集。"
#: forms.py:58
msgid ""
"Should individual IP addresses be collected? IP metadata (location, host, "
"etc) will still be collected."
msgstr "是否應收集個別 IP ?仍將收集 IP 中繼資料(位置、主機等)。"
#: forms.py:71
msgid ""
"Which users on this Shynet instance should have read-only access to this "
"service? (Comma separated list of emails.)"
msgstr ""
"此 Shynet 服務上的哪些使用者應具有對此服務的唯讀存取權限?(電子郵件的逗號分隔列表。)"
#: templates/account/account_inactive.html:5
#: templates/account/account_inactive.html:6
msgid "Account Inactive"
msgstr "帳戶不活躍"
#: templates/account/account_inactive.html:9
msgid "This account is inactive."
msgstr "此帳戶不活躍。"
#: templates/account/email.html:5 templates/account/email.html:6
msgid "Email Addresses"
msgstr "電子郵件地址"
#: templates/account/email.html:12
msgid "These are your known email addresses:"
msgstr "這些是您的已知電子郵件地址:"
#: templates/account/email.html:26
msgid "Verified"
msgstr "已驗證"
#: templates/account/email.html:28
msgid "Unverified"
msgstr "未驗證"
#: templates/account/email.html:30
msgid "Primary"
msgstr "主要"
#: templates/account/email.html:36
msgid "Make Primary"
msgstr "設為預設"
#: templates/account/email.html:37
msgid "Resend Verification"
msgstr "重新傳送驗證"
#: templates/account/email.html:38
msgid "Remove"
msgstr "移除"
#: templates/account/email.html:46
msgid ""
"You currently do not have an email address associated with your account. "
"Without one, you won't be able to reset your password, receive "
"notifications, etc."
msgstr ""
"您目前的帳戶沒有關聯的電子郵件地址。沒有它,您將無法重設密碼、接收通知等。"
#: templates/account/email.html:57
msgid "Add Address"
msgstr "新增地址"
#: templates/account/email.html:66
msgid "Do you really want to remove the selected email address?"
msgstr "您真的要移除選定的電子郵件地址嗎?"
#: templates/account/email/email_confirmation_message.txt:1
#, python-format
msgid ""
"Hi there,\n"
"\n"
"You're receiving this email because %(user_display)s has listed this email "
"as a valid contact address for their account.\n"
"\n"
"To confirm this is correct, go to %(activate_url)s\n"
msgstr ""
"您好,\n"
"\n"
"您收到此電子郵件是因為 %(user_display)s 已將此電子郵件列為其帳戶的有效聯絡地址。\n"
"\n"
"要確認這是正確的,請前往 %(activate_url)s\n"
#: templates/account/email/email_confirmation_message.txt:7
#: templates/account/email/password_reset_key_message.txt:9
#, python-format
msgid ""
"Thank you,\n"
"%(site_name)s\n"
msgstr ""
"謝謝您,\n"
"%(site_name)s\n"
#: templates/account/email/email_confirmation_subject.txt:3
#: templates/account/email_confirm.html:6
#: templates/account/email_confirm.html:7
msgid "Confirm Email Address"
msgstr "確認電子郵件地址"
#: templates/account/email/password_reset_key_message.txt:1
msgid ""
"Hi there,\n"
"\n"
"You're receiving this email because you or someone else has requested a "
"password for your account.\n"
"\n"
"This message can be safely ignored if you did not request a password reset. "
"Click the link below to reset your password."
msgstr ""
"您好,\n"
"\n"
"您收到此電子郵件是因為您或其他人已為您的帳戶請求密碼。\n"
"\n"
"如果您未請求重設密碼,則可以安全地忽略此訊息。點選下面的連結來重設您的密碼。"
#: templates/account/email/password_reset_key_subject.txt:3
msgid "Password Reset Email"
msgstr "密碼重設電子郵件"
#: templates/account/email_confirm.html:15
#, python-format
msgid ""
"Please confirm that <a\n"
" href=\"mailto:%(email)s\">%(email)s</a> is a valid email where we "
"can reach you."
msgstr ""
"請確認 <a\n"
" href=\"mailto:%(email)s\">%(email)s</a> 是我們可以聯絡您的有效電子郵件。"
#: templates/account/email_confirm.html:21
msgid "Confirm"
msgstr "確認"
#: templates/account/email_confirm.html:28
#, python-format
msgid ""
"This email confirmation link expired or is invalid. Please <a href="
"\"%(email_url)s\">issue a new\n"
" email confirmation request</a>."
msgstr ""
"此電子郵件確認連結已過期或無效。請<a href=\"%(email_url)s\">發出新的電子郵件確認請求</a>。"
#: templates/account/login.html:6 templates/account/login.html:7
#: templates/account/login.html:19
msgid "Sign In"
msgstr "登入"
#: templates/account/login.html:20 templates/account/password_reset.html:21
msgid "Reset Password"
msgstr "重設密碼"
#: templates/account/logout.html:5 templates/account/logout.html:6
#: templates/account/logout.html:16
msgid "Sign Out"
msgstr "登出"
#: templates/account/logout.html:9
msgid "Are you sure you want to sign out?"
msgstr "您確定要登出嗎?"
#: templates/account/messages/cannot_delete_primary_email.txt:2
#, python-format
msgid "You cannot remove your primary email address (%(email)s)."
msgstr "您不能移除您的主要電子郵件地址 (%(email)s)。"
#: templates/account/messages/email_confirmation_sent.txt:2
#, python-format
msgid "Confirmation email sent to %(email)s."
msgstr "確認電子郵件已傳送到 %(email)s。"
#: templates/account/messages/email_confirmed.txt:2
#, python-format
msgid "Confirmed %(email)s."
msgstr "已確認 %(email)s。"
#: templates/account/messages/email_deleted.txt:2
#, python-format
msgid "Removed email address %(email)s."
msgstr "已移除電子郵件地址 %(email)s。"
#: templates/account/messages/logged_in.txt:4
#, python-format
msgid "Successfully signed in as %(name)s."
msgstr "成功以 %(name)s 的身份登入。"
#: templates/account/messages/logged_out.txt:2
msgid "You have signed out."
msgstr "您已登出。"
#: templates/account/messages/password_changed.txt:2
msgid "Password successfully changed."
msgstr "密碼已成功更改。"
#: templates/account/messages/password_set.txt:2
msgid "Password successfully set."
msgstr "密碼已成功設定。"
#: templates/account/messages/primary_email_set.txt:2
msgid "New primary email address set."
msgstr "已設定新的主要電子郵件地址。"
#: templates/account/messages/unverified_primary_email.txt:2
msgid "Your primary email address must be verified."
msgstr "您的主要電子郵件地址必須經過驗證。"
#: templates/account/password_change.html:5
#: templates/account/password_change.html:6
#: templates/account/password_change.html:12
#: templates/account/password_reset_from_key.html:4
#: templates/account/password_reset_from_key.html:5
#: templates/account/password_reset_from_key.html:16
#: templates/account/password_reset_from_key_done.html:4
#: templates/account/password_reset_from_key_done.html:5
msgid "Change Password"
msgstr "更改密碼"
#: templates/account/password_reset.html:6
#: templates/account/password_reset.html:7
#: templates/account/password_reset_done.html:6
#: templates/account/password_reset_done.html:7
msgid "Password Reset"
msgstr "密碼重設"
#: templates/account/password_reset.html:15
msgid ""
"Forgotten your password? Enter your email address below, and we'll send you "
"an email to reset it."
msgstr ""
"忘記了您的密碼?在下面輸入您的電子郵件地址,我們將向您傳送電子郵件以重設它。"
#: templates/account/password_reset_done.html:14
msgid ""
"We have sent you an email with a password reset link. Please try again if "
"you do not receive it within a few minutes."
msgstr ""
"我們已向您發送了一封帶有密碼重設連結的電子郵件。如果您在幾分鐘內未收到,請再試一次。"
#: templates/account/password_reset_from_key.html:10
#, python-format
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a <a href=\"%(passwd_reset_url)s\">new password reset</"
"a>."
msgstr ""
"密碼重設連結無效,可能是因為它已被使用。請請求<a href=\"%(passwd_reset_url)s\">新的密碼重設</a>。"
#: templates/account/password_reset_from_key.html:19
#: templates/account/password_reset_from_key_done.html:8
msgid "Your password is now changed."
msgstr "您的密碼現在已更改。"
#: templates/account/password_set.html:5 templates/account/password_set.html:6
#: templates/account/password_set.html:12
msgid "Set Password"
msgstr "設定密碼"
#: templates/account/signup.html:5 templates/account/signup.html:6
#: templates/account/signup.html:17
msgid "Sign Up"
msgstr "註冊"
#: templates/account/signup.html:9
#, python-format
msgid ""
"Already have an account? Then please <a href=\"%(login_url)s\">sign in</a> "
"instead."
msgstr ""
"已經有帳戶了嗎?那麼請<a href=\"%(login_url)s\">登入</a>。"
#: templates/account/signup_closed.html:5
#: templates/account/signup_closed.html:6
msgid "Sign Up Closed"
msgstr "註冊已關閉"
#: templates/account/signup_closed.html:9
msgid "Public sign-ups are not allowed at this time."
msgstr "目前不允許公開註冊。"
#: templates/account/snippets/already_logged_in.html:6
msgid "Note"
msgstr "注意"
#: templates/account/snippets/already_logged_in.html:6
#, python-format
msgid "you are already logged in as %(user_display)s."
msgstr "您已經以 %(user_display)s 的身份登入。"
#: templates/account/verification_sent.html:5
#: templates/account/verification_sent.html:6
#: templates/account/verified_email_required.html:5
#: templates/account/verified_email_required.html:6
msgid "Verify Email Address"
msgstr "驗證電子郵件地址"
#: templates/account/verification_sent.html:9
msgid ""
"We have sent an email to you for verification. Follow the link provided to "
"finalize the signup process. Please try to log in again if you do not "
"receive it within a few minutes."
msgstr ""
"我們已向您發送了一封驗證電子郵件。按照提供的連結完成註冊過程。如果您在幾分鐘內未收到,請再試一次登入。"
#: templates/account/verified_email_required.html:11
msgid ""
"This part of the site requires us to verify that\n"
"you are who you claim to be. For this purpose, we require that you\n"
"verify ownership of your email address. "
msgstr ""
"該網站的這一部分要求我們驗證\n"
"您是您聲稱的人。為此,我們要求您\n"
"驗證您的電子郵件地址的所有權。"
#: templates/account/verified_email_required.html:15
msgid ""
"We have sent an email to you for\n"
"verification. Please click on the link inside this email. Please\n"
"try again if you do not receive it within a few minutes."
msgstr ""
"我們已向您發送了一封驗證電子郵件。\n"
"請點選此電子郵件內的連結。請\n"
"如果您在幾分鐘內未收到,請再試一次。"
#: templates/account/verified_email_required.html:19
#, python-format
msgid ""
"<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your "
"email address</a>."
msgstr ""
"<strong>注意:</strong>您仍然可以<a href=\"%(email_url)s\">更改您的電子郵件地址</a>。"
#: templates/base.html:47
msgid "Services"
msgstr "服務"
#: templates/base.html:60
msgid "+ Create"
msgstr "+ 建立"
#: templates/base.html:69
msgid "Collaborations"
msgstr "合作"
#: templates/base.html:81
msgid "Account"
msgstr "帳戶"
#: templates/dashboard/includes/service_form.html:8
msgid "Advanced settings"
msgstr "進階設定"
#: templates/dashboard/includes/service_overview.html:15
#: templates/dashboard/pages/service_session_list.html:5
msgid "Sessions"
msgstr "工作階段"
#: templates/dashboard/includes/service_overview.html:22
#: templates/dashboard/includes/session_list.html:9
#: templates/dashboard/pages/service.html:38
#: templates/dashboard/pages/service.html:110
msgid "Hits"
msgstr "點選次數"
#: templates/dashboard/includes/service_overview.html:29
#: templates/dashboard/pages/service.html:60
msgid "Bounce Rate"
msgstr "跳出率"
#: templates/dashboard/includes/service_overview.html:40
msgid "Avg. Duration"
msgstr "平均持續時間"
#: templates/dashboard/includes/session_list.html:5
msgid "Session Start"
msgstr "工作階段開始"
#: templates/dashboard/includes/session_list.html:6
msgid "Identity"
msgstr "身份"
#: templates/dashboard/includes/session_list.html:7
#: templates/dashboard/pages/service.html:73
#: templates/dashboard/pages/service_session.html:81
msgid "Duration"
msgstr "持續時間"
#: templates/dashboard/includes/session_list.html:36
msgid "No data yet"
msgstr "尚無資料"
#: templates/dashboard/pages/dashboard.html:16
msgid "New Service"
msgstr "新服務"
#: templates/dashboard/pages/index.html:6
#: templates/dashboard/pages/service_session_list.html:9
msgid "Analytics"
msgstr "分析"
#: templates/dashboard/pages/index.html:8
msgid "Log In"
msgstr "登入"
#: templates/dashboard/pages/service.html:9
msgid "Manage"
msgstr "管理"
#: templates/dashboard/pages/service.html:17
msgid ""
"This service hasn't collected any data yet. To get started, place the "
"following code snippet at the end of the <code>&lt;body&gt;</code> tag on "
"any page you'd like to track."
msgstr ""
"此服務尚未收集任何資料。要開始,請將以下程式碼片段放在您想要追蹤的任何頁面的 <code>&lt;body&gt;</code> 標籤的末端。"
#: templates/dashboard/pages/service.html:29
#: templates/dashboard/pages/service.html:158
#: templates/dashboard/pages/service.html:192
#: templates/dashboard/pages/service.html:226
#: templates/dashboard/pages/service.html:260
#: templates/dashboard/pages/service.html:295
msgid "sessions"
msgstr "工作階段"
#: templates/dashboard/pages/service.html:47
msgid "Load Time"
msgstr "載入時間"
#: templates/dashboard/pages/service.html:86
msgid "Hits/Session"
msgstr "每次工作階段點選次數"
#: templates/dashboard/pages/service.html:109
#: templates/dashboard/pages/service_session.html:51
msgid "Location"
msgstr "位置"
#: templates/dashboard/pages/service.html:133
msgid "No data yet..."
msgstr "尚無資料..."
#: templates/dashboard/pages/service.html:141
msgid "Sessions by Geography"
msgstr "工作階段依地理位置"
#: templates/dashboard/pages/service.html:143
msgid "view table"
msgstr "檢視表格"
#: templates/dashboard/pages/service.html:153
#: templates/dashboard/pages/service_session.html:47
msgid "Country"
msgstr "國家"
#: templates/dashboard/pages/service.html:191
msgid "Referrer"
msgstr "參照來源"
#: templates/dashboard/pages/service.html:225
msgid "Operating System"
msgstr "作業系統"
#: templates/dashboard/pages/service.html:259
#: templates/dashboard/pages/service_session.html:27
msgid "Browser"
msgstr "瀏覽器"
#: templates/dashboard/pages/service.html:294
#: templates/dashboard/pages/service_session.html:35
msgid "Device Type"
msgstr "裝置類型"
#: templates/dashboard/pages/service.html:329
msgid "View more sessions"
msgstr "檢視更多工作階段"
#: templates/dashboard/pages/service_create.html:5
#: templates/dashboard/pages/service_create.html:8
msgid "Create Service"
msgstr "建立服務"
#: templates/dashboard/pages/service_create.html:16
msgid "Create"
msgstr "建立"
#: templates/dashboard/pages/service_create.html:17
#: templates/dashboard/pages/service_update.html:30
msgid "Cancel"
msgstr "取消"
#: templates/dashboard/pages/service_delete.html:5
#: templates/dashboard/pages/service_update.html:33
msgid "Delete"
msgstr "刪除"
#: templates/dashboard/pages/service_delete.html:12
msgid ""
"Are you sure you want to delete this service? All of its analytics and "
"associated data will be permanently deleted."
msgstr ""
"您確定要刪除此服務嗎?其所有分析和相關資料將被永久刪除。"
#: templates/dashboard/pages/service_session.html:31
msgid "Device"
msgstr "裝置"
#: templates/dashboard/pages/service_session.html:39
msgid "OS"
msgstr "作業系統"
#: templates/dashboard/pages/service_session.html:54
msgid "Open in Maps"
msgstr "在地圖中開啟"
#: templates/dashboard/pages/service_session.html:56
msgid "Unknown"
msgstr "未知"
#: templates/dashboard/pages/service_session.html:85
msgid "Load"
msgstr "載入"
#: templates/dashboard/pages/service_session.html:89
msgid "Tracker"
msgstr "追蹤器"
#: templates/dashboard/pages/service_update.html:5
msgid "Management"
msgstr "管理"
#: templates/dashboard/pages/service_update.html:8
msgid "View"
msgstr "檢視"
#: templates/dashboard/pages/service_update.html:13
msgid "Installation"
msgstr "安裝"
#: templates/dashboard/pages/service_update.html:15
msgid ""
"Place the following snippet at the end of the <code>&lt;body&gt;</code> tag "
"on any page you'd like to track."
msgstr ""
"將以下片段放在您想要追蹤的任何頁面的 <code>&lt;body&gt;</code> 標籤的末端。"
#: templates/dashboard/pages/service_update.html:21
msgid "Settings"
msgstr "設定"
#: templates/dashboard/pages/service_update.html:29
msgid "Save"
msgstr "儲存"

View File

@ -1,12 +1,11 @@
from urllib.parse import urlparse
from datetime import datetime, time
from datetime import time, datetime
from django.utils import timezone
class DateRangeMixin:
def get_start_date(self):
if self.request.GET.get("startDate") != None:
if self.request.GET.get("startDate") is not None:
found_time = timezone.datetime.strptime(
self.request.GET.get("startDate"), "%Y-%m-%d"
)
@ -15,7 +14,7 @@ class DateRangeMixin:
return timezone.now() - timezone.timedelta(days=30)
def get_end_date(self):
if self.request.GET.get("endDate") != None:
if self.request.GET.get("endDate") is not None:
found_time = timezone.datetime.strptime(
self.request.GET.get("endDate"), "%Y-%m-%d"
)
@ -23,8 +22,47 @@ class DateRangeMixin:
else:
return timezone.now()
def get_date_ranges(self):
now = timezone.now()
return [
{
"name": "Last 3 days",
"start": now - timezone.timedelta(days=2),
"end": now,
},
{
"name": "Last 30 days",
"start": now - timezone.timedelta(days=29),
"end": now,
},
{
"name": "Last 90 days",
"start": now - timezone.timedelta(days=89),
"end": now,
},
{
"name": "This month",
"start": now.replace(day=1),
"end": now,
},
{
"name": "Last month",
"start": (now.replace(day=1) - timezone.timedelta(days=1)).replace(
day=1
),
"end": now.replace(day=1) - timezone.timedelta(days=1),
},
{
"name": "This year",
"start": now.replace(day=1, month=1),
"end": now,
},
]
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data["start_date"] = self.get_start_date()
data["end_date"] = self.get_end_date()
data["date_ranges"] = self.get_date_ranges()
return data

View File

@ -14,3 +14,41 @@
.rf {
text-align: right !important;
}
.chart-card .apexcharts-svg {
border-radius: 0 0 var(--border-radius-lg, 0.5rem) var(--border-radius-lg, 0.5rem);
}
.max-w-0 {
max-width: 0;
}
.min-w-48 {
min-width: 48px;
}
.geo-table {
display: none;
}
.geo-card--use-table-view .geo-map {
display: none;
}
.geo-card--use-table-view .geo-table {
display: inline-block;
}
:root {
--color-neutral-000: white;
--color-neutral-50: #F8FAFC;
--color-neutral-100: #F1F5F9;
--color-neutral-200: #E2E8F0;
--color-neutral-300: #CBD5E1;
--color-neutral-400: #94A3B8;
--color-neutral-500: #64748B;
--color-neutral-600: #475569;
--color-neutral-700: #334155;
--color-neutral-800: #1E293B;
--color-neutral-900: #0F172A;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

16
shynet/dashboard/tasks.py Normal file
View File

@ -0,0 +1,16 @@
from celery import shared_task
from django.core import mail
from django.conf import settings
import html2text
@shared_task
def send_email(to: [str], subject: str, content: str, from_email: str = None):
text_content = html2text.html2text(content)
mail.send_mail(
subject,
text_content,
from_email or settings.DEFAULT_FROM_EMAIL,
to,
html_message=content,
)

View File

@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div>
<h4 class="heading">{% block page_title %}{% endblock %}</h4>
<div class="flex-1 truncate">
<h4 class="heading truncate">{% block page_title %}{% endblock %}</h4>
</div>
<hr class="sep">
{% block main %}

View File

@ -1,10 +1,10 @@
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hi there,
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=request.get_host %}Hi there,
You're receiving this email because {{ user_display }} has listed this email as a valid contact address for their account.
To confirm this is correct, go to {{ activate_url }}
{% endblocktrans %}
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you,
{% blocktrans with site_name=current_site.name site_domain=request.get_host %}Thank you,
{{ site_name }}
{% endblocktrans %}
{% endautoescape %}

Some files were not shown because too many files have changed in this diff Show More