Compare commits

..

45 Commits

Author SHA1 Message Date
R. Miles McCain
6652acdf14 Bump version 2020-08-11 21:56:59 +00:00
R. Miles McCain
1dfbec06e1 Split testing options 2020-08-11 21:56:26 +00:00
R. Miles McCain
3e315f06ed Enforce origin checking on pixel trackers (indirectly fixes #65) 2020-08-11 21:56:20 +00:00
R. Miles McCain
2d42674e1a Add warning when hostname starts with http (fixes #68) 2020-08-11 21:39:08 +00:00
R. Miles McCain
e4deab2072 Fix file path creation (fixes #69) 2020-08-11 21:34:39 +00:00
R. Miles McCain
c5ed5ef0e7 Merge branch 'MagnumDingusEdu/master' into dev 2020-08-11 21:32:03 +00:00
R. Miles McCain
7268a4ea84 Improve GUIDE language 2020-08-11 21:31:39 +00:00
Vividh Mariy
2cbc5ac441 Added deployment using docker-compose. Fixed #70 2020-08-10 00:00:52 +05:30
R. Miles McCain
058601d669 Fix button styling on session page (fixes #63) 2020-07-31 16:32:15 +00:00
Jake Malachowski
213c44a45a Add Render as a deployment option (#62)
* Add Render deployment option

Add Render as deployment option

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

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

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

Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2020-06-28 15:11:48 -04:00
R. Miles McCain
510df192d8 Add script injection option 2020-06-28 18:59:05 +00:00
R. Miles McCain
2e7620f1eb Bump version 2020-06-28 17:56:29 +00:00
R. Miles McCain
93d4ee5241 Use specific version of pipenv in dockerfile 2020-06-28 17:56:09 +00:00
R. Miles McCain
1a7594be93 Use SHA256 for more secure session association 2020-06-28 17:55:59 +00:00
R. Miles McCain
f464a7ee67 Add automatic release drafter 2020-06-28 17:47:58 +00:00
R. Miles McCain
a1cd3d4609 Code cleanup 2020-06-28 17:36:20 +00:00
R. Miles McCain
358fb234a7 Fix multiple origin support (closes #52) 2020-06-28 17:36:12 +00:00
R. Miles McCain
94fed58de3 Fix charts 2020-06-28 17:18:58 +00:00
R. Miles McCain
49f452d9f2 Lock Python package versions 2020-06-28 17:11:08 +00:00
R. Miles McCain
40d07fe159 Add IE11 support 2020-06-28 04:10:06 +00:00
R. Miles McCain
e150e6bede Bump version 2020-06-28 04:01:50 +00:00
R. Miles McCain
87a411f42d Make ingress processing more resilient 2020-06-28 03:48:40 +00:00
R. Miles McCain
88f25b6743 Analytics script cleanup (closes #54) 2020-06-28 03:31:10 +00:00
R. Miles McCain
bb0dc2e90f Remove all external dependencies 2020-06-28 02:58:49 +00:00
R. Miles McCain
4a8939796e Arbitrarily update the pixel test 2020-06-28 02:45:46 +00:00
R. Miles McCain
ba795ccd5c Clarify collaborators description 2020-06-28 02:44:59 +00:00
Alexandre Bulté
c9b5a677d3 SIGNUPS_ENABLED -> ACCOUNT_SIGNUPS_ENABLED in docs (#51) 2020-06-18 15:15:10 -04:00
Alexandre Bulté
affcb893fa Add timeout to curl / geoip (#50)
W/o timeout, if the download is unresponsive it can be a pain on some environments (eg `dokku`) because the build hangs forever.
2020-06-18 14:00:06 -04:00
R. Miles McCain
e030807acb Fix GeoIP2 license keys 2020-06-18 16:46:39 +00:00
49 changed files with 561 additions and 166 deletions

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

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

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

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

3
.gitignore vendored
View File

@@ -3,6 +3,9 @@ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# JavaScript packages
node_modules/
# C extensions # C extensions
*.so *.so

View File

@@ -3,20 +3,22 @@ FROM python:3-alpine
# Getting things ready # Getting things ready
WORKDIR /usr/src/shynet WORKDIR /usr/src/shynet
COPY Pipfile.lock Pipfile ./ COPY Pipfile.lock Pipfile ./
COPY package.json package-lock.json ../
# Django expects node_modules to be in its parent directory.
# Install dependencies & configure machine # Install dependencies & configure machine
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 && \ apk add gettext curl bash npm && \
# URL from https://github.com/shlinkio/shlink/issues/596 :) curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \
curl "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz" | tar -xvz -C /tmp && \ 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 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz" | tar -xvz -C /tmp && \
mv /tmp/GeoLite2*/*.mmdb /etc && \ mv /tmp/GeoLite2*/*.mmdb /etc && \
apk del curl && \ apk del curl && \
apk add --no-cache postgresql-libs && \ apk add --no-cache postgresql-libs && \
apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev && \ apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev && \
pip install pipenv && \ npm i -P --prefix .. && \
pip install pipenv~=2020.6.2 && \
pipenv install --system --deploy && \ pipenv install --system --deploy && \
apk --purge del .build-deps && \ apk --purge del .build-deps && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \

View File

@@ -4,21 +4,26 @@
- [Installation](#installation) - [Installation](#installation)
- [Heroku](#heroku) - [Heroku](#heroku)
- [Render](#render)
- [Updating Your Configuration](#updating-your-configuration) - [Updating Your Configuration](#updating-your-configuration)
- [Enhancements](#enhancements) - [Advanced Usage](#advanced-usage)
* [Installation with SSL](#installation-with-ssl) * [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)
* [Health Checks](#health-checks)
* [Primary Key Integration](#primary-key-integration)
* [Usage with Single-Page Applications](#usage-with-single-page-applications)
+ [Troubleshooting](#troubleshooting) + [Troubleshooting](#troubleshooting)
--- ---
## Staying Updated ## Staying Updated
**If you install Shynet, you should strongly consider enabling notifications when new versions are released.** You can do this under the "Watch" tab on GitHub (above). This will ensure that you are notified when new versions are available, some of which may be security updates. (Shynet will never automatically update itself.) **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.)
## Installation ## Installation
Installation of Shynet is easy! Follow the [Basic Installation](#basic-installation) guide below if you'd like to run Shynet over HTTP or if you are going to be running it 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. If you'd like to run Shynet over HTTPS without a reverse proxy, skip ahead to [Installation with SSL](#installation-with-ssl) instead.
> **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.
@@ -36,7 +41,7 @@ Before continuing, please be sure to have the latest version of Docker installed
5. Create an admin user by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py registeradmin <your email>`. A temporary password will be printed to the console. 5. Create an admin user by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py registeradmin <your email>`. A temporary password will be printed to the console.
6. Set the hostname of your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py hostname <your public hostname>`, where `<your public hostname>` is the _publicly accessible hostname_ of your instance, including port. This setting affects the URL that the tracking script sends its results to, so make sure it's correct. (Example hostnames: `shynet.rmrm.io` or `example.com:8000`.) 6. Set the hostname of your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py hostname <your public hostname>`, where `<your public hostname>` is the _publicly accessible hostname_ of your instance, including port. This setting affects the URL that the tracking script sends its results to, so make sure it's correct. (Example hostnames: `shynet.example.com` or `example.com:8000`.)
7. Set the whitelabel of your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: `"My Shynet Instance"` or `"Acme Analytics"`.) 7. Set the whitelabel of your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: `"My Shynet Instance"` or `"Acme Analytics"`.)
@@ -46,6 +51,27 @@ Before continuing, please be sure to have the latest version of Docker installed
10. Finally, click on "Manage" in the top right of the service's page to get the tracking script code. Inject this script on all pages you'd like the service to track. 10. Finally, click on "Manage" in the top right of the service's page to get the tracking script code. Inject this script on all pages you'd like the service to track.
### Basic Installation with Docker Compose
> Make sure you have `docker-compose` installed. If not, [install it](https://docs.docker.com/compose/install/)
1. Clone the repository.
2. Using [TEMPLATE.env](/TEMPLATE.env) as a template, 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.
3. On line 2 of the `nginx.conf` file located in the root of the repository, replace `example.com` with your hostname. Then, in the `docker-compose.yml` file, set the port number by replacing `8080` in line 38 ( `- 8080:80` ) with whatever local port you want to bind it to. For example, set the port number to `- 80:80` if you want your site will be available via HTTP (port 80) at `http://<your hostname>`.
4. Launch the Shynet server for the first time by running `docker-compose up -d`. If you get an error like "permission denied" or "Couldn't connect to Docker daemon", either prefix the command with `sudo` or add your user to the `docker` group.
5. Create an admin user by running `docker exec -it shynet_main ./manage.py registeradmin <your email>`. A temporary password will be printed to the console.
6. Set the hostname of your Shynet instance by running `docker exec -it shynet_main ./manage.py hostname <your public hostname>`, where `<your public hostname>` is the same as the hostname you set in step 3. This setting affects the URL that the tracking script sends its results to, so make sure it's correct. (Example hostnames: shynet.example.com or example.com:8000.)
7. Set the whitelabel of your Shynet instance by running `docker exec -it shynet_main ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: "My Shynet Instance" or "Acme Analytics".)
Your site should now be accessible at `http://hostname:port`. Now you can follow steps 9-10 of the [Basic Installation](#basic-installation) guide above to get Shynet integrated on your sites.
## Heroku ## Heroku
You may wish to deploy Shynet on Heroku. Note that Heroku's free offerings (namely the free Postgres addon) are unlikely to support running any Shynet instance that records more than a few hundred requests per day &mdash; the database will quickly fill up. In most cases, the more cost-effective option for running Shynet is renting a VPS from a full cloud service provider. However, if you're sure Heroku is the right option for you, or you just want to try Shynet out, you can use the Quick Deploy button then follow the steps below. You may wish to deploy Shynet on Heroku. Note that Heroku's free offerings (namely the free Postgres addon) are unlikely to support running any Shynet instance that records more than a few hundred requests per day &mdash; the database will quickly fill up. In most cases, the more cost-effective option for running Shynet is renting a VPS from a full cloud service provider. However, if you're sure Heroku is the right option for you, or you just want to try Shynet out, you can use the Quick Deploy button then follow the steps below.
@@ -58,9 +84,23 @@ Once you deploy, you'll need to setup an admin user, whitelabel, and hostname be
2. `heroku run --app=<your app> ./manage.py hostname <the hostname where you will run Shynet>` 2. `heroku run --app=<your app> ./manage.py hostname <the hostname where you will run Shynet>`
3. `heroku run --app=<your app> ./manage.py whitelabel "<your Shynet instance's name>"` 3. `heroku run --app=<your app> ./manage.py whitelabel "<your Shynet instance's name>"`
## Render
[Render](https://render.com) is a modern cloud platform to build and run all your apps and websites with free SSL, a global CDN, private networks and auto deploys from Git. To deploy Shynet, click the `Deploy to Render` button and follow the steps below.
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/render-examples/shynet)
Once your deploy has completed, use the **Render Shell** to configure your app:
1. Set your email: `./manage.py registeradmin your-email@example.com`
1. Add your onrender.com domain: `./manage.py hostname your-shynet-domain.onrender.com`
1. Set your whitelabel: `./manage.py whitelabel "Your Shynet Instance Name"`
See the [Render docs](https://render.com/docs/deploy-shynet) for more information on deploying your application on Render.
--- ---
## Enhancements ## Advanced Usage
### Installation with SSL ### Installation with SSL
@@ -175,6 +215,40 @@ Nginx is a self hosted, highly configurable webserver. Nginx can be configured t
* [How to add SSL/HTTPS to Nginx (Ubuntu 16.04)](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04) * [How to add SSL/HTTPS to Nginx (Ubuntu 16.04)](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04)
* [Nginx Documentation](https://nginx.org/en/docs/) * [Nginx Documentation](https://nginx.org/en/docs/)
### Health Checks
By default, Shynet includes a default health check endpoint at `/healthz/`. If the instance is running normally, this endpoint will return an HTTP status code of 200; if something is wrong, it will have a non-200 status code. To view the health data as JSON, send your request to `/healthz/?format=json`.
This feature is helpful when running Shynet with Kubernetes, as it allows you to setup [startup readiness probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) that prevent traffic from being sent to your Shynet instances before they are ready.
### Primary-Key Integration
In some cases, it is useful to associate particular users on your platform with their sessions in Shynet. In Shynet, this is called _primary key integration_, and is done by adding an additional element to the Shynet script url for each particular user.
If the Shynet script location (for either the pixel or the script) is, for example, `//shynet.example.com/ingress/your_service_uuid/pixel.gif` and `//shynet.example.com/ingress/your_service_uuid/script.js`, the URLs for primary-key enabled users would be `//shynet.example.com/ingress/your_service_uuid/USER_PRIMARY_KEY/pixel.gif` and `//shynet.example.com/ingress/your_service_uuid/USER_PRIMARY_KEY/script.js`.
Adding this path can be done easily using server-side rendering. For example, here is a Django template that adds users' primary keys to the Shynet tracking script:
```html
{% if request.user.is_authenticated %}
<noscript>
<img src="//shynet.example.com/ingress/service-uuid/{{request.user.email|urlencode:""}}/pixel.gif">
</noscript>
<script src="//shynet.example.com/ingress/service-uuid/{{request.user.email|urlencode:""}}/script.js"></script>
{% else %}
<noscript>
<img src="//shynet.example.com/ingress/service-uuid/pixel.gif">
</noscript>
<script src="//shynet.example.com/ingress/service-uuid/script.js"></script>
{% endif %}
```
### Usage with Single-Page Applications
In a single-page application, the page never reloads. (That's the entire point of single-page applications, after all!) Unfortunately, this also means that Shynet will not automatically recognize and track when the user navigates between pages _within_ your application.
Fortunately, Shynet offers a simple method you can call from anywhere within your JavaScript to indicate that a new page has been loaded: `Shynet.newPageLoad()`. Add this method call to the code that handles routing in your app, and you'll be ready to go.
--- ---
## Troubleshooting ## Troubleshooting

48
Pipfile
View File

@@ -3,29 +3,29 @@ name = "pypi"
url = "https://pypi.org/simple" url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
[dev-packages]
black = "*"
[packages]
django = "*"
django-allauth = "*"
geoip2 = "*"
whitenoise = "*"
celery = "*"
django-ipware = "*"
pyyaml = "*"
ua-parser = "*"
user-agents = "*"
emoji-country-flag = "*"
rules = "*"
gunicorn = "*"
psycopg2-binary = "*"
redis = "*"
django-redis-cache = "*"
pycountry = "*"
ipaddress = "*"
html2text = "*"
django-health-check = "*"
[pipenv] [pipenv]
allow_prereleases = true allow_prereleases = true
[packages]
django = "~=3.0"
django-allauth = "~=0.42.0"
geoip2 = "~=3.0.0"
whitenoise = "~=5.1.0"
celery = "~=4.4.6"
django-ipware = "~=2.1.0"
pyyaml = "~=5.3.1"
ua-parser = "~=0.10.0"
user-agents = "~=2.1"
emoji-country-flag = "~=1.2.1"
rules = "~=2.2"
gunicorn = "~=20.0.4"
psycopg2-binary = "~=2.8.5"
redis = "~=3.5.3"
django-redis-cache = "~=2.1.1"
pycountry = "~=19.8.18"
html2text = "~=2020.1.16"
django-health-check = "~=3.12.1"
django-npm = "~=1.0.0"
[dev-packages]
black = "*"

65
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "327b897f359bad486c08fc88fb70a1f9d2edaf1aadafcb1d31e5b3e144125ff7" "sha256": "c18d6dc7c78d5f0634e38bb81bc1cf2cd4a0c128d70ca667fe765a66b294e66e"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@@ -23,10 +23,10 @@
}, },
"asgiref": { "asgiref": {
"hashes": [ "hashes": [
"sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5", "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
"sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c" "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
], ],
"version": "==3.2.7" "version": "==3.2.10"
}, },
"billiard": { "billiard": {
"hashes": [ "hashes": [
@@ -37,18 +37,18 @@
}, },
"celery": { "celery": {
"hashes": [ "hashes": [
"sha256:c3f4173f83ceb5a5c986c5fdaefb9456de3b0729a72a5776e46bd405fda7b647", "sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916",
"sha256:d1762d6065522879f341c3d67c2b9fe4615eb79756d59acb1434601d4aca474b" "sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.4.5" "version": "==4.4.6"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
], ],
"version": "==2020.4.5.2" "version": "==2020.6.20"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@@ -66,11 +66,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:db4c9b29615d17f808f2b1914d5cd73cd457c9fd90581195172c0888c210d944", "sha256:045be31d68dfed684831e39ab1d9e77a595f1a393935cb43b6c5451d2e78c8a4",
"sha256:dd96f98ec1c3e60877d45cea7350215f16de409848d23cced8443db1b188bd9b" "sha256:ccf6c208424c0e1b0eaffd36efe12618a9ab4d0037e26f6ffceaa5277af985d7"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.1a1" "version": "==3.1b1"
}, },
"django-allauth": { "django-allauth": {
"hashes": [ "hashes": [
@@ -94,6 +94,13 @@
"index": "pypi", "index": "pypi",
"version": "==2.1.0" "version": "==2.1.0"
}, },
"django-npm": {
"hashes": [
"sha256:2e6bba65e728fa18b9db3c8dc0d4490b70cb7f43bacf60eb3654d7dcb6424272"
],
"index": "pypi",
"version": "==1.0.0"
},
"django-redis-cache": { "django-redis-cache": {
"hashes": [ "hashes": [
"sha256:06d4e48545243883f88dc9263dda6c8a0012cb7d0cee2d8758d8917eca92cece", "sha256:06d4e48545243883f88dc9263dda6c8a0012cb7d0cee2d8758d8917eca92cece",
@@ -142,25 +149,17 @@
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
], ],
"version": "==2.9" "version": "==2.10"
},
"ipaddress": {
"hashes": [
"sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc",
"sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2"
],
"index": "pypi",
"version": "==1.0.23"
}, },
"kombu": { "kombu": {
"hashes": [ "hashes": [
"sha256:437b9cdea193cc2ed0b8044c85fd0f126bb3615ca2f4d4a35b39de7cacfa3c1a", "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
"sha256:dc282bb277197d723bccda1a9ba30a27a28c9672d0ab93e9e51bb05a37bd29c3" "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
], ],
"version": "==4.6.10" "version": "==4.6.11"
}, },
"maxminddb": { "maxminddb": {
"hashes": [ "hashes": [
@@ -220,10 +219,10 @@
}, },
"python3-openid": { "python3-openid": {
"hashes": [ "hashes": [
"sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa", "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf",
"sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502" "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"
], ],
"version": "==3.1.0" "version": "==3.2.0"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
@@ -259,10 +258,10 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
], ],
"version": "==2.23.0" "version": "==2.24.0"
}, },
"requests-oauthlib": { "requests-oauthlib": {
"hashes": [ "hashes": [

View File

@@ -23,7 +23,7 @@ DJANGO_SECRET_KEY=random_string
ALLOWED_HOSTS=* ALLOWED_HOSTS=*
# 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)
SIGNUPS_ENABLED=False ACCOUNT_SIGNUPS_ENABLED=False
# Should user email addresses be verified? Only set this to `required` if you've setup the email settings and allow # Should user email addresses be verified? Only set this to `required` if you've setup the email settings and allow
# public sign-ups; otherwise, it's unnecessary. # public sign-ups; otherwise, it's unnecessary.
@@ -59,8 +59,8 @@ PORT=8080
# Don't uncomment these unless you know what you are doing! # Don't uncomment these unless you know what you are doing!
# NUM_WORKERS=1 # NUM_WORKERS=1
# Make sure you set a REDIS_CACHE_LOCATION if you have more than one frontend worker/instance. # Make sure you set a REDIS_CACHE_LOCATION if you have more than one frontend worker/instance.
# REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0 # REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
# If CELERY_BROKER_URL is set, make sure CELERY_TASK_ALWAYS_EAGER is False and # If CELERY_BROKER_URL is set, make sure CELERY_TASK_ALWAYS_EAGER is False and
# that you have a separate queue consumer running somewhere via `celeryworker.sh`. # that you have a separate queue consumer running somewhere via `celeryworker.sh`.
# CELERY_TASK_ALWAYS_EAGER=False # CELERY_TASK_ALWAYS_EAGER=False
# CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1 # CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1

View File

@@ -83,7 +83,7 @@
"value": "*", "value": "*",
"required": false "required": false
}, },
"SIGNUPS_ENABLED": { "ACCOUNT_SIGNUPS_ENABLED": {
"description": "Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended).", "description": "Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended).",
"value": "False", "value": "False",
"required": false "required": false
@@ -119,4 +119,4 @@
"required": false "required": false
} }
} }
} }

View File

@@ -1,6 +1,7 @@
version: '3' version: '3'
services: services:
shynet: shynet:
container_name: shynet_main
image: milesmcc/shynet:latest image: milesmcc/shynet:latest
restart: unless-stopped restart: unless-stopped
expose: expose:
@@ -16,6 +17,7 @@ services:
depends_on: depends_on:
- db - db
db: db:
container_name: shynet_database
image: postgres image: postgres
restart: always restart: always
environment: environment:
@@ -26,6 +28,18 @@ services:
- shynet_db:/var/lib/postgresql/data - shynet_db:/var/lib/postgresql/data
networks: networks:
- internal - internal
webserver:
container_name: shynet_webserver
image: nginx
restart: always
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 8080:80
depends_on:
- shynet
networks:
- internal
volumes: volumes:
shynet_db: shynet_db:
networks: networks:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 931 B

View File

@@ -8,7 +8,7 @@ stringData:
DEBUG: "False" DEBUG: "False"
ALLOWED_HOSTS: "*" # For better security, set this to your deployment's domain. Comma separated. ALLOWED_HOSTS: "*" # For better security, set this to your deployment's domain. Comma separated.
DJANGO_SECRET_KEY: "" DJANGO_SECRET_KEY: ""
SIGNUPS_ENABLED: "False" ACCOUNT_SIGNUPS_ENABLED: "False"
TIME_ZONE: "America/New_York" TIME_ZONE: "America/New_York"
# Redis configuration (if you use the default Kubernetes config, this will work) # Redis configuration (if you use the default Kubernetes config, this will work)

19
nginx.conf Normal file
View File

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

109
package-lock.json generated Normal file
View File

@@ -0,0 +1,109 @@
{
"name": "shynet",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@fortawesome/fontawesome-free": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.1.tgz",
"integrity": "sha512-D819f34FLHeBN/4xvw0HR0u7U2G7RqjPSggXqf7LktsxWQ48VAfGwvMrhcVuaZV2fF069c/619RdgCCms0DHhw=="
},
"a17t": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/a17t/-/a17t-0.2.2.tgz",
"integrity": "sha512-/hUtRe5KTwPpfy62jtOsFm35Sq/W0PtuDp/ltbSU+3j4Disop5g85YuuQ6mfc6jRjDgIa6XRs8PdJZVkKe1Y2A=="
},
"apexcharts": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.3.tgz",
"integrity": "sha512-pECgHHNR/etDW2SLUTA58ElrrEyUrhQsEgSiBJCLTwgJ8GMPHA/uSiI5pUJ2jy9+v2FY8Tj+8suH4CCCl3T/pQ==",
"requires": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"inter-ui": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/inter-ui/-/inter-ui-3.13.1.tgz",
"integrity": "sha512-A+gHBm9WXZZmIYHdQci9ZoIrsPkzwYvWqG2+DyrwOuxjZVnRyz3b73ridPUWI/JvZ1nGf2j0VdJ+vxh0/bKBwg=="
},
"litepicker": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/litepicker/-/litepicker-1.5.7.tgz",
"integrity": "sha512-4L2ZcF8iqCE4A/qGWS3PbdFplZR1g751x5SsZ87zCRZ4LQN1Fgezarnvqi0eHk/kDWK7Qx0HZ9Y4bNznJMF1xA=="
},
"svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"requires": {
"svg.js": "^2.0.1"
}
},
"svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=",
"requires": {
"svg.js": ">=2.3.x"
}
},
"svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=",
"requires": {
"svg.js": "^2.2.5"
}
},
"svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"requires": {
"svg.js": "^2.4.0"
}
},
"svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"requires": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"dependencies": {
"svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"requires": {
"svg.js": "^2.2.5"
}
}
}
},
"svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"requires": {
"svg.js": "^2.6.5"
}
},
"turbolinks": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/turbolinks/-/turbolinks-5.2.0.tgz",
"integrity": "sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw=="
}
}
}

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "shynet",
"description": "Modern, privacy-friendly, and cookie-free web analytics.",
"repository": {
"type": "git",
"url": "git+https://github.com/milesmcc/shynet.git"
},
"keywords": [
"privacy",
"analytics",
"self-host"
],
"author": "R. Miles McCain <shynet@sendmiles.email>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/milesmcc/shynet/issues"
},
"homepage": "https://github.com/milesmcc/shynet#readme",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.1",
"a17t": "^0.2.2",
"apexcharts": "^3.19.3",
"inter-ui": "^3.13.1",
"litepicker": "^1.5.7",
"turbolinks": "^5.2.0"
}
}

View File

@@ -1,10 +0,0 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@0.1.3/dist/a17t.css">
<script async src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--family-primary: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--family-secondary: var(--family-primary);
}
</style>

View File

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

View File

@@ -1,23 +1,23 @@
<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 w-auto mr-1">Previous</a> <a href="?page={{ page.previous_page_number }}{{url_parameters}}" class="button field bg-neutral-000 w-auto mr-1">Previous</a>
{% else %} {% else %}
<a class="button field w-auto mr-1" disabled>Previous</a> <a class="button field bg-neutral-000 w-auto mr-1" disabled>Previous</a>
{% endif %} {% endif %}
{% if page.has_next %} {% if page.has_next %}
<a href="?page={{ page.next_page_number }}{{url_parameters}}" class="button field w-auto">Next</a> <a href="?page={{ page.next_page_number }}{{url_parameters}}" class="button field bg-neutral-000 w-auto">Next</a>
{% else %} {% else %}
<a class="button field w-auto" disabled>Next</a> <a class="button field bg-neutral-000 w-auto" disabled>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 %} {% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li> <li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endifequal %}
{% endfor %} {% endfor %}
@@ -25,9 +25,9 @@
<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 %} {% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li> <li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endifequal %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@@ -36,9 +36,9 @@
<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 %} {% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li> <li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endifequal %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

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

View File

@@ -1,7 +1,7 @@
import ipaddress import ipaddress
import json import json
import logging import logging
from hashlib import sha1 from hashlib import sha256
import geoip2.database import geoip2.database
import user_agents import user_agents
@@ -64,7 +64,10 @@ def ingress_request(
try: try:
remote_ip = ipaddress.ip_network(ip) remote_ip = ipaddress.ip_network(ip)
for ignored_network in service.get_ignored_networks(): for ignored_network in service.get_ignored_networks():
if ignored_network.supernet_of(remote_ip): if (
ignored_network.version == remote_ip.version
and ignored_network.supernet_of(remote_ip)
):
return return
except ValueError as e: except ValueError as e:
log.exception(e) log.exception(e)
@@ -73,7 +76,7 @@ def ingress_request(
if payload.get("loadTime", 1) <= 0: if payload.get("loadTime", 1) <= 0:
payload["loadTime"] = None payload["loadTime"] = None
association_id_hash = sha1() association_id_hash = sha256()
association_id_hash.update(str(ip).encode("utf-8")) association_id_hash.update(str(ip).encode("utf-8"))
association_id_hash.update(str(user_agent).encode("utf-8")) association_id_hash.update(str(user_agent).encode("utf-8"))
session_cache_path = ( session_cache_path = (
@@ -121,11 +124,11 @@ def ingress_request(
device=ua.device.family or ua.device.model or "", device=ua.device.family or ua.device.model or "",
device_type=device_type, device_type=device_type,
os=ua.os.family or "", os=ua.os.family or "",
asn=ip_data.get("asn", ""), asn=ip_data.get("asn") or "",
country=ip_data.get("country", ""), country=ip_data.get("country") or "",
longitude=ip_data.get("longitude"), longitude=ip_data.get("longitude"),
latitude=ip_data.get("latitude"), latitude=ip_data.get("latitude"),
time_zone=ip_data.get("time_zone", ""), time_zone=ip_data.get("time_zone") or "",
) )
cache.set( cache.set(
session_cache_path, session.pk, timeout=settings.SESSION_MEMORY_TIMEOUT session_cache_path, session.pk, timeout=settings.SESSION_MEMORY_TIMEOUT

View File

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

View File

@@ -1,10 +1,11 @@
import base64 import base64
import json import json
from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.http import Http404, HttpResponse, HttpResponseBadRequest from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import render, reverse 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
@@ -49,7 +50,20 @@ class ValidateServiceOriginsMixin:
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600) cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
resp = super().dispatch(request, *args, **kwargs) resp = super().dispatch(request, *args, **kwargs)
resp["Access-Control-Allow-Origin"] = origins
if origins != "*":
remote_origin = request.META.get("HTTP_ORIGIN")
if remote_origin is None and request.META.get("HTTP_REFERER") is not None:
parsed = urlparse(request.META.get("HTTP_REFERER"))
remote_origin = f"{parsed.scheme}://{parsed.netloc}".lower()
origins = [origin.strip().lower() for origin in origins.split(",")]
if remote_origin in origins:
resp["Access-Control-Allow-Origin"] = remote_origin
else:
return HttpResponseForbidden()
else:
resp["Access-Control-Allow-Origin"] = "*"
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST" resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
resp[ resp[
"Access-Control-Allow-Headers" "Access-Control-Allow-Headers"
@@ -78,7 +92,7 @@ class PixelView(ValidateServiceOriginsMixin, View):
"R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" "R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
) )
resp = HttpResponse(data, content_type="image/gif") resp = HttpResponse(data, content_type="image/gif")
resp["Cache-Control"] = "no-cache" resp["Cache-Control"] = "no-cache, no-store, must-revalidate"
resp["Access-Control-Allow-Origin"] = "*" resp["Access-Control-Allow-Origin"] = "*"
return resp return resp
@@ -105,11 +119,14 @@ class ScriptView(ValidateServiceOriginsMixin, View):
return render( return render(
self.request, self.request,
"analytics/scripts/page.js", "analytics/scripts/page.js",
context={ context=dict(
"endpoint": endpoint, {
"protocol": protocol, "endpoint": endpoint,
"heartbeat_frequency": heartbeat_frequency, "protocol": protocol,
}, "heartbeat_frequency": heartbeat_frequency,
"script_inject": self.get_script_inject(),
}
),
content_type="application/javascript", content_type="application/javascript",
) )
@@ -125,3 +142,12 @@ class ScriptView(ValidateServiceOriginsMixin, View):
return HttpResponse( return HttpResponse(
json.dumps({"status": "OK"}), content_type="application/json" json.dumps({"status": "OK"}), content_type="application/json"
) )
def get_script_inject(self):
service_uuid = self.kwargs.get("service_uuid")
script_inject = cache.get(f"script_inject_{service_uuid}")
if script_inject == None:
service = Service.objects.get(uuid=service_uuid)
script_inject = service.script_inject
cache.set(f"script_inject_{service_uuid}", script_inject, timeout=3600)
return script_inject

View File

@@ -20,6 +20,12 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
site = Site.objects.get(pk=settings.SITE_ID) site = Site.objects.get(pk=settings.SITE_ID)
site.domain = options.get("hostname") site.domain = options.get("hostname")
if options.get("hostname").lower().startswith("http"):
self.stdout.write(
self.style.WARNING(
f"Warning: the hostname '{options.get('hostname')}' starts with `http`. You almost certainly don't want this. The hostname is supposed to be the raw domain name of your Shynet instance, without `http://` or `https://`. For example, if your Shynet instance will eventually be hosted at `https://analytics.example.com`, the hostname should be `analytics.example.com`."
)
)
site.save() site.save()
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(

View File

@@ -13,7 +13,7 @@ from core.models import User
class Command(BaseCommand): class Command(BaseCommand):
help = "Internal command to perform startup sanity checks." help = "Internal command to perform startup checks."
def check_migrations(self): def check_migrations(self):
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor

View File

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

View File

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

View File

@@ -73,6 +73,7 @@ class Service(models.Model):
hide_referrer_regex = models.TextField( hide_referrer_regex = models.TextField(
default="", blank=True, validators=[_validate_regex] default="", blank=True, validators=[_validate_regex]
) )
script_inject = models.TextField(default="", blank=True)
class Meta: class Meta:
ordering = ["name", "uuid"] ordering = ["name", "uuid"]
@@ -234,7 +235,9 @@ class Service(models.Model):
"session_chart_data": json.dumps( "session_chart_data": json.dumps(
[ [
{"x": str(key), "y": value} {"x": str(key), "y": value}
for key, value in session_chart_data.items() for key, value in sorted(
session_chart_data.items(), key=lambda k: k[0]
)
] ]
), ),
"online": True, "online": True,

View File

@@ -18,6 +18,7 @@ class ServiceForm(forms.ModelForm):
"hide_referrer_regex", "hide_referrer_regex",
"origins", "origins",
"collaborators", "collaborators",
"script_inject"
] ]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
@@ -27,30 +28,33 @@ class ServiceForm(forms.ModelForm):
"collect_ips": 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")]), "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})
} }
labels = { labels = {
"origins": "Allowed Hostnames", "origins": "Allowed origins",
"respect_dnt": "Respect DNT", "respect_dnt": "Respect DNT",
"collect_ips": "Collect IP addresses", "collect_ips": "Collect IP addresses",
"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",
} }
help_texts = { help_texts = {
"name": _("What should the service be called?"), "name": _("What should the service be called?"),
"link": _("What's the service's primary URL?"), "link": _("What's the service's primary URL?"),
"origins": _( "origins": _(
"At what hostnames does the service operate? This sets CORS headers, so use '*' if you're not sure (or don't care)." "At what origins does the service operate? Use commas to separate multiple values. This sets CORS headers, so use '*' if you're not sure (or don't care)."
), ),
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?", "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?",
"collect_ips": "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected.", "collect_ips": "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected.",
"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').", "ignored_ips": "A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32').",
"ignore_robots": "Should sessions generated by bots be excluded from tracking?", "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.", "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.",
} }
collaborators = forms.CharField( collaborators = forms.CharField(
help_text="Which users 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

@@ -7,24 +7,24 @@
<title>{% block head_title %}Privacy-oriented analytics{% endblock %} | {{request.site.name}}</title> <title>{% block head_title %}Privacy-oriented analytics{% endblock %} | {{request.site.name}}</title>
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% include 'a17t/head.html' %} {% include 'a17t/includes/head.html' %}
<script src="https://cdn.jsdelivr.net/npm/litepicker@1.2.0/dist/js/main.js" <script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
integrity="sha256-mOlCEHUNWZPYIrc5OFL4Ab2rsJGzIPld3cy1ok7Cfx0=" crossorigin="anonymous"></script> <script src="{% static 'litepicker/dist/js/main.js' %}"></script>
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.18.1/dist/apexcharts.min.js" <script src="{% static 'turbolinks/dist/turbolinks.js' %}"></script>
integrity="sha256-RalQXBZdisB04aaBsm+6YZ0b/iRYjX1MZn90m19AnCY=" crossorigin="anonymous"></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' %}">
{% block extra_head %} {% block extra_head %}
{% endblock %} {% endblock %}
</head> </head>
<body class="bg-gray-200 min-h-full"> <body class="bg-neutral-200 min-h-full">
{% block body %} {% block body %}
<section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex"> <section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex">
<aside class="mb-8 md:w-2/12 md:pr-6 relative flex flex-wrap md:block justify-between items-center overflow-x-hidden"> <aside class="mb-8 md:w-2/12 md:pr-6 relative flex flex-wrap md:block justify-between items-center overflow-x-hidden">
<a class="icon ~urge ml-2 md:ml-6 md:mb-8 md:mt-3" href="{% url 'dashboard:dashboard' %}"> <a class="icon ~urge ml-2 md:ml-6 md:mb-8 md:mt-3" href="{% url 'dashboard:dashboard' %}">
<i class="fas fa-binoculars fa-3x text-purple-600 hidden md:block"></i> <i class="fas fa-binoculars fa-3x text-urge-600 hidden md:block"></i>
<i class="fas fa-binoculars fa-2x text-purple-600 md:hidden"></i> <i class="fas fa-binoculars fa-2x text-urge-600 md:hidden"></i>
</a> </a>
<button class="button ~neutral !low md:hidden" <button class="button ~neutral !low md:hidden"
@@ -35,7 +35,7 @@
</button> </button>
<hr class="sep h-4 md:h-8 w-full"> <hr class="sep h-4 md:h-8 w-full">
<div id="navMenuExpanded" <div id="navMenuExpanded"
class="bg-white 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">Services</p>
@@ -72,7 +72,7 @@
{% if user.is_superuser %} {% if user.is_superuser %}
{% url 'admin:index' as url %} {% url 'admin:index' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="Admin" url=url %} {% include 'dashboard/includes/sidebar_portal.html' with label="Admin" disable_turbolinks=True url=url %}
{% endif %} {% endif %}
{% url 'account_email' as url %} {% url 'account_email' as url %}

View File

@@ -2,7 +2,7 @@
<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 cursor-pointer" readonly> <input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer" readonly>
<style> <style>
:root { :root {
--litepickerMonthButtonHover: var(--color-urge); --litepickerMonthButtonHover: var(--color-urge);

View File

@@ -13,4 +13,5 @@
{{form.ignore_robots|a17t}} {{form.ignore_robots|a17t}}
{{form.hide_referrer_regex|a17t}} {{form.hide_referrer_regex|a17t}}
{{form.origins|a17t}} {{form.origins|a17t}}
{{form.script_inject|a17t}}
</details> </details>

View File

@@ -4,7 +4,7 @@
{% with stats=object.stats %} {% with stats=object.stats %}
<div class="p-4 md:flex justify-between"> <div class="p-4 md:flex justify-between">
<div class="flex items-center mb-4 md:mb-0"> <div class="flex items-center mb-4 md:mb-0">
<h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-purple-600"> <h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-urge-600">
{{object.name}} {{object.name}}
</h3> </h3>
{% include 'dashboard/includes/stats_status_chip.html' %} {% include 'dashboard/includes/stats_status_chip.html' %}

View File

@@ -13,7 +13,7 @@
<tr> <tr>
<td> <td>
<a href="{% url 'dashboard:service_session' object.pk session.pk %}" <a href="{% url 'dashboard:service_session' object.pk session.pk %}"
class="font-medium text-purple-700"> class="font-medium text-urge-700">
{{session.start_time|date:"M j Y, g:i a"|capfirst}} {{session.start_time|date:"M j Y, g:i a"|capfirst}}
{% if session.is_currently_active %} {% if session.is_currently_active %}
<span class="badge ~positive">Online</span> <span class="badge ~positive">Online</span>

View File

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

View File

@@ -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 w-auto">+ New Service</a> <a href="{% url 'dashboard:service_create' %}" class="button field bg-neutral-000 w-auto">+ New Service</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@@ -6,13 +6,13 @@
<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="{% url 'dashboard:service_update' service.uuid %}" class="button field ~neutral w-auto">Manage &rarr;</a> <a href="{% url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage &rarr;</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
<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-green-400" bad_classes="text-red-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">Sessions</p>
<p class="heading"> <p class="heading">

View File

@@ -5,7 +5,7 @@
{% block head_title %}{{object.name}} Session{% endblock %} {% block head_title %}{{object.name}} Session{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral w-auto">Analytics &rarr;</a> <a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}

View File

@@ -6,7 +6,7 @@
{% 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="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral w-auto">Analytics &rarr;</a> <a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral bg-neutral-000 w-auto">Analytics &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}

View File

@@ -5,7 +5,7 @@
{% block head_title %}{{object.name}} Management{% endblock %} {% block head_title %}{{object.name}} Management{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral w-auto">View &rarr;</a> <a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">View &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}

View File

@@ -84,6 +84,9 @@ class ServiceUpdateView(
cache.set( cache.set(
f"service_origins_{self.object.uuid}", self.object.origins, timeout=3600 f"service_origins_{self.object.uuid}", self.object.origins, timeout=3600
) )
cache.set(
f"script_inject_{self.object.uuid}", self.object.script_inject, timeout=3600
)
return resp return resp
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):

View File

@@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
""" """
import os import os
# import module sys to get the type of exception # import module sys to get the type of exception
import sys import sys
import urllib.parse as urlparse import urllib.parse as urlparse
@@ -17,7 +18,7 @@ import urllib.parse as urlparse
from django.contrib.messages import constants as messages from django.contrib.messages import constants as messages
# Increment on new releases # Increment on new releases
VERSION = "v0.5.2" VERSION = "v0.6.2"
# 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__)))
@@ -116,24 +117,26 @@ else:
} }
# Solution to removal of Heroku DB Injection # Solution to removal of Heroku DB Injection
if 'DATABASE_URL' in os.environ: if "DATABASE_URL" in os.environ:
if 'DATABASES' not in locals(): if "DATABASES" not in locals():
DATABASES = {} DATABASES = {}
url = urlparse.urlparse(os.environ['DATABASE_URL']) url = urlparse.urlparse(os.environ["DATABASE_URL"])
# Ensure default database exists. # Ensure default database exists.
DATABASES['default'] = DATABASES.get('default', {}) DATABASES["default"] = DATABASES.get("default", {})
# Update with environment configuration. # Update with environment configuration.
DATABASES['default'].update({ DATABASES["default"].update(
'NAME': url.path[1:], {
'USER': url.username, "NAME": url.path[1:],
'PASSWORD': url.password, "USER": url.username,
'HOST': url.hostname, "PASSWORD": url.password,
'PORT': url.port, "HOST": url.hostname,
}) "PORT": url.port,
if url.scheme == 'postgres': }
DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql_psycopg2' )
if url.scheme == "postgres":
DATABASES["default"]["ENGINE"] = "django.db.backends.postgresql_psycopg2"
# Password validation # Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
@@ -202,6 +205,11 @@ USE_TZ = True
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATIC_ROOT = "compiledstatic/" STATIC_ROOT = "compiledstatic/"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STATICFILES_FINDERS = [
"npm.finders.NpmFinder",
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
# Redis # Redis
if not DEBUG and os.getenv("REDIS_CACHE_LOCATION") is not None: if not DEBUG and os.getenv("REDIS_CACHE_LOCATION") is not None:
@@ -273,6 +281,20 @@ else:
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
EMAIL_USE_SSL = True EMAIL_USE_SSL = True
# NPM
NPM_ROOT_PATH = "../"
NPM_FILE_PATTERNS = {
"a17t": [os.path.join("dist", "a17t.css"), os.path.join("dist", "tailwind.css")],
"apexcharts": [os.path.join("dist", "apexcharts.min.js")],
"litepicker": [os.path.join("dist", "js", "main.js")],
"turbolinks": [os.path.join("dist", "turbolinks.js")],
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
"inter-ui": [os.path.join("Inter (web)", "*")],
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
}
# Shynet # Shynet
# Can everyone create services, or only superusers? # Can everyone create services, or only superusers?

View File

@@ -21,6 +21,6 @@ urlpatterns = [
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
path("ingress/", include(("analytics.ingress_urls", "ingress")), name="ingress"), path("ingress/", include(("analytics.ingress_urls", "ingress")), name="ingress"),
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")),
] ]

View File

@@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# Check if setup is necessary, do setup as needed # Check if setup is necessary, do setup as needed
echo "Performing startup checks..." echo "Performing startup checks..."
sanity_results=( $(./manage.py startup_checks) ) startup_results=( $(./manage.py startup_checks) )
if [[ ${sanity_results[0]} == True ]]; then if [[ ${startup_results[0]} == True ]]; then
echo "Running migrations (setting up DB)..." echo "Running migrations (setting up DB)..."
{ {
./manage.py migrate && echo "Migrations complete!" ./manage.py migrate && echo "Migrations complete!"
@@ -12,13 +12,13 @@ if [[ ${sanity_results[0]} == True ]]; then
else else
echo "Database is ready to go." echo "Database is ready to go."
fi fi
if [[ ${sanity_results[1]} == True ]]; then if [[ ${startup_results[1]} == True ]]; then
echo "Warning: no admin user available. Consult docs for instructions." echo "Warning: no admin user available. Consult docs for instructions."
fi fi
if [[ ${sanity_results[2]} == True ]]; then if [[ ${startup_results[2]} == True ]]; then
echo "Warning: Shynet's hostname is not set. The script won't work correctly. Consult docs for instructions." echo "Warning: Shynet's hostname is not set. The script won't work correctly. Consult docs for instructions."
fi fi
if [[ ${sanity_results[3]} == True ]]; then if [[ ${startup_results[3]} == True ]]; then
echo "Warning: Shynet's whitelabel is not set. Consult docs for instructions." echo "Warning: Shynet's whitelabel is not set. Consult docs for instructions."
fi fi
echo "Startup checks complete!" echo "Startup checks complete!"

13
tests/js.html Normal file
View File

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

View File

@@ -7,8 +7,7 @@
</head> </head>
<body> <body>
<noscript><img src="//localhost:8000/ingress/test_uuid/pixel.gif"></noscript> <img src="http://localhost:8000/ingress/9b2c4e2f-8d29-4418-82d4-b68e06795025/pixel.gif">
<script src="//localhost:8000/ingress/66015ce4-c69d-40fb-be8f-5535538d795e/script.js"></script>
</body> </body>
</html> </html>