Compare commits

..

116 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
77 changed files with 4509 additions and 1264 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

View File

@ -9,35 +9,50 @@ jobs:
publish_to_docker_hub: publish_to_docker_hub:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Prepare tags
id: prep
run: |
DOCKER_IMAGE=milesmcc/shynet
TAGS="${DOCKER_IMAGE}:edge"
echo ::set-output name=tags::${TAGS}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }} 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 - name: Build and push advanced image
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.prep.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

View File

@ -9,35 +9,50 @@ jobs:
publish_to_docker_hub: publish_to_docker_hub:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Create Docker Metadata
uses: actions/checkout@v2 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: Prepare tags - name: Checkout code
id: prep uses: actions/checkout@v4
run: |
DOCKER_IMAGE=milesmcc/shynet
TAGS="${DOCKER_IMAGE}:${{ github.event.inputs.tag }}"
echo ::set-output name=tags::${TAGS}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }} 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 - name: Build and push advanced image
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.prep.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

View File

@ -9,36 +9,52 @@ jobs:
publish_to_docker_hub: publish_to_docker_hub:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Prepare tags
id: prep
run: |
DOCKER_IMAGE=milesmcc/shynet
VERSION=${GITHUB_REF#refs/tags/}
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:latest"
echo ::set-output name=tags::${TAGS}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }} 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 - name: Build and push advanced image
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.prep.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

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

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'

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

@ -7,37 +7,37 @@ WORKDIR /usr/src/shynet
ARG GF_UID="500" ARG GF_UID="500"
ARG GF_GID="500" ARG GF_GID="500"
RUN apk update && \ RUN apk update && \
apk add gettext curl bash npm libffi-dev rust cargo 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, # libffi-dev and rust are used for the cryptography package,
# which we indirectly rely on. Necessary for aarch64 support. # which we indirectly rely on. Necessary for aarch64 support.
# Collect GeoIP Database # Collect GeoIP Database
RUN curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \ RUN apk add --no-cache curl && \
curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \ 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 && \ mv /tmp/GeoLite2*/*.mmdb /etc && \
apk del curl apk --purge del curl
# Move dependency files # Move dependency files
COPY poetry.lock pyproject.toml ./ COPY poetry.lock pyproject.toml ./
COPY package.json package-lock.json ../ COPY package.json package-lock.json ../
# Django expects node_modules to be in its parent directory. # Django expects node_modules to be in its parent directory.
# Install more dependencies # Install more dependencies and cleanup build dependencies afterwards
RUN apk add --no-cache postgresql-libs && \ RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev libressl-dev libffi-dev && \
apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev && \
npm i -P --prefix .. && \ npm i -P --prefix .. && \
pip install poetry==1.1.7 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
# Install Python dependencies # Setup user group
RUN poetry config virtualenvs.create false && \ RUN addgroup --system -g $GF_GID appgroup && \
poetry install --no-dev --no-interaction --no-ansi adduser appuser --system --uid $GF_UID -G appgroup && \
mkdir -p /var/local/shynet/db/ && \
# Cleanup dependencies & setup user group chown -R appuser:appgroup /var/local/shynet
RUN apk --purge del .build-deps && \
rm -rf /var/lib/apt/lists/* && \
rm /var/cache/apk/* && \
addgroup --system -g $GF_GID appgroup && \
adduser appuser --system --uid $GF_UID -G appgroup
# Install Shynet # Install Shynet
COPY shynet . COPY shynet .
@ -47,4 +47,5 @@ RUN python manage.py collectstatic --noinput && \
# Launch # Launch
USER appuser USER appuser
EXPOSE 8080 EXPOSE 8080
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" ] CMD [ "./entrypoint.sh" ]

View File

@ -7,7 +7,6 @@
- [Render](#render) - [Render](#render)
- [Updating Your Configuration](#updating-your-configuration) - [Updating Your Configuration](#updating-your-configuration)
- [Advanced Usage](#advanced-usage) - [Advanced Usage](#advanced-usage)
* [Installation with SSL](#installation-with-ssl)
* [Configuring a Reverse Proxy](#configuring-a-reverse-proxy) * [Configuring a Reverse Proxy](#configuring-a-reverse-proxy)
+ [Cloudflare](#cloudflare) + [Cloudflare](#cloudflare)
+ [Nginx](#nginx) + [Nginx](#nginx)
@ -21,9 +20,11 @@
**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.) **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
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. If you'd like to run Shynet over HTTPS without a reverse proxy, skip ahead to [Installation with SSL](#installation-with-ssl) instead. 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. > **These commands assume Ubuntu.** If you're installing Shynet on a different platform, the process will be different.
@ -33,9 +34,13 @@ Before continuing, please be sure to have the latest version of Docker installed
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/). 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, host, and port. (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. For database you can use either PostgreSQL or SQLite:
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. 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. 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.
@ -56,7 +61,7 @@ Before continuing, please be sure to have the latest version of Docker installed
1. Clone the repository. 1. Clone the repository.
2. Using [TEMPLATE.env](/TEMPLATE.env) as a template, confiure 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. 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>`. 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>`.
@ -96,40 +101,6 @@ See the [Render docs](https://render.com/docs/deploy-shynet) for more informatio
## Advanced Usage ## Advanced Usage
### Installation with SSL
If you are going to be running Shynet through a reverse proxy, please see [Configuring a Reverse Proxy](#configuring-a-reverse-proxy) instead.
0. We'll be cloning this into the home directory to make this installation easier, so run `cd ~/` if you need to.
1. Instead of pulling from Docker, we will be pulling from GitHub and building using Docker in order to easily add SSL certificates. You will want to run `git clone https://github.com/milesmcc/shynet.git` to clone the GitHub repo to your current working directory.
2. To install `certbot` follow [the guide here](https://certbot.eff.org/instructions) or follow along below
* Ubuntu 18.04
* `sudo apt-get update`
* `sudo apt-get install software-properties-common`
* `sudo add-apt-repository universe`
* `sudo add-apt-repository ppa:certbot/certbot`
* `sudo apt-get update`
* `sudo apt-get install certbot`
3. Run `sudo certbot certonly --standalone` and follow the instructions to generate your SSL certificate.
* If you registering the certificate to a domain name like `example.com`, please be sure to point your DNS records to your current server before running `certbot`.
4. We are going to move the SSL certificates to the Shynet repo with with command below. Replace `<domain>` with the domain name you used in step 3.
* `cp /etc/letsencrypt/live/<domain>/{cert,privkey}.pem ~/shynet/shynet/`
5. With that, we are going to replace the `webserver.sh` with `ssl.webserver.sh` to enable the use of SSL certificates. The original `webserver.sh` will be backed up to `backup.webserver.sh`
* `mv ~/shynet/shynet/webserver.sh ~/shynet/shynet/backup.webserver.sh`
* `mv ~/shynet/shynet/ssl.webserver.sh ~/shynet/shynet/webserver.sh`
6. Now we build the image!
* `docker image build shynet -t shynet-ssl:latest`
7. 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, host, and port (default is `5432`). (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)).
8. Follow the [Basic Installation](#basic-installation) guide with just one modification: in step #4, change the local bind port from `80` to `443`, and use `shynet-ssl:latest` as your Docker image instead of `milesmcc/shynet:latest`.
### Configuring a Reverse Proxy ### 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/)! 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/)!
@ -243,6 +214,22 @@ In a single-page application, the page never reloads. (That's the entire point o
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. 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 ## Troubleshooting

View File

@ -8,7 +8,7 @@
<br> <br>
<strong><a href="#installation">Getting started »</a></strong> <strong><a href="#installation">Getting started »</a></strong>
</p> </p>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#features">Features</a> &bull; <a href="https://github.com/milesmcc/a17t">Design</a></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> </p>
<br> <br>
@ -93,7 +93,7 @@ Shynet is pretty simple, but there are a few key terms you need to know in order
## Installation ## Installation
You can find intructions 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)). 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 ## FAQ

View File

@ -9,6 +9,10 @@ DB_PASSWORD=shynet_db_user_password
DB_HOST=db DB_HOST=db
DB_PORT=5432 DB_PORT=5432
# Database settings (SQLite) - comment PostgreSQL settings
# SQLITE=True
# DB_NAME=/var/local/shynet/db/db.sqlite3
# Email settings (optional) # Email settings (optional)
EMAIL_HOST_USER=example EMAIL_HOST_USER=example
EMAIL_HOST_PASSWORD=example_password EMAIL_HOST_PASSWORD=example_password
@ -19,11 +23,16 @@ EMAIL_USE_SSL=True
# EMAIL_USE_TLS=True # EMAIL_USE_TLS=True
SERVER_EMAIL=Shynet <noreply@shynet.example.com> SERVER_EMAIL=Shynet <noreply@shynet.example.com>
# General Django settings # General Django settings - to generate run: python3 -c "import secrets; print(secrets.token_urlsafe())"
DJANGO_SECRET_KEY=random_string DJANGO_SECRET_KEY=random_string
# For better security, set this to your deployment's domain. Comma separated. # Set these to your deployment's domain. Both are comma separated, but CSRF_TRUSTED_ORIGINS also requires a scheme (e.g., `https://`).
ALLOWED_HOSTS=* 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) # Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended)
ACCOUNT_SIGNUPS_ENABLED=False ACCOUNT_SIGNUPS_ENABLED=False

View File

@ -17,7 +17,7 @@ spec:
spec: spec:
containers: containers:
- name: "shynet-webserver" - name: "shynet-webserver"
image: "milesmcc/shynet:latest" image: "milesmcc/shynet:edge" # Change to the version appropriate for you (e.g., :latest)
imagePullPolicy: Always imagePullPolicy: Always
envFrom: envFrom:
- secretRef: - secretRef:
@ -42,7 +42,7 @@ spec:
spec: spec:
containers: containers:
- name: "shynet-celeryworker" - name: "shynet-celeryworker"
image: "milesmcc/shynet:latest" image: "milesmcc/shynet:edge" # Change to the version appropriate for you (e.g., :latest)
command: ["./celeryworker.sh"] command: ["./celeryworker.sh"]
imagePullPolicy: Always imagePullPolicy: Always
envFrom: envFrom:
@ -95,7 +95,7 @@ spec:
selector: selector:
app: shynet-webserver app: shynet-webserver
--- ---
apiVersion: networking.k8s.io/v1beta1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: shynet-webserver-ingress name: shynet-webserver-ingress

43
package-lock.json generated
View File

@ -4,6 +4,7 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "shynet",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1", "@fortawesome/fontawesome-free": "^5.15.1",
@ -160,8 +161,7 @@
"esprima": "^3.1.3", "esprima": "^3.1.3",
"estraverse": "^4.2.0", "estraverse": "^4.2.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"optionator": "^0.8.1", "optionator": "^0.8.1"
"source-map": "~0.6.1"
}, },
"bin": { "bin": {
"escodegen": "bin/escodegen.js", "escodegen": "bin/escodegen.js",
@ -325,9 +325,9 @@
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.4.1", "version": "1.4.1",
@ -367,9 +367,9 @@
} }
}, },
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
}, },
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.1.2", "version": "1.1.2",
@ -480,8 +480,7 @@
"esprima": "^4.0.1", "esprima": "^4.0.1",
"estraverse": "^4.2.0", "estraverse": "^4.2.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"optionator": "^0.8.1", "optionator": "^0.8.1"
"source-map": "~0.6.1"
}, },
"bin": { "bin": {
"escodegen": "bin/escodegen.js", "escodegen": "bin/escodegen.js",
@ -674,9 +673,9 @@
"integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==" "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow=="
}, },
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.3", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -957,9 +956,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"object-inspect": { "object-inspect": {
"version": "1.4.1", "version": "1.4.1",
@ -993,9 +992,9 @@
} }
}, },
"path-parse": { "path-parse": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
}, },
"prelude-ls": { "prelude-ls": {
"version": "1.1.2", "version": "1.1.2",
@ -1244,9 +1243,9 @@
"integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==" "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow=="
}, },
"word-wrap": { "word-wrap": {
"version": "1.2.3", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA=="
}, },
"wordwrap": { "wordwrap": {
"version": "0.0.3", "version": "0.0.3",

1997
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,23 @@
[tool.poetry] [tool.poetry]
name = "shynet" name = "shynet"
version = "0.10.0" version = "0.13.1"
description = "Modern, privacy-friendly, and cookie-free web analytics." description = "Modern, privacy-friendly, and cookie-free web analytics."
authors = ["R. Miles McCain <github@sendmiles.email>"] authors = ["R. Miles McCain <github@sendmiles.email>"]
license = "Apache-2.0" license = "Apache-2.0"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
Django = "^3.2.5" Django = "^4"
django-allauth = "^0.45.0" django-allauth = "^0.45.0"
geoip2 = "^4.2.0" geoip2 = "^4.2.0"
whitenoise = "^5.3.0" whitenoise = "^5.3.0"
celery = "^5.1.2" celery = "^5.2.2"
django-ipware = "^3.0.2" django-ipware = "^4.0.2"
PyYAML = "^5.4.1" PyYAML = "^5.4.1"
user-agents = "^2.2.0" user-agents = "^2.2.0"
rules = "^3.0" rules = "^3.0"
gunicorn = "^20.1.0" gunicorn = "^20.1.0"
psycopg2-binary = "^2.9.1" psycopg2-binary = "^2.9.2"
redis = "^3.5.3" redis = "^3.5.3"
django-redis-cache = "^3.0.0" django-redis-cache = "^3.0.0"
pycountry = "^20.7.3" pycountry = "^20.7.3"
@ -26,7 +26,19 @@ django-health-check = "^3.16.4"
django-npm = "^1.0.0" django-npm = "^1.0.0"
python-dotenv = "^0.18.0" python-dotenv = "^0.18.0"
django-debug-toolbar = "^3.2.1" 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] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" 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 +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"> <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"> <div class="w-full md:w-auto mb-2">
{% if page.has_previous %} {% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button field bg-neutral-000 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 %} {% else %}
<a class="button field bg-neutral-000 w-auto mr-1" disabled>Previous</a> <a class="button field !low bg-neutral-000 w-auto mr-1" disabled>{% trans 'Previous' %}</a>
{% endif %} {% endif %}
{% if page.has_next %} {% if page.has_next %}
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button field bg-neutral-000 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 %} {% else %}
<a class="button field bg-neutral-000 w-auto" disabled>Next</a> <a class="button field !low bg-neutral-000 w-auto" disabled>{% trans 'Next' %}</a>
{% endif %} {% endif %}
</div> </div>
<ul class="pagination-list w-full md:w-auto mb-2 flex"> <ul class="pagination-list w-full md:w-auto mb-2 flex">
{% for pnum in begin %} {% for pnum in begin %}
{% ifequal page.number pnum %} {% if page.number == pnum %}
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li> <li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endif %}
{% endfor %} {% endfor %}
{% if middle %} {% if middle %}
<li><span class="pagination-ellipsis">&hellip;</span></li> <li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in middle %} {% for pnum in middle %}
{% ifequal page.number pnum %} {% if page.number == pnum %}
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li> <li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if end %} {% if end %}
<li><span class="pagination-ellipsis">&hellip;</span></li> <li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in end %} {% for pnum in end %}
{% ifequal page.number pnum %} {% if page.number == pnum %}
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li> <li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</ul> </ul>

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

@ -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

@ -3,6 +3,7 @@ import uuid
from django.db import models from django.db import models
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import Service, ACTIVE_USER_TIMEDELTA from core.models import Service, ACTIVE_USER_TIMEDELTA
@ -13,43 +14,56 @@ def _default_uuid():
class Session(models.Model): class Session(models.Model):
uuid = models.UUIDField(default=_default_uuid, primary_key=True) 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 # 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 # Time
start_time = models.DateTimeField(default=timezone.now, db_index=True) start_time = models.DateTimeField(
last_seen = models.DateTimeField(default=timezone.now, db_index=True) 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 # Core request information
user_agent = models.TextField() user_agent = models.TextField(verbose_name=_("User agent"))
browser = models.TextField() browser = models.TextField(verbose_name=_("Browser"))
device = models.TextField() device = models.TextField(verbose_name=_("Device"))
device_type = models.CharField( device_type = models.CharField(
max_length=7, max_length=7,
choices=[ choices=[
("PHONE", "Phone"), ("PHONE", _("Phone")),
("TABLET", "Tablet"), ("TABLET", _("Tablet")),
("DESKTOP", "Desktop"), ("DESKTOP", _("Desktop")),
("ROBOT", "Robot"), ("ROBOT", _("Robot")),
("OTHER", "Other"), ("OTHER", _("Other")),
], ],
default="OTHER", default="OTHER",
verbose_name=_("Device type"),
) )
os = models.TextField() os = models.TextField(verbose_name=_("OS"))
ip = models.GenericIPAddressField(db_index=True, null=True) ip = models.GenericIPAddressField(db_index=True, null=True, verbose_name=_("IP"))
# GeoIP data # GeoIP data
asn = models.TextField(blank=True) asn = models.TextField(blank=True, verbose_name=_("Asn"))
country = models.TextField(blank=True) country = models.TextField(blank=True, verbose_name=_("Country"))
longitude = models.FloatField(null=True) longitude = models.FloatField(null=True, verbose_name=_("Longitude"))
latitude = models.FloatField(null=True) latitude = models.FloatField(null=True, verbose_name=_("Latitude"))
time_zone = models.TextField(blank=True) time_zone = models.TextField(blank=True, verbose_name=_("Time zone"))
is_bounce = models.BooleanField(default=True, db_index=True) is_bounce = models.BooleanField(
default=True, db_index=True, verbose_name=_("Is bounce")
)
class Meta: class Meta:
verbose_name = _("Session")
verbose_name_plural = _("Sessions")
ordering = ["-start_time"] ordering = ["-start_time"]
indexes = [ indexes = [
models.Index(fields=["service", "-start_time"]), models.Index(fields=["service", "-start_time"]),
@ -82,7 +96,9 @@ class Session(models.Model):
class Hit(models.Model): 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) initial = models.BooleanField(default=True, db_index=True)
# Base request information # Base request information
@ -103,6 +119,8 @@ class Hit(models.Model):
service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True) service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True)
class Meta: class Meta:
verbose_name = _("Hit")
verbose_name_plural = _("Hits")
ordering = ["-start_time"] ordering = ["-start_time"]
indexes = [ indexes = [
models.Index(fields=["session", "-start_time"]), models.Index(fields=["session", "-start_time"]),

View File

@ -61,7 +61,7 @@ def ingress_request(
log.debug(f"Linked to service {service}") log.debug(f"Linked to service {service}")
if dnt and service.respect_dnt: if dnt and service.respect_dnt:
log.debug("Ignoring because of DNT") log.debug("Ignoring because of DNT or GPC")
return return
try: try:

View File

@ -5,7 +5,13 @@
// //
// This script only sends the current URL, the referrer URL, and the page load time. That's it! // This script only sends the current URL, the referrer URL, and the page load time. That's it!
{% if dnt %}
var Shynet = { var Shynet = {
dnt: true
};
{% else %}
var Shynet = {
dnt: false,
idempotency: null, idempotency: null,
heartbeatTaskId: null, heartbeatTaskId: null,
skipHeartbeat: false, skipHeartbeat: false,
@ -53,6 +59,8 @@ var Shynet = {
}; };
window.addEventListener("load", Shynet.newPageLoad); window.addEventListener("load", Shynet.newPageLoad);
{% endif %}
{% if script_inject %} {% if script_inject %}
// The following is script is not part of Shynet, and was instead // The following is script is not part of Shynet, and was instead

View File

@ -15,7 +15,7 @@ from django.shortcuts import render, reverse
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt 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 ipware import get_client_ip
from core.models import Service from core.models import Service
@ -29,6 +29,9 @@ def ingress(request, service_uuid, identifier, tracker, payload):
location = request.META.get("HTTP_REFERER", "").strip() location = request.META.get("HTTP_REFERER", "").strip()
user_agent = request.META.get("HTTP_USER_AGENT", "").strip() user_agent = request.META.get("HTTP_USER_AGENT", "").strip()
dnt = request.META.get("HTTP_DNT", "0").strip() == "1" 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( ingress_request.delay(
service_uuid, service_uuid,
@ -116,7 +119,7 @@ class ScriptView(ValidateServiceOriginsMixin, View):
"service_uuid": self.kwargs.get("service_uuid"), "service_uuid": self.kwargs.get("service_uuid"),
}, },
) )
if self.kwargs.get("identifier") == None if self.kwargs.get("identifier") is None
else reverse( else reverse(
"ingress:endpoint_script_id", "ingress:endpoint_script_id",
kwargs={ kwargs={
@ -126,6 +129,9 @@ class ScriptView(ValidateServiceOriginsMixin, View):
) )
) )
heartbeat_frequency = settings.SCRIPT_HEARTBEAT_FREQUENCY 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( return render(
self.request, self.request,
"analytics/scripts/page.js", "analytics/scripts/page.js",
@ -135,6 +141,7 @@ class ScriptView(ValidateServiceOriginsMixin, View):
"protocol": protocol, "protocol": protocol,
"heartbeat_frequency": heartbeat_frequency, "heartbeat_frequency": heartbeat_frequency,
"script_inject": self.get_script_inject(), "script_inject": self.get_script_inject(),
"dnt": dnt and service.respect_dnt,
} }
), ),
content_type="application/javascript", content_type="application/javascript",
@ -156,7 +163,7 @@ class ScriptView(ValidateServiceOriginsMixin, View):
def get_script_inject(self): def get_script_inject(self):
service_uuid = self.kwargs.get("service_uuid") service_uuid = self.kwargs.get("service_uuid")
script_inject = cache.get(f"script_inject_{service_uuid}") script_inject = cache.get(f"script_inject_{service_uuid}")
if script_inject == None: if script_inject is None:
service = Service.objects.get(uuid=service_uuid) service = Service.objects.get(uuid=service_uuid)
script_inject = service.script_inject script_inject = service.script_inject
cache.set(f"script_inject_{service_uuid}", script_inject, timeout=3600) cache.set(f"script_inject_{service_uuid}", script_inject, timeout=3600)

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 import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import Service, User from .models import Service, User
class UserAdmin(BaseUserAdmin):
fieldsets = BaseUserAdmin.fieldsets + (
("Extra fields", {"fields": ("api_token",)}),
)
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)

View File

@ -1,20 +1,21 @@
from django.contrib.auth import get_user_model
import factory import factory
from django.contrib.auth import get_user_model
from factory import post_generation
from factory.django import DjangoModelFactory from factory.django import DjangoModelFactory
from .models import Service from .models import Service
class UserFactory(DjangoModelFactory): class UserFactory(DjangoModelFactory):
username = factory.Faker("user_name") username = factory.Faker("user_name")
email = factory.Faker("email") email = factory.Faker("email")
name = factory.Faker("name") first_name = factory.Faker("name")
@post_generation @post_generation
def password(self, create, extracted, **kwargs): def password(self, create, extracted, **kwargs):
password = ( password = (
extracted extracted
if extracted or factory.Faker(
else factory.Faker(
"password", "password",
length=42, length=42,
special_chars=True, special_chars=True,
@ -23,6 +24,7 @@ class UserFactory(DjangoModelFactory):
lower_case=True, lower_case=True,
).evaluate(None, None, extra={"locale": None}) ).evaluate(None, None, extra={"locale": None})
) )
self.set_password(password) self.set_password(password)
class Meta: class Meta:

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

@ -20,7 +20,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
email = options.get("email") email = options.get("email")
password = get_random_string() password = get_random_string(10)
User.objects.create_superuser(str(uuid.uuid4()), email=email, password=password) 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(self.style.SUCCESS("Successfully created a Shynet superuser"))
self.stdout.write(f"Email address: {email}") self.stdout.write(f"Email address: {email}")

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,8 +1,9 @@
import ipaddress import ipaddress
import json
import re import re
import uuid import uuid
from secrets import token_urlsafe
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -12,11 +13,13 @@ from django.db.models.functions import TruncDate, TruncHour
from django.db.utils import NotSupportedError from django.db.utils import NotSupportedError
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils import timezone 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) # 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( ACTIVE_USER_TIMEDELTA = timezone.timedelta(
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2 milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
) )
RESULTS_LIMIT = 300
def _default_uuid(): def _default_uuid():
@ -43,9 +46,14 @@ def _parse_network_list(networks: str):
return [ipaddress.ip_network(network.strip()) for network in networks.split(",")] return [ipaddress.ip_network(network.strip()) for network in networks.split(",")]
def _default_api_token():
return token_urlsafe(32)
class User(AbstractUser): class User(AbstractUser):
username = models.TextField(default=_default_uuid, unique=True) username = models.TextField(default=_default_uuid, unique=True)
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
api_token = models.TextField(default=_default_api_token, unique=True)
def __str__(self): def __str__(self):
return self.email return self.email
@ -54,34 +62,54 @@ class User(AbstractUser):
class Service(models.Model): class Service(models.Model):
ACTIVE = "AC" ACTIVE = "AC"
ARCHIVED = "AR" ARCHIVED = "AR"
SERVICE_STATUSES = [(ACTIVE, "Active"), (ARCHIVED, "Archived")] SERVICE_STATUSES = [(ACTIVE, _("Active")), (ARCHIVED, _("Archived"))]
uuid = models.UUIDField(default=_default_uuid, primary_key=True) 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( 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( 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) created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))
link = models.URLField(blank=True) link = models.URLField(blank=True, verbose_name=_("link"))
origins = models.TextField(default="*") origins = models.TextField(default="*", verbose_name=_("origins"))
status = models.CharField( 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) respect_dnt = models.BooleanField(default=True, verbose_name=_("Respect dnt"))
ignore_robots = models.BooleanField(default=False) ignore_robots = models.BooleanField(default=False, verbose_name=_("Ignore robots"))
collect_ips = models.BooleanField(default=True) collect_ips = models.BooleanField(default=True, verbose_name=_("Collect ips"))
ignored_ips = models.TextField( ignored_ips = models.TextField(
default="", blank=True, validators=[_validate_network_list] default="",
blank=True,
validators=[_validate_network_list],
verbose_name=_("Igored ips"),
) )
hide_referrer_regex = models.TextField( hide_referrer_regex = models.TextField(
default="", blank=True, validators=[_validate_regex] default="",
blank=True,
validators=[_validate_regex],
verbose_name=_("Hide referrer regex"),
)
script_inject = models.TextField(
default="", blank=True, verbose_name=_("Script inject")
) )
script_inject = models.TextField(default="", blank=True)
class Meta: class Meta:
verbose_name = _("Service")
verbose_name_plural = _("Services")
ordering = ["name", "uuid"] ordering = ["name", "uuid"]
def __str__(self): def __str__(self):
@ -149,7 +177,7 @@ class Service(models.Model):
locations = ( locations = (
hits.values("location") hits.values("location")
.annotate(count=models.Count("location")) .annotate(count=models.Count("location"))
.order_by("-count") .order_by("-count")[:RESULTS_LIMIT]
) )
referrer_ignore = self.get_ignored_referrer_regex() referrer_ignore = self.get_ignored_referrer_regex()
@ -159,7 +187,7 @@ class Service(models.Model):
hits.filter(initial=True) hits.filter(initial=True)
.values("referrer") .values("referrer")
.annotate(count=models.Count("referrer")) .annotate(count=models.Count("referrer"))
.order_by("-count") .order_by("-count")[:RESULTS_LIMIT]
) )
if not referrer_ignore.match(referrer["referrer"]) if not referrer_ignore.match(referrer["referrer"])
] ]
@ -167,29 +195,31 @@ class Service(models.Model):
countries = ( countries = (
sessions.values("country") sessions.values("country")
.annotate(count=models.Count("country")) .annotate(count=models.Count("country"))
.order_by("-count") .order_by("-count")[:RESULTS_LIMIT]
) )
operating_systems = ( 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 = ( browsers = (
sessions.values("browser") sessions.values("browser")
.annotate(count=models.Count("browser")) .annotate(count=models.Count("browser"))
.order_by("-count") .order_by("-count")[:RESULTS_LIMIT]
) )
device_types = ( device_types = (
sessions.values("device_type") sessions.values("device_type")
.annotate(count=models.Count("device_type")) .annotate(count=models.Count("device_type"))
.order_by("-count") .order_by("-count")[:RESULTS_LIMIT]
) )
devices = ( devices = (
sessions.values("device") sessions.values("device")
.annotate(count=models.Count("device")) .annotate(count=models.Count("device"))
.order_by("-count") .order_by("-count")[:RESULTS_LIMIT]
) )
avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[ avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
@ -203,6 +233,7 @@ class Service(models.Model):
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data( chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
sessions, hits, start_time, end_time, tz_now sessions, hits, start_time, end_time, tz_now
) )
return { return {
"currently_online": currently_online, "currently_online": currently_online,
"session_count": session_count, "session_count": session_count,
@ -256,7 +287,8 @@ class Service(models.Model):
.order_by("hour") .order_by("hour")
) )
chart_data = { chart_data = {
k["hour"]: {"sessions": k["count"]} for k in sessions_per_hour k["hour"]: {"sessions": k["count"], "hits": 0}
for k in sessions_per_hour
} }
hits_per_hour = ( hits_per_hour = (
hits.annotate(hour=TruncHour("start_time")) hits.annotate(hour=TruncHour("start_time"))
@ -284,7 +316,9 @@ class Service(models.Model):
.annotate(count=models.Count("uuid")) .annotate(count=models.Count("uuid"))
.order_by("date") .order_by("date")
) )
chart_data = {k["date"]: {"sessions": k["count"]} for k in sessions_per_day} chart_data = {
k["date"]: {"sessions": k["count"], "hits": 0} for k in sessions_per_day
}
hits_per_day = ( hits_per_day = (
hits.annotate(date=TruncDate("start_time")) hits.annotate(date=TruncDate("start_time"))
.values("date") .values("date")

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

@ -25,19 +25,25 @@ class ServiceForm(forms.ModelForm):
"name": forms.TextInput(), "name": forms.TextInput(),
"origins": forms.TextInput(), "origins": forms.TextInput(),
"ignored_ips": forms.TextInput(), "ignored_ips": forms.TextInput(),
"respect_dnt": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), "respect_dnt": forms.RadioSelect(
"collect_ips": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), choices=[(True, _("Yes")), (False, _("No"))]
"ignore_robots": 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(), "hide_referrer_regex": forms.TextInput(),
"script_inject": forms.Textarea(attrs={"class": "font-mono", "rows": 5}), "script_inject": forms.Textarea(attrs={"class": "font-mono", "rows": 5}),
} }
labels = { labels = {
"origins": "Allowed origins", "origins": _("Allowed origins"),
"respect_dnt": "Respect DNT", "respect_dnt": _("Respect DNT"),
"ignored_ips": "Ignored IP addresses", "ignored_ips": _("Ignored IP addresses"),
"ignore_robots": "Ignore robots", "ignore_robots": _("Ignore robots"),
"hide_referrer_regex": "Hide specific referrers", "hide_referrer_regex": _("Hide specific referrers"),
"script_inject": "Additional injected JS", "script_inject": _("Additional injected JS"),
} }
help_texts = { help_texts = {
"name": _("What should the service be called?"), "name": _("What should the service be called?"),
@ -45,18 +51,30 @@ class ServiceForm(forms.ModelForm):
"origins": _( "origins": _(
"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)." "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?", "respect_dnt": _(
"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').", "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?"
"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.", "ignored_ips": _(
"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.", "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."
),
} }
collect_ips = forms.BooleanField( collect_ips = forms.BooleanField(
help_text="IP address collection is disabled globally by your administrator." help_text=_("IP address collection is disabled globally by your administrator.")
if settings.BLOCK_ALL_IPS if settings.BLOCK_ALL_IPS
else "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected.", else _(
widget=forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), "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, initial=False if settings.BLOCK_ALL_IPS else True,
required=False, required=False,
disabled=settings.BLOCK_ALL_IPS, disabled=settings.BLOCK_ALL_IPS,
@ -68,7 +86,9 @@ class ServiceForm(forms.ModelForm):
return False if settings.BLOCK_ALL_IPS else collect_ips return False if settings.BLOCK_ALL_IPS else collect_ips
collaborators = forms.CharField( collaborators = forms.CharField(
help_text="Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)", help_text=_(
"Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)"
),
required=False, required=False,
) )

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

@ -47,8 +47,10 @@ class DateRangeMixin:
}, },
{ {
"name": "Last month", "name": "Last month",
"start": now.replace(day=1, month=now.month - 1), "start": (now.replace(day=1) - timezone.timedelta(days=1)).replace(
"end": now.replace(day=1, month=now.month) - timezone.timedelta(days=1), day=1
),
"end": now.replace(day=1) - timezone.timedelta(days=1),
}, },
{ {
"name": "This year", "name": "This year",

View File

@ -2,8 +2,8 @@
{% load i18n a17t_tags %} {% load i18n a17t_tags %}
{% block head_title %}{% trans "Change Password" %}{% endblock %} {% block head_title %}{% trans "Change authentication info" %}{% endblock %}
{% block page_title %}{% trans "Change Password" %}{% endblock %} {% block page_title %}{% trans "Change authentication info" %}{% endblock %}
{% block card %} {% block card %}
<form method="POST" action="{% url 'account_change_password' %}" class="password_change max-w-lg"> <form method="POST" action="{% url 'account_change_password' %}" class="password_change max-w-lg">
@ -11,4 +11,27 @@
{{ form|a17t }} {{ form|a17t }}
<button type="submit" name="action" class="button ~urge !high">{% trans "Change Password" %}</button> <button type="submit" name="action" class="button ~urge !high">{% trans "Change Password" %}</button>
</form> </form>
<hr class="sep">
<div>
<p class="label mb-1">Personal API token</p>
<div class="flex justify-between">
{% if request.user.api_token %}
<span class='chip ~info !normal'>{{request.user.api_token}}</span>
{% else %}
<span>Token not generated</span>
{% endif %}
<form method="POST" action="{% url 'dashboard:api_token_refresh' %}">
{% csrf_token %}
<button type="submit" name="action" class="button ~neutral @high">
{% if request.user.api_token %}
{% trans "Refresh token" %}
{% else %}
{% trans "Generate token" %}
{% endif %}
</button>
</form>
</div>
<p class="support mt-1">To learn more about the API, see our <a href="https://github.com/milesmcc/shynet/blob/master/GUIDE.md#api">API guide</a>.</p>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
{% load static rules helpers %} {% load static i18n rules helpers %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -15,7 +15,6 @@
<script src="{% static 'd3/d3.min.js' %}"></script> <script src="{% static 'd3/d3.min.js' %}"></script>
<script src="{% static 'topojson/build/topojson.min.js' %}"></script> <script src="{% static 'topojson/build/topojson.min.js' %}"></script>
<script src="{% static 'datamaps/dist/datamaps.world.min.js' %}"></script> <script src="{% static 'datamaps/dist/datamaps.world.min.js' %}"></script>
<script src="{% static 'dashboard/js/base.js' %}"></script>
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}"> <link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
<link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}"> <link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}">
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}"> <link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
@ -44,7 +43,9 @@
<div id="navMenuExpanded" <div id="navMenuExpanded"
class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full"> class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full">
{% if user.owning_services.all %} {% if user.owning_services.all %}
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Services</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">
{% trans 'Services' %}
</p>
{% for service in user.owning_services.all %} {% for service in user.owning_services.all %}
{% contextual_url 'dashboard:service' service.uuid as url %} {% contextual_url 'dashboard:service' service.uuid as url %}
@ -56,14 +57,17 @@
{% has_perm 'core.create_service' user as can_create %} {% has_perm 'core.create_service' user as can_create %}
{% if can_create %} {% if can_create %}
{% url 'dashboard:service_create' as url %} {% url 'dashboard:service_create' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="+ Create" url=url %} {% trans '+ Create' as create %}
{% include 'dashboard/includes/sidebar_portal.html' with label=create url=url %}
<hr class="sep h-8"> <hr class="sep h-8">
{% endif %} {% endif %}
{% if user.collaborating_services.all %} {% if user.collaborating_services.all %}
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">
{% trans 'Collaborations' %}
</p>
{% for service in user.collaborating_services.all %} {% for service in user.collaborating_services.all %}
{% contextual_url 'dashboard:service' service.uuid as url %} {% contextual_url 'dashboard:service' service.uuid as url %}
@ -73,7 +77,9 @@
<hr class="sep h-8"> <hr class="sep h-8">
{% endif %} {% endif %}
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Account</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">
{% trans 'Account' %}
</p>
{% if user.is_authenticated %} {% if user.is_authenticated %}

View File

@ -1,8 +1,10 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<form method="GET" id="datePicker"> <form method="GET" id="datePicker">
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate"> <input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate"> <input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
</form> </form>
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer" style="max-width: 200px;" readonly> <input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral !low bg-neutral-000 cursor-pointer" style="max-width: 200px;" readonly>
<style> <style>
:root { :root {
--litepicker-button-prev-month-color-hover: var(--color-urge); --litepicker-button-prev-month-color-hover: var(--color-urge);
@ -20,11 +22,21 @@
} }
</style> </style>
<script> <script>
var picker = new Litepicker({ function getLocaleDateString(locale) {
const formats = {
'de': "DD. MMM YY",
'zh_TW': "YYYY 年 MM 月 DD 日"
}
return formats[locale] || "MMM DD 'YY";
}
let locale = "{{ LANGUAGE_CODE }}";
let picker = new Litepicker({
element: document.getElementById('rangePicker'), element: document.getElementById('rangePicker'),
plugins: ['ranges'], plugins: ['ranges'],
singleMode: false, singleMode: false,
format: "MMM DD 'YY", lang: locale,
format: getLocaleDateString(locale),
maxDate: new Date(), maxDate: new Date(),
startDate: Date.parse(document.getElementById("startDate").getAttribute("value")), startDate: Date.parse(document.getElementById("startDate").getAttribute("value")),
endDate: Date.parse(document.getElementById("endDate").getAttribute("value")), endDate: Date.parse(document.getElementById("endDate").getAttribute("value")),

View File

@ -1,11 +1,11 @@
{% load a17t_tags %} {% load i18n a17t_tags %}
{{form.name|a17t}} {{form.name|a17t}}
{{form.link|a17t}} {{form.link|a17t}}
{{form.collaborators|a17t}} {{form.collaborators|a17t}}
<details {% if form.errors %}open{% endif %}> <details {% if form.errors %}open{% endif %}>
<summary class="cursor-pointer text-sm">Advanced settings</summary> <summary class="cursor-pointer text-sm">{% trans 'Advanced settings' %}</summary>
<hr class="sep h-4"> <hr class="sep h-4">
{{form.respect_dnt|a17t}} {{form.respect_dnt|a17t}}
{{form.collect_ips|a17t}} {{form.collect_ips|a17t}}

View File

@ -1,4 +1,4 @@
{% load humanize helpers %} {% load i18n humanize helpers %}
<a class="card chart-card overflow-visible ~neutral !low service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}"> <a class="card chart-card overflow-visible ~neutral !low service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}">
{% with stats=object.stats %} {% with stats=object.stats %}
@ -12,21 +12,21 @@
</div> </div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-3 lg:gap-6 md:flex-none"> <div class="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-3 lg:gap-6 md:flex-none">
<div> <div>
<p>Sessions</p> <p>{% trans 'Sessions' %}</p>
<p class="label"> <p class="label">
{{stats.session_count|intcomma}} {{stats.session_count|intcomma}}
{% compare stats.compare.session_count stats.session_count "UP" %} {% compare stats.compare.session_count stats.session_count "UP" %}
</p> </p>
</div> </div>
<div> <div>
<p>Hits</p> <p>{% trans 'Hits' %}</p>
<p class="label"> <p class="label">
{{stats.hit_count|intcomma}} {{stats.hit_count|intcomma}}
{% compare stats.compare.hit_count stats.hit_count "UP" %} {% compare stats.compare.hit_count stats.hit_count "UP" %}
</p> </p>
</div> </div>
<div> <div>
<p>Bounce Rate</p> <p>{% trans 'Bounce Rate' %}</p>
<p class="label"> <p class="label">
{% if stats.bounce_rate_pct != None %} {% if stats.bounce_rate_pct != None %}
{{stats.bounce_rate_pct|floatformat:"-1"}}% {{stats.bounce_rate_pct|floatformat:"-1"}}%
@ -37,7 +37,7 @@
</p> </p>
</div> </div>
<div> <div>
<p>Avg. Duration</p> <p>{% trans 'Avg. Duration' %}</p>
<p class="label"> <p class="label">
{% if stats.avg_session_duration != None %} {% if stats.avg_session_duration != None %}
{{stats.avg_session_duration|naturaldelta}} {{stats.avg_session_duration|naturaldelta}}

View File

@ -1,12 +1,12 @@
{% load humanize helpers %} {% load i18n humanize helpers %}
<table class="table"> <table class="table">
<thead> <thead>
<th>Session Start</th> <th>{% trans 'Session Start' %}</th>
<th>Identity</th> <th>{% trans 'Identity' %}</th>
<th>Network</th> <th>{% trans 'Network' %}</th>
<th class="rf">Duration</th> <th class="rf">{% trans 'Duration' %}</th>
<th class="rf">Hits</th> <th class="rf">{% trans 'Hits' %}</th>
</thead> </thead>
<tbody> <tbody>
{% for session in object_list %} {% for session in object_list %}
@ -14,7 +14,7 @@
<td> <td>
<a href="{% contextual_url 'dashboard:service_session' object.pk session.pk %}" <a href="{% contextual_url 'dashboard:service_session' object.pk session.pk %}"
class="font-medium text-urge-700"> class="font-medium text-urge-700">
{{session.start_time|date:"M j Y, g:i a"|capfirst}} {{ session.start_time|date:"DATETIME_FORMAT"|capfirst }}
{% if session.is_currently_active %} {% if session.is_currently_active %}
<span class="badge ~positive">Online</span> <span class="badge ~positive">Online</span>
{% endif %} {% endif %}
@ -33,7 +33,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td><span class="text-gray-600">No data yet...</span></td> <td><span class="text-gray-600">{% trans 'No data yet' %}...</span></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -1,6 +1,6 @@
{% load helpers %} {% load i18n helpers %}
<div> <div>
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %} flex items-center" title="{{label}}" <a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %} flex items-center" title="{% trans label %}"
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} <span class="truncate">{{label}}</span></a> {% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} <span class="truncate">{% trans label %}</span></a>
</div> </div>

View File

@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load rules pagination %} {% load i18n rules pagination %}
{% block content %} {% block content %}
<div class="md:flex justify-between items-center"> <div class="md:flex justify-between items-center">
@ -13,7 +13,7 @@
</div> </div>
{% has_perm "core.create_service" user as can_create %} {% has_perm "core.create_service" user as can_create %}
{% if can_create %} {% if can_create %}
<a href="{% url 'dashboard:service_create' %}" class="button field bg-neutral-000 w-auto">+ New Service</a> <a href="{% url 'dashboard:service_create' %}" class="button field !low bg-neutral-000 w-auto">+ {% trans 'New Service' %}</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -1,9 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %}
{% block content %} {% block content %}
<section class="content"> <section class="content">
<h2>{{request.site.name}} Analytics</h2> <h2>{{request.site.name}} {% trans 'Analytics' %}</h2>
<p>{{request.site.name}} uses Shynet. Eventually, more information about Shynet will be available here.</p> <p>{{request.site.name}} uses Shynet. Eventually, more information about Shynet will be available here.</p>
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a> <a href="{% url 'account_login' %}" class="button ~urge !high">{% trans 'Log In' %}</a>
</section> </section>
{% endblock %} {% endblock %}

View File

@ -1,12 +1,12 @@
{% extends "dashboard/service_base.html" %} {% extends "dashboard/service_base.html" %}
{% load humanize helpers rules %} {% load i18n humanize helpers rules %}
{% block service_actions %} {% block service_actions %}
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div> <div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
{% has_perm 'core.change_service' user object as can_update %} {% has_perm 'core.change_service' user object as can_update %}
{% if can_update %} {% if can_update %}
<a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage &rarr;</a> <a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field !low bg-neutral-000 w-auto">{% trans 'Manage' %} &rarr;</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@ -14,7 +14,11 @@
{% if not stats.has_hits %} {% if not stats.has_hits %}
<div class="content mb-6"> <div class="content mb-6">
<p> <p>
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. {% blocktrans trimmed %}
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.
{% endblocktrans %}
</p> </p>
{% include 'dashboard/includes/service_snippet.html' %} {% include 'dashboard/includes/service_snippet.html' %}
</div> </div>
@ -22,7 +26,7 @@
<div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral !high px-6" id="stats"> <div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral !high px-6" id="stats">
{% with classes="text-sm font-semibold" good_classes="text-positive-400" bad_classes="text-critical-400" neutral_classes="text-gray-400" %} {% with classes="text-sm font-semibold" good_classes="text-positive-400" bad_classes="text-critical-400" neutral_classes="text-gray-400" %}
<article class=""> <article class="">
<p class="label text-gray-400">Sessions</p> <p class="label text-gray-400">{% trans 'sessions' %}</p>
<p class="heading"> <p class="heading">
{{stats.session_count|intcomma}} {{stats.session_count|intcomma}}
<div> <div>
@ -31,7 +35,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Hits</p> <p class="label text-gray-400">{% trans 'Hits' %}</p>
<p class="heading"> <p class="heading">
{{stats.hit_count|intcomma}} {{stats.hit_count|intcomma}}
<div> <div>
@ -40,7 +44,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Load Time</p> <p class="label text-gray-400">{% trans 'Load Time' %}</p>
<p class="heading"> <p class="heading">
{% if stats.avg_load_time %} {% if stats.avg_load_time %}
{{stats.avg_load_time|floatformat:"0"}}ms {{stats.avg_load_time|floatformat:"0"}}ms
@ -53,7 +57,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Bounce Rate</p> <p class="label text-gray-400">{% trans 'Bounce Rate' %}</p>
<p class="heading"> <p class="heading">
{% if stats.bounce_rate_pct %} {% if stats.bounce_rate_pct %}
{{stats.bounce_rate_pct|floatformat:"-1"}}% {{stats.bounce_rate_pct|floatformat:"-1"}}%
@ -66,7 +70,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Duration</p> <p class="label text-gray-400">{% trans 'Duration' %}</p>
<p class="heading"> <p class="heading">
{% if stats.avg_session_duration %} {% if stats.avg_session_duration %}
{{stats.avg_session_duration|naturaldelta}} {{stats.avg_session_duration|naturaldelta}}
@ -79,7 +83,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Hits/Session</p> <p class="label text-gray-400">{% trans 'Hits/Session' %}</p>
<p class="heading"> <p class="heading">
{% if stats.avg_hits_per_session %} {% if stats.avg_hits_per_session %}
{{stats.avg_hits_per_session|floatformat:"-1"}} {{stats.avg_hits_per_session|floatformat:"-1"}}
@ -102,8 +106,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Location</th> <th>{% trans 'Location' %}</th>
<th class="rf">Hits</th> <th class="rf">{% trans 'Hits' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -126,17 +130,23 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td><span class="text-gray-600">No data yet...</span></td> <td><span class="text-gray-600">{% trans 'No data yet...' %}</span></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if stats.locations.count == RESULTS_LIMIT %}
<hr class="sep h-8 md:h-12">
<a href="{% contextual_url 'dashboard:service_location_list' service.uuid %}" class="button ~neutral w-auto mb-2">
{% trans 'View more locations' %} &rarr;
</a>
{% endif %}
</div> </div>
<div class="geo-map card ~neutral !low py-2 overflow-y-hidden"> <div class="geo-map card ~neutral !low py-2 overflow-y-hidden">
<p class="text-sm font-semibold p-2 border-b mb-2" style="color: var(--color-title)"> <p class="text-sm font-semibold p-2 border-b mb-2" style="color: var(--color-title)">
Sessions by Geography &nbsp {% trans 'Sessions by Geography' %} &nbsp
<button onclick="document.getElementById('card-grid').classList.add('geo-card--use-table-view')" class="text-xs select-none p-0 button ~urge !low"> <button onclick="document.getElementById('card-grid').classList.add('geo-card--use-table-view')" class="text-xs select-none p-0 button ~urge !low">
(view table) ({% trans 'view table' %})
</button> </button>
</p> </p>
{% include 'dashboard/includes/map_chart.html' with countries=stats.countries %} {% include 'dashboard/includes/map_chart.html' with countries=stats.countries %}
@ -146,12 +156,12 @@
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th> <th>
Country &nbsp {% trans 'Country' %} &nbsp
<button onclick="document.getElementById('card-grid').classList.remove('geo-card--use-table-view'); geoMap.resize()" class="text-xs select-none p-0 button ~urge !low"> <button onclick="document.getElementById('card-grid').classList.remove('geo-card--use-table-view'); geoMap.resize()" class="text-xs select-none p-0 button ~urge !low">
(view map) (view map)
</button> </button>
</th> </th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -184,8 +194,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Referrer</th> <th>{% trans 'Referrer' %}</th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -218,8 +228,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Operating System</th> <th>{% trans 'Operating System' %}</th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -252,8 +262,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Browser</th> <th>{% trans 'Browser' %}</th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -287,8 +297,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Device Type</th> <th>{% trans 'Device Type' %}</th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -321,8 +331,8 @@
<div class="card ~neutral !low py-2 overflow-auto"> <div class="card ~neutral !low py-2 overflow-auto">
{% include 'dashboard/includes/session_list.html' %} {% include 'dashboard/includes/session_list.html' %}
<hr class="sep h-8 md:h-12"> <hr class="sep h-8 md:h-12">
<a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more <a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">
sessions {% trans 'View more sessions' %} &rarr;
&rarr;</a> </a>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load a17t_tags %} {% load i18n a17t_tags %}
{% block head_title %}Create Service{% endblock %} {% block head_title %}{% trans 'Create Service' %}{% endblock %}
{% block content %} {% block content %}
<h4 class="heading leading-none">Create Service</h4> <h4 class="heading leading-none">{% trans 'Create Service' %}</h4>
<hr class="sep"> <hr class="sep">
<form class="card ~neutral !low p-0 max-w-xl" method="POST"> <form class="card ~neutral !low p-0 max-w-xl" method="POST">
{% csrf_token %} {% csrf_token %}
@ -13,8 +13,8 @@
{% include 'dashboard/includes/service_form.html' %} {% include 'dashboard/includes/service_form.html' %}
</div> </div>
<div class="section ~urge !normal p-4"> <div class="section ~urge !normal p-4">
<button type="submit" class="button ~urge !high">Create</button> <button type="submit" class="button ~urge !high">{% trans 'Create' %}</button>
<a href="{% url 'dashboard:dashboard' %}" class="button ~urge !low">Cancel</a> <a href="{% url 'dashboard:dashboard' %}" class="button ~urge !low">{% trans 'Cancel' %}</a>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,15 +1,19 @@
{% extends "dashboard/service_base.html" %} {% extends "dashboard/service_base.html" %}
{% load a17t_tags %} {% load i18n a17t_tags %}
{% block head_title %}Delete {{object.name}}{% endblock %} {% block head_title %}{% trans 'Delete' %} {{object.name}}{% endblock %}
{% block service_content %} {% block service_content %}
<form class="card ~neutral !low p-0 max-w-xl" method="POST"> <form class="card ~neutral !low p-0 max-w-xl" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="p-4"> <div class="p-4">
<p>Are you sure you want to delete this service? All of its <p>
analytics and associated data will be permanently deleted.</p> {% blocktrans trimmed %}
Are you sure you want to delete this service? All of its
analytics and associated data will be permanently deleted.
{% endblocktrans %}
</p>
{{form|a17t}} {{form|a17t}}
</div> </div>
<div class="section ~critical !normal p-4"> <div class="section ~critical !normal p-4">

View File

@ -0,0 +1,47 @@
{% extends "dashboard/service_base.html" %}
{% load i18n a17t_tags pagination humanize helpers %}
{% block head_title %}{{object.name}} {% trans 'Locations' %}{% endblock %}
{% block service_actions %}
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral !low bg-neutral-000 w-auto">{% trans 'Analytics' %} &rarr;</a>
{% endblock %}
{% block service_content %}
<div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-auto">
<table class="table">
<thead class="text-sm">
<tr>
<th>{% trans 'Location' %}</th>
<th class="rf">{% trans 'Hits' %}</th>
</tr>
</thead>
<tbody>
{% for location in object_list %}
<tr>
<td class="truncate w-full max-w-0 relative">
<div class="relative flex items-center">
{{location.location|default:"Unknown"|urldisplay}}
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{location.count|intcomma}}
<span class="text-xs rf min-w-48">
({{location.count|percent:hit_count}})
</span>
</div>
</td>
</tr>
{% empty %}
<tr>
<td><span class="text-gray-600">{% trans 'No data yet...' %}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% pagination page_obj request %}
{% endblock %}

View File

@ -1,11 +1,11 @@
{% extends "dashboard/service_base.html" %} {% extends "dashboard/service_base.html" %}
{% load a17t_tags pagination humanize helpers %} {% load i18n a17t_tags pagination humanize helpers %}
{% block head_title %}{{object.name}} Session{% endblock %} {% block head_title %}{{object.name}} Session{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics &rarr;</a> <a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field !low bg-neutral-000 w-auto">Analytics &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
@ -24,37 +24,36 @@
<hr class="sep h-8 md:h-12"> <hr class="sep h-8 md:h-12">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium">
<div> <div>
<p>Browser</p> <p>{% trans 'Browser' %}</p>
<p class="label">{{session.browser|default:"Unknown"}}</p> <p class="label">{{session.browser|default:"Unknown"}}</p>
</div> </div>
<div> <div>
<p>Device</p> <p>{% trans 'Device' %}</p>
<p class="label">{{session.device|default:"Unknown"}}</p> <p class="label">{{session.device|default:"Unknown"}}</p>
</div> </div>
<div> <div>
<p>Device Type</p> <p{% trans 'Device Type' %}</p>
<p class="label">{{session.device_type|title}}</p> <p class="label">{{session.device_type|title}}</p>
</div> </div>
<div> <div>
<p>OS</p> <p>{% trans 'OS' %}</p>
<p class="label">{{session.os|default:"Unknown"}}</p> <p class="label">{{session.os|default:"Unknown"}}</p>
</div> </div>
<div> <div>
<p>Network</p> <p>{% trans 'Network' %}</p>
<p class="label">{{session.asn|default:"Unknown"}}</p> <p class="label">{{session.asn|default:"Unknown"}}</p>
</div> </div>
<div> <div>
<p>Country</p> <p>{% trans 'Country' %}</p>
<p class="label"><span class="{{session.country|flag_class}}"></span>{{session.country|country_name}}</p> <p class="label"><span class="{{session.country|flag_class}}"></span>{{session.country|country_name}}</p>
</div> </div>
<div> <div>
<p>Location</p> <p>{% trans 'Location' %}</p>
<p class="label"> <p class="label">
{% if session.latitude %} {% if session.latitude %}
<a href="{{session|location_url}}" target="_blank">Open <a href="{{session|location_url}}" target="_blank">{% trans 'Open in Maps' %} &nearr;</a>
in Maps &nearr;</a>
{% else %} {% else %}
Unknown {% trans 'Unknown' %}
{% endif %} {% endif %}
</p> </p>
</div> </div>
@ -79,15 +78,15 @@
</div> </div>
<div class="grid grid-cols-3 gap-3 md:pl-8 md:w-1/2"> <div class="grid grid-cols-3 gap-3 md:pl-8 md:w-1/2">
<div> <div>
<p>Duration</p> <p>{% trans 'Duration' %}</p>
<p class="label">{{hit.duration|naturaldelta}}</p> <p class="label">{{hit.duration|naturaldelta}}</p>
</div> </div>
<div> <div>
<p>Load</p> <p>{% trans 'Load' %}</p>
<p class="label">{{hit.load_time|floatformat:"0"}}ms</p> <p class="label">{{hit.load_time|floatformat:"0"}}ms</p>
</div> </div>
<div> <div>
<p>Tracker</p> <p>{% trans 'Tracker' %}</p>
<p class="label">{{hit.tracker}}</p> <p class="label">{{hit.tracker}}</p>
</div> </div>
</div> </div>

View File

@ -1,12 +1,12 @@
{% extends "dashboard/service_base.html" %} {% extends "dashboard/service_base.html" %}
{% load a17t_tags pagination humanize helpers %} {% load i18n a17t_tags pagination humanize helpers %}
{% block head_title %}{{object.name}} Sessions{% endblock %} {% block head_title %}{{object.name}} {% trans 'Sessions' %}{% endblock %}
{% block service_actions %} {% block service_actions %}
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div> <div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral bg-neutral-000 w-auto">Analytics &rarr;</a> <a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral !low bg-neutral-000 w-auto">{% trans 'Analytics' %} &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
@ -14,4 +14,4 @@
{% include 'dashboard/includes/session_list.html' %} {% include 'dashboard/includes/session_list.html' %}
</div> </div>
{% pagination page_obj request %} {% pagination page_obj request %}
{% endblock %} {% endblock %}

View File

@ -1,20 +1,24 @@
{% extends "dashboard/service_base.html" %} {% extends "dashboard/service_base.html" %}
{% load a17t_tags %} {% load i18n a17t_tags %}
{% block head_title %}{{object.name}} Management{% endblock %} {% block head_title %}{{object.name}} {% trans 'Management' %}{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">View &rarr;</a> <a href="{% url 'dashboard:service' object.uuid %}" class="button field !low bg-neutral-000 w-auto">{% trans 'View' %} &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
<div class="max-w-xl content"> <div class="max-w-xl content">
<h5>Installation</h5> <h5>{% trans 'Installation' %}</h5>
<p>Place the following snippet at the end of the <code>&lt;body&gt;</code> tag on any page you'd like to track.</p> <p>
{% blocktrans trimmed %}
Place the following snippet at the end of the <code>&lt;body&gt;</code> tag on any page you'd like to track.
{% endblocktrans %}
</p>
{% include 'dashboard/includes/service_snippet.html' %} {% include 'dashboard/includes/service_snippet.html' %}
<hr class="sep h-4"> <hr class="sep h-4">
<h5>Settings</h5> <h5>{% trans 'Settings' %}</h5>
<form class="card ~neutral !low p-0" method="POST"> <form class="card ~neutral !low p-0" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="p-4"> <div class="p-4">
@ -22,13 +26,28 @@
</div> </div>
<div class="section ~neutral !normal p-4 flex justify-between"> <div class="section ~neutral !normal p-4 flex justify-between">
<div> <div>
<button type="submit" class="button ~neutral !high">Save</button> <button type="submit" class="button ~neutral !high">{% trans 'Save' %}</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral !low">Cancel</a> <a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral !low">{% trans 'Cancel' %}</a>
</div> </div>
<div> <div>
<a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical !high">Delete</a> <a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical !high">{% trans 'Delete' %}</a>
</div> </div>
</div> </div>
</form> </form>
<hr class="sep h-4">
<h5>API</h5>
<div class="card ~neutral !low content">
<p>Shynet provides a simple API that you can use to pull data programmatically. You can access this data via this URL:</p>
<code class="text-sm">{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}</code>
<p>
There are 2 optional query parameters:
<ul>
<li><code class="text-sm">startDate</code> &mdash; to set the start date (in format YYYY-MM-DD)</li>
<li><code class="text-sm">endDate</code> &mdash; to set the end date (in format YYYY-MM-DD)</li>
</ul>
</p>
<p>Example using cURL:</p>
<code class="text-sm">curl -H 'Authorization: Token (your API token, available on security page)' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01'</code>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -168,6 +168,7 @@ def iconify(text):
"firefox mobile": "firefox.com", "firefox mobile": "firefox.com",
"edge mobile": "microsoft.com", "edge mobile": "microsoft.com",
"chromium": "chromium.org", "chromium": "chromium.org",
"duckduckgo mobile": "duckduckgo.com",
} }
domain = None domain = None

View File

@ -1,6 +1,4 @@
from django.contrib import admin from django.urls import path
from django.urls import include, path
from django.views.generic import RedirectView
from . import views from . import views
@ -28,4 +26,14 @@ urlpatterns = [
views.ServiceSessionView.as_view(), views.ServiceSessionView.as_view(),
name="service_session", name="service_session",
), ),
path(
"service/<pk>/locations/",
views.ServiceLocationsListView.as_view(),
name="service_location_list",
),
path(
"api-token-refresh/",
views.RefreshApiTokenView.as_view(),
name="api_token_refresh",
),
] ]

View File

@ -2,21 +2,20 @@ from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Q from django.db.models import Q, Count
from django.shortcuts import get_object_or_404, reverse from django.shortcuts import get_object_or_404, reverse, redirect
from django.utils import timezone
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
DeleteView, DeleteView,
DetailView, DetailView,
ListView, ListView,
TemplateView,
UpdateView, UpdateView,
View,
) )
from rules.contrib.views import PermissionRequiredMixin from rules.contrib.views import PermissionRequiredMixin
from analytics.models import Session from analytics.models import Session, Hit
from core.models import Service from core.models import Service, _default_api_token, RESULTS_LIMIT
from .forms import ServiceForm from .forms import ServiceForm
from .mixins import DateRangeMixin from .mixins import DateRangeMixin
@ -68,6 +67,7 @@ class ServiceView(
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["script_protocol"] = "https://" if settings.SCRIPT_USE_HTTPS else "http://" data["script_protocol"] = "https://" if settings.SCRIPT_USE_HTTPS else "http://"
data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"]) data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"])
data["RESULTS_LIMIT"] = RESULTS_LIMIT
data["object_list"] = Session.objects.filter( data["object_list"] = Session.objects.filter(
service=self.get_object(), service=self.get_object(),
start_time__lt=self.get_end_date(), start_time__lt=self.get_end_date(),
@ -141,6 +141,36 @@ class ServiceSessionsListView(
return data return data
class ServiceLocationsListView(
LoginRequiredMixin, PermissionRequiredMixin, DateRangeMixin, ListView
):
model = Hit
template_name = "dashboard/pages/service_location_list.html"
paginate_by = RESULTS_LIMIT
permission_required = "core.view_service"
def get_object(self):
return get_object_or_404(Service, pk=self.kwargs.get("pk"))
def get_queryset(self):
hits = Hit.objects.filter(
service=self.get_object(),
start_time__lt=self.get_end_date(),
start_time__gt=self.get_start_date(),
)
self.hit_count = hits.count()
return (
hits.values("location").annotate(count=Count("location")).order_by("-count")
)
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data["object"] = self.get_object()
data["hit_count"] = self.hit_count
return data
class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
model = Session model = Session
template_name = "dashboard/pages/service_session.html" template_name = "dashboard/pages/service_session.html"
@ -155,3 +185,10 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk")) data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk"))
return data return data
class RefreshApiTokenView(LoginRequiredMixin, View):
def post(self, request):
request.user.api_token = _default_api_token()
request.user.save()
return redirect("account_change_password")

View File

@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
""" """
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
# import module sys to get the type of exception # import module sys to get the type of exception
@ -22,7 +23,7 @@ from django.contrib.messages import constants as messages
load_dotenv() load_dotenv()
# Increment on new releases # Increment on new releases
VERSION = "0.11.0" VERSION = "0.13.1"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -38,7 +39,7 @@ SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "onlyusethisindev")
DEBUG = os.getenv("DEBUG", "False") == "True" DEBUG = os.getenv("DEBUG", "False") == "True"
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",") ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",")
CSRF_TRUSTED_ORIGINS = filter(lambda k: len(k) > 0, os.getenv("CSRF_TRUSTED_ORIGINS", "").split(","))
# Application definition # Application definition
@ -59,16 +60,19 @@ INSTALLED_APPS = [
"core", "core",
"dashboard.apps.DashboardConfig", "dashboard.apps.DashboardConfig",
"analytics", "analytics",
"api",
"allauth", "allauth",
"allauth.account", "allauth.account",
"allauth.socialaccount", "allauth.socialaccount",
"debug_toolbar", "debug_toolbar",
"corsheaders",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
@ -100,13 +104,13 @@ WSGI_APPLICATION = "shynet.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
if os.getenv("SQLITE", "False") == "True": if os.getenv("SQLITE", "False") == "True":
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"), "NAME": os.environ.get("DB_NAME", "/var/local/shynet/db/db.sqlite3"),
} }
} }
else: else:
@ -201,7 +205,7 @@ LOGGING = {
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/ # https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = os.getenv("LANGUAGE_CODE", "en-us")
TIME_ZONE = os.getenv("TIME_ZONE", "America/New_York") TIME_ZONE = os.getenv("TIME_ZONE", "America/New_York")
@ -370,3 +374,20 @@ DASHBOARD_PAGE_SIZE = int(os.getenv("DASHBOARD_PAGE_SIZE", "5"))
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = ( USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = (
os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True" os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True"
) )
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_METHODS = ["GET", "OPTIONS"]
# IPWare Precedence Options
IPWARE_META_PRECEDENCE_ORDER = (
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR', 'X_FORWARDED_FOR', # client, proxy1, proxy2
'HTTP_CLIENT_IP',
'HTTP_X_REAL_IP',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'HTTP_VIA',
'REMOTE_ADDR',
)

View File

@ -25,4 +25,5 @@ urlpatterns = [
path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")), path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")),
path("healthz/", include("health_check.urls")), path("healthz/", include("health_check.urls")),
path("", include(("core.urls", "core"), namespace="core")), path("", include(("core.urls", "core"), namespace="core")),
path("api/v1/", include(("api.urls", "api"), namespace="api")),
] ]

16
tests/js.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>JS test</title>
</head>
<body>
<noscript>
<img src="http://localhost:8000/ingress/0ca733e8-c41f-462b-a11a-4ba0cea29948/pixel.gif">
</noscript>
<script defer src="http://localhost:8000/ingress/0ca733e8-c41f-462b-a11a-4ba0cea29948/script.js"></script>
</body>
</html>

13
tests/pixel.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Pixel test</title>
</head>
<body>
<img src="http://localhost:8000/ingress/9b2c4e2f-8d29-4418-82d4-b68e06795025/pixel.gif">
</body>
</html>