Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fecea17a9d | ||
|
|
03062e3de5 | ||
|
|
6652acdf14 | ||
|
|
1dfbec06e1 | ||
|
|
3e315f06ed | ||
|
|
2d42674e1a | ||
|
|
e4deab2072 | ||
|
|
c5ed5ef0e7 | ||
|
|
7268a4ea84 | ||
|
|
2cbc5ac441 | ||
|
|
058601d669 | ||
|
|
213c44a45a | ||
|
|
8b98cf2277 | ||
|
|
4c53b94588 | ||
|
|
a70e07be05 | ||
|
|
0195c4595b | ||
|
|
a54d9e6840 | ||
|
|
a4245eb733 | ||
|
|
7e0584b5d2 | ||
|
|
37396cde63 | ||
|
|
a1e4bef08f | ||
|
|
c3510278e3 | ||
|
|
da61b9b400 | ||
|
|
98187a39f8 | ||
|
|
3d27efba8b | ||
|
|
80c66ceb8e | ||
|
|
a2776e64f6 | ||
|
|
c73f96525a | ||
|
|
510df192d8 | ||
|
|
2e7620f1eb | ||
|
|
93d4ee5241 | ||
|
|
1a7594be93 | ||
|
|
f464a7ee67 | ||
|
|
a1cd3d4609 | ||
|
|
358fb234a7 | ||
|
|
94fed58de3 | ||
|
|
49f452d9f2 | ||
|
|
40d07fe159 | ||
|
|
e150e6bede | ||
|
|
87a411f42d | ||
|
|
88f25b6743 | ||
|
|
bb0dc2e90f | ||
|
|
4a8939796e | ||
|
|
ba795ccd5c | ||
|
|
c9b5a677d3 | ||
|
|
affcb893fa | ||
|
|
e030807acb | ||
|
|
4ced1365d4 | ||
|
|
dcdbb7cd45 | ||
|
|
b3102f5f32 | ||
|
|
2a61cf1b51 | ||
|
|
0d7c9c4c33 | ||
|
|
6649aeaaf0 | ||
|
|
cb11dc0c4e | ||
|
|
4a4f2645df | ||
|
|
81a836df53 | ||
|
|
919ca52ca1 | ||
|
|
f7ecb88659 | ||
|
|
1a0dcf7579 | ||
|
|
0f3037b315 | ||
|
|
b234ef2917 | ||
|
|
1b344fb90c | ||
|
|
d164306f8b | ||
|
|
c61d23caf1 | ||
|
|
fcfbbe8809 | ||
|
|
1bb4aac32f | ||
|
|
d895eac14d | ||
|
|
5cce890ff6 | ||
|
|
387c1e375d | ||
|
|
4e13842334 | ||
|
|
62c3a87cda | ||
|
|
cac6d44166 | ||
|
|
1a5f68e353 | ||
|
|
4569744726 | ||
|
|
6978bbd03e | ||
|
|
d88f61b281 | ||
|
|
c84dac6b01 | ||
|
|
abe37800ec | ||
|
|
8aef1f0dc7 | ||
|
|
1c01c27326 | ||
|
|
a766c1eaa2 | ||
|
|
a457c2be7b | ||
|
|
6a5ce6ddb9 | ||
|
|
bd88617dc5 | ||
|
|
77f1fbc2cc | ||
|
|
0a0f76d84e | ||
|
|
364ec655a0 | ||
|
|
9fe79c9f23 | ||
|
|
446d672004 | ||
|
|
fe1cb39bc5 | ||
|
|
4737aa1295 | ||
|
|
77871dd56a | ||
|
|
1a0fe6e304 | ||
|
|
26778f0219 | ||
|
|
a210e23bb3 | ||
|
|
34e698e309 | ||
|
|
f33e0e342c | ||
|
|
dfb78b3669 | ||
|
|
5d26ab292b | ||
|
|
837f939de1 | ||
|
|
725496cc0f | ||
|
|
6fa67f0531 | ||
|
|
9b9d70f711 | ||
|
|
c896a4c150 | ||
|
|
bb1860b5c8 | ||
|
|
653594ca48 | ||
|
|
73dad4cb6b | ||
|
|
dd6a9d1eaf | ||
|
|
3c74331a74 | ||
|
|
7bfcb1caff | ||
|
|
0a3441428a | ||
|
|
f7e8580114 | ||
|
|
25b7b1d0e5 | ||
|
|
2223530f51 | ||
|
|
c41e999028 | ||
|
|
1a9d57ed0c | ||
|
|
d2c930fa17 | ||
|
|
62844db6bf | ||
|
|
3a63f6f850 | ||
|
|
2e386a7e25 | ||
|
|
f8d33cbc4d | ||
|
|
2d85a23a20 | ||
|
|
36de929577 | ||
|
|
db8dbb7723 | ||
|
|
23f1fdbb3f | ||
|
|
1f13408f7f | ||
|
|
5c2838af27 | ||
|
|
20c530f669 | ||
|
|
17cdf052d8 | ||
|
|
e693406114 | ||
|
|
39ef4c9645 | ||
|
|
3f7aaa8f0d | ||
|
|
9881dedac0 | ||
|
|
ee99218f2a | ||
|
|
d5e6be7cba |
4
.github/release-drafter.yml
vendored
Normal file
4
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
template: |
|
||||
## What’s Changed
|
||||
|
||||
$CHANGES
|
||||
14
.github/workflows/draft.yml
vendored
Normal file
14
.github/workflows/draft.yml
vendored
Normal 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 }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -3,6 +3,9 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# JavaScript packages
|
||||
node_modules/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
@@ -109,6 +112,9 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
Vagrantfile
|
||||
.vagrant
|
||||
ubuntu-xenial-16.04-cloudimg-console.log
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@@ -131,3 +137,5 @@ dmypy.json
|
||||
# Secrets & env
|
||||
secrets.yml
|
||||
.vscode
|
||||
.DS_Store
|
||||
compiledstatic/
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at shynet@sendmiles.email. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
52
Dockerfile
52
Dockerfile
@@ -1,33 +1,37 @@
|
||||
FROM python:3
|
||||
FROM python:3-alpine
|
||||
|
||||
# Getting things ready
|
||||
WORKDIR /usr/src/shynet
|
||||
COPY Pipfile.lock Pipfile ./
|
||||
COPY package.json package-lock.json ../
|
||||
# Django expects node_modules to be in its parent directory.
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y gettext
|
||||
|
||||
# URL from https://github.com/shlinkio/shlink/issues/596 :)
|
||||
RUN curl "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz" | tar -xvz -C /tmp
|
||||
RUN curl "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz" | tar -xvz -C /tmp
|
||||
RUN mv /tmp/GeoLite2*/*.mmdb /etc
|
||||
|
||||
RUN pip install pipenv
|
||||
COPY Pipfile.lock ./
|
||||
COPY Pipfile ./
|
||||
RUN pipenv install --system --deploy
|
||||
|
||||
COPY shynet .
|
||||
RUN python manage.py collectstatic --noinput
|
||||
RUN python manage.py compilemessages
|
||||
|
||||
# Install dependencies & configure machine
|
||||
ARG GF_UID="500"
|
||||
ARG GF_GID="500"
|
||||
RUN apk update && \
|
||||
apk add gettext curl bash npm && \
|
||||
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 -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \
|
||||
mv /tmp/GeoLite2*/*.mmdb /etc && \
|
||||
apk del curl && \
|
||||
apk add --no-cache postgresql-libs && \
|
||||
apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev && \
|
||||
npm i -P --prefix .. && \
|
||||
pip install pipenv~=2020.6.2 && \
|
||||
pipenv install --system --deploy && \
|
||||
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
|
||||
|
||||
# add group & user
|
||||
RUN groupadd -r -g $GF_GID appgroup && \
|
||||
useradd appuser -r -u $GF_UID -g appgroup
|
||||
# Install Shynet
|
||||
COPY shynet .
|
||||
RUN python manage.py collectstatic --noinput && \
|
||||
python manage.py compilemessages
|
||||
|
||||
# Launch
|
||||
USER appuser
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD [ "./webserver.sh" ]
|
||||
CMD [ "./entrypoint.sh" ]
|
||||
|
||||
275
GUIDE.md
Normal file
275
GUIDE.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Usage Guide
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Heroku](#heroku)
|
||||
- [Render](#render)
|
||||
- [Updating Your Configuration](#updating-your-configuration)
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
* [Installation with SSL](#installation-with-ssl)
|
||||
* [Configuring a Reverse Proxy](#configuring-a-reverse-proxy)
|
||||
+ [Cloudflare](#cloudflare)
|
||||
+ [Nginx](#nginx)
|
||||
* [Health Checks](#health-checks)
|
||||
* [Primary Key Integration](#primary-key-integration)
|
||||
* [Usage with Single-Page Applications](#usage-with-single-page-applications)
|
||||
+ [Troubleshooting](#troubleshooting)
|
||||
---
|
||||
|
||||
## Staying Updated
|
||||
|
||||
**If you install Shynet, you should strongly consider enabling notifications when new versions are released.** You can do this under the "Watch" tab on GitHub (above). This will ensure that you are notified when new versions are available, some of which may be security updates. (Shynet will never automatically update itself.)
|
||||
|
||||
## 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.
|
||||
|
||||
> **These commands assume Ubuntu.** If you're installing Shynet on a different platform, the process will be different.
|
||||
|
||||
Before continuing, please be sure to have the latest version of Docker installed.
|
||||
|
||||
### Basic Installation
|
||||
|
||||
1. Pull the latest version of Shynet using `docker pull milesmcc/shynet:latest`. If you don't have Docker installed, [install it](https://docs.docker.com/get-docker/).
|
||||
|
||||
2. 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)).
|
||||
|
||||
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.
|
||||
|
||||
4. Launch the Shynet server for the first time by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. Provided you're using the default environment information (i.e., `PERFORM_CHECKS_AND_SETUP` is `True`), you'll see a few warnings about not having an admin user or host setup; these are normal. Don't worry — we'll do this in the next step. You only need to stop if you see a stacktrace about being unable to connect to the database.
|
||||
|
||||
5. Create an admin user by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py registeradmin <your email>`. A temporary password will be printed to the console.
|
||||
|
||||
6. Set the 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"`.)
|
||||
|
||||
8. Launch your webserver by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. You may need to bind Docker's port 8080 (where Shynet runs) to your local port 80 (http); this can be done using the flag `-p 80:8080` after `run`. Visit your service's homepage, and verify everything looks right! You should see a login prompt. Log in with the credentials from step 5. You'll probably be prompted to "confirm your email"—if you haven't set up an email server, the confirmation email will be printed to the console instead.
|
||||
|
||||
9. Create a service by clicking "+ Create Service" in the top right hand corner. Fill out the options as appropriate. Once you're done, press "create" and you'll be redirected to your new service's analytics page.
|
||||
|
||||
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
|
||||
|
||||
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 — 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.
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/milesmcc/shynet/tree/master)
|
||||
|
||||
Once you deploy, you'll need to setup an admin user, whitelabel, and hostname before you can use Shynet. Do that with the following commands:
|
||||
|
||||
1. `heroku run --app=<your app> ./manage.py registeradmin <your email>`
|
||||
2. `heroku run --app=<your app> ./manage.py hostname <the hostname where you will run Shynet>`
|
||||
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.
|
||||
|
||||
[](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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
A reverse proxy has many benefits. It can be used for DDoS protection, caching files to reduce server load, routing HTTPS and/or HTTP connections, hosting multiple services on a single server, [and more](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/)!
|
||||
|
||||
#### Cloudflare
|
||||
|
||||
[Cloudflare](https://www.cloudflare.com/) is a great reverse proxy option. It's free, automatically configures HTTPs, offers out-of-the-box security features, provides DNS, and requires minimal setup.
|
||||
|
||||
1. Follow Cloudflare's [getting started guide](https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website).
|
||||
|
||||
2. After setting up Cloudflare, here are a few things you should consider doing:
|
||||
* Under the `SSL` Tab > `Overview` > Change your `SSL/TLS Encryption Mode` to `Flexible`
|
||||
* The following will block your admin panel from anyone who isn't on your IP address. This is optional, but great for security.
|
||||
* Under the `Firewall` tab > `Overview` > `+ Create Firewall Rule`:
|
||||
* Name: `Admin Panel Restriction`
|
||||
* Field: `URI Path`
|
||||
* Operator: `equals`
|
||||
* Value: `/admin`
|
||||
* Click `AND`
|
||||
* Field: `IP Address`
|
||||
* Operator: `does not equal`
|
||||
* Value: `<your public IP address>`
|
||||
* Then: `Block`
|
||||
|
||||
#### Nginx
|
||||
|
||||
Nginx is a self hosted, highly configurable webserver. Nginx can be configured to run as a reverse proxy on either the same machine or a remote machine.
|
||||
|
||||
> **These commands assume Ubuntu.** If you're installing Nginx on a different platform, the process will be different.
|
||||
|
||||
0. Before starting, shut down your Docker containers (if any are running)
|
||||
* Run `docker container ls` to find the container ID
|
||||
* Run `docker stop <container id from the last step>`
|
||||
|
||||
1. Update your packages and install Nginx
|
||||
* `sudo apt-get update`
|
||||
* `sudo apt-get install nginx`
|
||||
|
||||
2. Disable the default Nginx placeholder
|
||||
* `sudo unlink /etc/nginx/sites-enabled/default`
|
||||
|
||||
3. Create the Nginx reverse proxy config file
|
||||
* `cd /etc/nginx/sites-available/`
|
||||
* `vi reverse-proxy.conf` or `nano reverse-proxy.conf`
|
||||
* Paste the following configuration into that file:
|
||||
|
||||
```nginx
|
||||
# Know what you're pasting! Read the Reference!
|
||||
# Reference: https://nginx.org/en/docs/
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Save and exit the text editor
|
||||
* `:wq` for vi
|
||||
* `ctrl+x` then `y` for nano
|
||||
* Link Nginx's `sites-enabled` to read the new config
|
||||
* `sudo ln -s /etc/nginx/sites-available/reverse-proxy.conf /etc/nginx/sites-enabled/reverse-proxy.conf`
|
||||
* Make sure the config is working
|
||||
* `service nginx configtest`
|
||||
* `service nginx restart`
|
||||
|
||||
4. Restart your Docker image, but this time use `8080` as the local bind port, as that's where we configured Nginx to look
|
||||
* `cd ~/`
|
||||
* `docker run -p 8080:8080 --env-file=<your env file> milesmcc/shynet:latest`
|
||||
|
||||
5. Finally, time to test!
|
||||
* Go to `http://<your site>/admin`
|
||||
|
||||
6. If everything is working as expected, please read through some of the following links below to customize Nginx
|
||||
* [How to add SSL/HTTPS to Nginx (Ubuntu 18.04)](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04)
|
||||
* [How to add SSL/HTTPS to Nginx (Ubuntu 16.04)](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04)
|
||||
* [Nginx Documentation](https://nginx.org/en/docs/)
|
||||
|
||||
### Health Checks
|
||||
|
||||
By default, Shynet includes a default health check endpoint at `/healthz/`. If the instance is running normally, this endpoint will return an HTTP status code of 200; if something is wrong, it will have a non-200 status code. To view the health data as JSON, send your request to `/healthz/?format=json`.
|
||||
|
||||
This feature is helpful when running Shynet with Kubernetes, as it allows you to setup [startup readiness probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) that prevent traffic from being sent to your Shynet instances before they are ready.
|
||||
|
||||
### Primary-Key Integration
|
||||
|
||||
In some cases, it is useful to associate particular users on your platform with their sessions in Shynet. In Shynet, this is called _primary key integration_, and is done by adding an additional element to the Shynet script url for each particular user.
|
||||
|
||||
If the Shynet script location (for either the pixel or the script) is, for example, `//shynet.example.com/ingress/your_service_uuid/pixel.gif` and `//shynet.example.com/ingress/your_service_uuid/script.js`, the URLs for primary-key enabled users would be `//shynet.example.com/ingress/your_service_uuid/USER_PRIMARY_KEY/pixel.gif` and `//shynet.example.com/ingress/your_service_uuid/USER_PRIMARY_KEY/script.js`.
|
||||
|
||||
Adding this path can be done easily using server-side rendering. For example, here is a Django template that adds users' primary keys to the Shynet tracking script:
|
||||
|
||||
```html
|
||||
{% if request.user.is_authenticated %}
|
||||
<noscript>
|
||||
<img src="//shynet.example.com/ingress/service-uuid/{{request.user.email|urlencode:""}}/pixel.gif">
|
||||
</noscript>
|
||||
<script src="//shynet.example.com/ingress/service-uuid/{{request.user.email|urlencode:""}}/script.js"></script>
|
||||
{% else %}
|
||||
<noscript>
|
||||
<img src="//shynet.example.com/ingress/service-uuid/pixel.gif">
|
||||
</noscript>
|
||||
<script src="//shynet.example.com/ingress/service-uuid/script.js"></script>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Usage with Single-Page Applications
|
||||
|
||||
In a single-page application, the page never reloads. (That's the entire point of single-page applications, after all!) Unfortunately, this also means that Shynet will not automatically recognize and track when the user navigates between pages _within_ your application.
|
||||
|
||||
Fortunately, Shynet offers a simple method you can call from anywhere within your JavaScript to indicate that a new page has been loaded: `Shynet.newPageLoad()`. Add this method call to the code that handles routing in your app, and you'll be ready to go.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Here are solutions for some common issues. If your situation isn't described here or the solution didn't work, feel free to [create an issue](https://github.com/milesmcc/shynet/issues/new) (but be sure to check for duplicate issues first).
|
||||
|
||||
#### The admin panel works, but no page views are showing up!
|
||||
|
||||
* If you are running a single Shynet webserver instance (i.e., you followed the default installation instructions), verify that you haven't set `CELERY_TASK_ALWAYS_EAGER` to `False` in your environment file.
|
||||
* Verify that your cache is properly configured. In single-instance deployments, this means making sure that you haven't set any `REDIS_*` or `CELERY_*` environment variables (those are for more advanced deployments; you'll just want the defaults).
|
||||
* If your service is configured to respect Do Not Track (under "Advanced Settings"), verify that your browser isn't sending the `DNT=1` header with your requests (or temporarily disable DNT support in Shynet while testing). Sometimes, an adblocker or privacy browser extension will add this header to requests unexpectedly.
|
||||
|
||||
#### Shynet isn't linking different pageviews from the same visitor into a single session!
|
||||
|
||||
* Verify that your cache is properly configured. (See #2 above.) In multi-instance deployments, it's critical that all webservers are using the _same_ cache—so make sure you configure a Redis cache if you're using a non-default installation.
|
||||
* This can happen between Shynet restarts if you're not using an external cache provider (like Redis).
|
||||
|
||||
#### I changed the `SHYNET_WHITELABEL`/`SHYNET_HOST` environment variable, but nothing happened!
|
||||
|
||||
* Those values only affect how your Shynet instance is setup on first run; once it's configured, they have no effect. See [updating your configuration](#updating-your-configuration) for help on how to update your configuration. (Note: these environment variables are not present in newer Shynet versions; they have been removed from the guide.)
|
||||
|
||||
#### Shynet can't connect to my database running on `localhost`/`127.0.0.1`
|
||||
|
||||
* The problem is likely that to Shynet, `localhost` points to the local network in the container itself, not on the host machine. Try adding the `--network='host'` option when you run Docker.
|
||||
45
Pipfile
45
Pipfile
@@ -3,26 +3,29 @@ name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
black = "*"
|
||||
|
||||
[packages]
|
||||
django = "*"
|
||||
django-allauth = "*"
|
||||
geoip2 = "*"
|
||||
whitenoise = "*"
|
||||
celery = "*"
|
||||
django-ipware = "*"
|
||||
pyyaml = "*"
|
||||
ua-parser = "*"
|
||||
user-agents = "*"
|
||||
emoji-country-flag = "*"
|
||||
rules = "*"
|
||||
gunicorn = "*"
|
||||
psycopg2-binary = "*"
|
||||
redis = "*"
|
||||
django-redis-cache = "*"
|
||||
pycountry = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
||||
[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 = "*"
|
||||
|
||||
185
Pipfile.lock
generated
185
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "2cd3e33ea333a40476d2030b6be66826e93c3a4de67032655061725835c92f09"
|
||||
"sha256": "c18d6dc7c78d5f0634e38bb81bc1cf2cd4a0c128d70ca667fe765a66b294e66e"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -16,17 +16,17 @@
|
||||
"default": {
|
||||
"amqp": {
|
||||
"hashes": [
|
||||
"sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8",
|
||||
"sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"
|
||||
"sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b",
|
||||
"sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"
|
||||
],
|
||||
"version": "==2.5.2"
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
|
||||
"sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
|
||||
"sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
|
||||
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
|
||||
],
|
||||
"version": "==3.2.7"
|
||||
"version": "==3.2.10"
|
||||
},
|
||||
"billiard": {
|
||||
"hashes": [
|
||||
@@ -37,18 +37,18 @@
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
"sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f",
|
||||
"sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a"
|
||||
"sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916",
|
||||
"sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.4.2"
|
||||
"version": "==4.4.6"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
|
||||
"sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
],
|
||||
"version": "==2020.4.5.1"
|
||||
"version": "==2020.6.20"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -59,25 +59,33 @@
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
|
||||
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
|
||||
"sha256:8ede8ba04cf5bf7999e1492fa77df545db83717f52c5eab625f97228ebd539bf",
|
||||
"sha256:aa621655d72cdd30f57073893b96cd0c3831a85b08b8e4954531bdac47e3e8c8"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"version": "==0.7.0rc1"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:642d8eceab321ca743ae71e0f985ff8fdca59f07aab3a9fb362c617d23e33a76",
|
||||
"sha256:d4666c2edefa38c5ede0ec1655424c56dc47ceb04b6d8d62a7eac09db89545c1"
|
||||
"sha256:045be31d68dfed684831e39ab1d9e77a595f1a393935cb43b6c5451d2e78c8a4",
|
||||
"sha256:ccf6c208424c0e1b0eaffd36efe12618a9ab4d0037e26f6ffceaa5277af985d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.5"
|
||||
"version": "==3.1b1"
|
||||
},
|
||||
"django-allauth": {
|
||||
"hashes": [
|
||||
"sha256:7ab91485b80d231da191d5c7999ba93170ef1bf14ab6487d886598a1ad03e1d8"
|
||||
"sha256:f17209410b7f87da0a84639fd79d3771b596a6d3fc1a8e48ce50dabc7f441d30"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.41.0"
|
||||
"version": "==0.42.0"
|
||||
},
|
||||
"django-health-check": {
|
||||
"hashes": [
|
||||
"sha256:0563827e003d25fd4d9ebbd7467dea5f390435628d645aaa63f8889deaded73a",
|
||||
"sha256:9e6b7d93d4902901474efd4e25d31b5aaea7563b570c0260adce52cd3c3a9e36"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.12.1"
|
||||
},
|
||||
"django-ipware": {
|
||||
"hashes": [
|
||||
@@ -86,6 +94,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"django-npm": {
|
||||
"hashes": [
|
||||
"sha256:2e6bba65e728fa18b9db3c8dc0d4490b70cb7f43bacf60eb3654d7dcb6424272"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"django-redis-cache": {
|
||||
"hashes": [
|
||||
"sha256:06d4e48545243883f88dc9263dda6c8a0012cb7d0cee2d8758d8917eca92cece",
|
||||
@@ -102,6 +117,12 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"geoip2": {
|
||||
"hashes": [
|
||||
"sha256:5869e987bc54c0d707264fec4710661332cc38d2dca5a7f9bb5362d0308e2ce0",
|
||||
@@ -118,25 +139,33 @@
|
||||
"index": "pypi",
|
||||
"version": "==20.0.4"
|
||||
},
|
||||
"html2text": {
|
||||
"hashes": [
|
||||
"sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b",
|
||||
"sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2020.1.16"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.9"
|
||||
"version": "==2.10"
|
||||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
"sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76",
|
||||
"sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2"
|
||||
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
||||
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
|
||||
],
|
||||
"version": "==4.6.8"
|
||||
"version": "==4.6.11"
|
||||
},
|
||||
"maxminddb": {
|
||||
"hashes": [
|
||||
"sha256:d0ce131d901eb11669996b49a59f410efd3da2c6dbe2c0094fe2fef8d85b6336"
|
||||
"sha256:f4d28823d9ca23323d113dc7af8db2087aa4f657fafc64ff8f7a8afda871425b"
|
||||
],
|
||||
"version": "==1.5.2"
|
||||
"version": "==1.5.4"
|
||||
},
|
||||
"oauthlib": {
|
||||
"hashes": [
|
||||
@@ -190,17 +219,17 @@
|
||||
},
|
||||
"python3-openid": {
|
||||
"hashes": [
|
||||
"sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa",
|
||||
"sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502"
|
||||
"sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf",
|
||||
"sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
|
||||
"sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
|
||||
],
|
||||
"version": "==2019.3"
|
||||
"version": "==2020.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
@@ -221,18 +250,18 @@
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:0dcfb335921b88a850d461dc255ff4708294943322bd55de6cfd68972490ca1f",
|
||||
"sha256:b205cffd05ebfd0a468db74f0eedbff8df1a7bfc47521516ade4692991bb0833"
|
||||
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
||||
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.4.1"
|
||||
"version": "==3.5.3"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
|
||||
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
],
|
||||
"version": "==2.23.0"
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
@@ -250,10 +279,10 @@
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
@@ -294,20 +323,20 @@
|
||||
},
|
||||
"whitenoise": {
|
||||
"hashes": [
|
||||
"sha256:0f9137f74bd95fa54329ace88d8dc695fbe895369a632e35f7a136e003e41d73",
|
||||
"sha256:62556265ec1011bd87113fb81b7516f52688887b7a010ee899ff1fd18fd22700"
|
||||
"sha256:60154b976a13901414a25b0273a841145f77eb34a141f9ae032a0ace3e4d5b27",
|
||||
"sha256:6dd26bfda3af29177d8ab7333a0c7b7642eb615ce83764f4d15a9aecda3201c4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.0.1"
|
||||
"version": "==5.1.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
|
||||
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@@ -326,10 +355,10 @@
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
|
||||
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"version": "==7.1.1"
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
@@ -340,36 +369,36 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b",
|
||||
"sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8",
|
||||
"sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3",
|
||||
"sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e",
|
||||
"sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683",
|
||||
"sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1",
|
||||
"sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142",
|
||||
"sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3",
|
||||
"sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468",
|
||||
"sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e",
|
||||
"sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3",
|
||||
"sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a",
|
||||
"sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f",
|
||||
"sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6",
|
||||
"sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156",
|
||||
"sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b",
|
||||
"sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db",
|
||||
"sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd",
|
||||
"sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a",
|
||||
"sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948",
|
||||
"sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"
|
||||
"sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a",
|
||||
"sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938",
|
||||
"sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29",
|
||||
"sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae",
|
||||
"sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387",
|
||||
"sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a",
|
||||
"sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf",
|
||||
"sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610",
|
||||
"sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9",
|
||||
"sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5",
|
||||
"sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3",
|
||||
"sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89",
|
||||
"sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded",
|
||||
"sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754",
|
||||
"sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f",
|
||||
"sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868",
|
||||
"sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd",
|
||||
"sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910",
|
||||
"sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3",
|
||||
"sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac",
|
||||
"sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"
|
||||
],
|
||||
"version": "==2020.4.4"
|
||||
"version": "==2020.6.8"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
|
||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
|
||||
117
README.md
117
README.md
@@ -1,15 +1,18 @@
|
||||
|
||||
<p align="center">
|
||||
<h3 align="center">🔭 Shynet 🔭</h3>
|
||||
<img align="center" src="images/logo.png" height="50" alt="Shynet logo">
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
Web analytics that's self hosted, cookie free, privacy friendly, and useful(?)
|
||||
<br>
|
||||
<br>
|
||||
<a href="#installation"><strong>Getting started »</strong></a>
|
||||
Modern, privacy-friendly, and cookie-free web analytics.
|
||||
<br>
|
||||
<strong><a href="#installation">Getting started »</a></strong>
|
||||
</p>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#features">Features</a> • <a href="https://github.com/milesmcc/a17t">Design</a></p>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
## Motivation
|
||||
|
||||
There are a _lot_ of web analytics tools. Unfortunately, most of them come with the following caveats:
|
||||
@@ -31,16 +34,21 @@ _Note: These screenshots have been edited to hide sensitive data. The "real" Shy
|
||||

|
||||
_Shynet's homepage, where you can see all of your services at a glance._
|
||||
|
||||
Not shown: service view, management view, session view, full service view. (You'll need to install Shynet to see those!)
|
||||

|
||||
_A real service page, where you can see higher-level details about a site._
|
||||
|
||||
Not shown: management view, session view, full service view. (You'll need to install Shynet for yourself to see those!)
|
||||
|
||||
> **Shynet is built using [a17t](https://github.com/milesmcc/a17t),** an atomic design library. Customization and extension is simple; [learn more about a17t](https://github.com/milesmcc/a17t).
|
||||
|
||||
## Features
|
||||
|
||||
#### Architecture
|
||||
|
||||
* **Runs on a single machine** — Because it's so small, Shynet can easily run as a single docker container on a single small VPS.
|
||||
* **...or across a giant Kubernetes cluster** — For higher traffic installations, Shynet can be deployed with as many parallelized ingress nodes as needed, with Redis caching and separate backend workers for database IO.
|
||||
* **Built using Django** — Shynet is built using Django, so deploying, updating, and migrating can be done without headaches.
|
||||
* **Multiple users and sites** — A single Shynet instance can support multiple users, each tracking multiple different sites.
|
||||
* **Runs on a single machine** — Because it's so small, Shynet can easily run as a single docker container on a single small VPS
|
||||
* **...or across a giant Kubernetes cluster** — For higher traffic installations, Shynet can be deployed with as many parallelized ingress nodes as needed, with Redis caching and separate backend workers for database IO
|
||||
* **Built using Django** — Shynet is built using Django, so deploying, updating, and migrating can be done without headaches
|
||||
* **Multiple users and sites** — A single Shynet instance can support multiple users, each tracking multiple different sites
|
||||
|
||||
#### Tracking
|
||||
|
||||
@@ -67,11 +75,11 @@ Here's the information Shynet can give you about your visitors:
|
||||
|
||||
#### Workflow
|
||||
* **Collaboration built-in** — Administrators can easily share services with other users, as well
|
||||
* **Accounts (or not)** — Shynet has a fully featured account management workflow (powered by [Django Allauth](https://github.com/pennersr/django-allauth/)).
|
||||
* **Accounts (or not)** — Shynet has a fully featured account management workflow (powered by [Django Allauth](https://github.com/pennersr/django-allauth/))
|
||||
|
||||
## Recommendations
|
||||
|
||||
Shynet isn't for everyone. It's great for personal projects and small to medium size websites, but hasn't been tested with ultra-high traffic sites. It's also requires a fair amount of technical know-how to deploy and maintain, so if you need a one-click solution, you're best served with other tools.
|
||||
Shynet isn't for everyone. It's great for personal projects and small to medium size websites, but hasn't been tested with ultra-high traffic sites. It also requires a fair amount of technical know-how to deploy and maintain, so if you need a one-click solution, you're best served with other tools.
|
||||
|
||||
## Concepts
|
||||
|
||||
@@ -85,88 +93,21 @@ Shynet is pretty simple, but there are a few key terms you need to know in order
|
||||
|
||||
## Installation
|
||||
|
||||
To install Shynet using the simplest possible setup, follow these instructions. Instructions for multi-machine deployments will be available soon.
|
||||
|
||||
> **These commands assume Ubuntu.** If you're installing Shynet on a different platform, the process will be different.
|
||||
|
||||
1. Pull the latest version of Shynet using `docker pull milesmcc/shynet:latest`. If you don't have Docker installed, [install it](https://docs.docker.com/get-docker/).
|
||||
|
||||
2. Have a PostgreSQL server ready to go. This can be on the same machine as the deployment, or elsewhere. You'll just need a username, password, and host. (For info on how to setup a PostgreSQL server on Ubuntu, follow [this guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04)).
|
||||
|
||||
3. Configure an environment file for Shynet. (For example, create a file called `.env`.) Be sure to swap out the variables below with the correct values for your setup. (The comments refer to the lines that follow. Note that Docker is weird with quotes, so it tends to be better to omit them from your env file.)
|
||||
|
||||
```
|
||||
# Database
|
||||
DB_NAME=<your db name>
|
||||
DB_USER=<your db user>
|
||||
DB_PASSWORD=<your db user password>
|
||||
DB_HOST=<your db host>
|
||||
|
||||
# General Django settings
|
||||
DJANGO_SECRET_KEY=<your Django secret key; just a random string>
|
||||
# Don't leak error details to visitors, very important
|
||||
DEBUG=False
|
||||
CELERY_TASK_ALWAYS_EAGER=True
|
||||
# For better security, set this to your deployment's domain. Comma separated.
|
||||
ALLOWED_HOSTS=*
|
||||
# Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended)
|
||||
SIGNUPS_ENABLED=False
|
||||
# Change as required
|
||||
TIME_ZONE=America/New_York
|
||||
# Set to "False" if you will not be serving content over HTTPS
|
||||
SCRIPT_USE_HTTPS=True
|
||||
```
|
||||
|
||||
For more advanced deployments, you may consider adding the following settings to your environment file. **The following settings are optional, and not required for simple deployments.**
|
||||
|
||||
```env
|
||||
# Email settings
|
||||
EMAIL_HOST_USER=<your SMTP email user>
|
||||
EMAIL_HOST_PASSWORD=<your SMTP email password>
|
||||
EMAIL_HOST=<your SMTP email hostname>
|
||||
SERVER_EMAIL=Shynet <noreply@shynet.example.com>
|
||||
|
||||
# Redis and queue settings; not necessary for single-instance deployments
|
||||
REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
|
||||
# If set, make sure CELERY_TASK_ALWAYS_EAGER is False
|
||||
CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
||||
```
|
||||
|
||||
4. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py migrate`.
|
||||
|
||||
5. Create your admin account by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py registeradmin <your email>`. The command will print a temporary password that you'll be able to use to log in.
|
||||
|
||||
6. Configure Shynet's hostname (e.g. `shynet.example.com` or `localhost:8000`) by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py hostname "<your hostname>"`. This doesn't affect Shynet's bind port; instead, it determines what hostname to inject into the tracking script. (So you'll want to use the "user-facing" hostname here.)
|
||||
|
||||
7. Name your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py whitelabel "<your instance name>"`. This could be something like "My Shynet Server" or "Acme Analytics"—whatever suits you.
|
||||
|
||||
8. Launch the Shynet server by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. You may need to bind Docker's port 8000 (where Shynet runs) to your local port 8000; this can be done using the flag `-p 8080:8080`.
|
||||
|
||||
9. Visit your service's homepage, and verify everything looks right! You should see a login prompt. Log in with the credentials from step 5. You'll probably be prompted to "confirm your email"—if you haven't set up an email server, the confirmation email will be printed to the console instead.
|
||||
|
||||
10. Create a service by clicking "+ Create Service" in the top right hand corner. Fill out the options as appropriate. Once you're done, press "create" and you'll be redirected to your new service's analytics page.
|
||||
|
||||
11. Finally, click on "Manage" in the top right of the service's page to get the tracking script code. Inject this script on all pages you'd like the service to track.
|
||||
|
||||
**Next steps:** while out of the scope of this short guide, next steps include setting up Shynet behind a reverse proxy (be it your own [Nginx server](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) or [Cloudflare](https://cloudflare.com)), making it run in the background, and integrating it on your sites. Integration instructions are available on each service's management page.
|
||||
|
||||
You can find 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)).
|
||||
|
||||
## FAQ
|
||||
|
||||
**Does Shynet respond to Do Not Track (DNT) signals?** Yes. While there isn't any standardized way to handle DNT requests, Shynet allows you to specify whether you want to collect any data from users with DNT enabled on a per-service basis. (By default, Shynet will _not_ collect any data from users who specify DNT.)
|
||||
|
||||
**Is this GDPR compliant?** I think so, but it also depends on how you use it. If you're worried about GDPR, you should talk to a lawyer about your particular data collection practices. I'm not a lawyer. (And this isn't legal advice.)
|
||||
**Is this GDPR compliant?** It depends on how you use it. If you're worried about GDPR, you should talk to a lawyer about your particular data collection practices. I'm not a lawyer. (And this isn't legal advice.)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Having trouble with Shynet? Check out the [troubleshooting guide](GUIDE.md#troubleshooting), or [create an issue](https://github.com/milesmcc/shynet/issues/new) if you think you found a bug in Shynet itself (or have a feature suggestion).
|
||||
|
||||
## Roadmap
|
||||
|
||||
The following features are planned:
|
||||
|
||||
* **Rollups** (aggregate old data to save space)
|
||||
* **Anomaly detection** (get email alerts when you get a traffic spike or dip)
|
||||
* **Interactive traffic heatmap** (see where in the world your visitors are coming from)
|
||||
* **Better collaboration interface** (the current interface is... a draft)
|
||||
* **Data deletion tool** (easily prune user data by specifying an ID or IP)
|
||||
* **Differential privacy** (explore and share your data without revealing any personal information)
|
||||
To see the upcoming planned features, check out the repository's [roadmap project](https://github.com/milesmcc/shynet/projects/1). Upcoming features include data aggregation through rollups, anomaly detection, detailed data exports, two-factor authentication, and a data deletion tool.
|
||||
|
||||
## In the Wild
|
||||
|
||||
@@ -174,7 +115,7 @@ These sites use Shynet to monitor usage without violating visitors' privacy: [Po
|
||||
|
||||
## Contributing
|
||||
|
||||
Are you interested in contributing to Shynet? Just send a pull request! Maybe once the project matures there will be more detailed contribution guidelines, but for now just send the code this way. Just know that by contributing, you agree to share all of your contributions under the same license as the project (see [LICENSE](LICENSE)).
|
||||
Are you interested in contributing to Shynet? Just send a pull request! Maybe once the project matures there will be more detailed contribution guidelines, but for now just send the code this way and we'll make sure it meets our standards together. Just know that by contributing, you agree to share all of your contributions under the same license as the project (see [LICENSE](LICENSE)). And always be sure to follow the [Code of Conduct](https://github.com/milesmcc/shynet/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
## License
|
||||
|
||||
@@ -182,4 +123,4 @@ Shynet is made available under the [Apache License, version 2.0](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
a17t was created by [Miles McCain](https://miles.land) at the [Recurse Center](https://recurse.com) using [a17t](https://a17t.miles.land).
|
||||
Shynet was created by [Miles McCain](https://miles.land) ([@MilesMcCain](https://twitter.com/MilesMcCain)) at the [Recurse Center](https://recurse.com) using [a17t](https://a17t.miles.land).
|
||||
|
||||
66
TEMPLATE.env
Normal file
66
TEMPLATE.env
Normal file
@@ -0,0 +1,66 @@
|
||||
# This file shows all of the environment variables you can
|
||||
# set to configure Shynet, as well as information about their
|
||||
# effects. Make a copy of this file to configure your deployment.
|
||||
|
||||
# Database settings (PostgreSQL)
|
||||
DB_NAME=shynet_db
|
||||
DB_USER=shynet_db_user
|
||||
DB_PASSWORD=shynet_db_user_password
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
|
||||
# Email settings (optional)
|
||||
EMAIL_HOST_USER=example
|
||||
EMAIL_HOST_PASSWORD=example_password
|
||||
EMAIL_HOST=smtp.example.com
|
||||
EMAIL_PORT=465
|
||||
SERVER_EMAIL=<Shynet> noreply@shynet.example.com
|
||||
|
||||
# General Django settings
|
||||
DJANGO_SECRET_KEY=random_string
|
||||
|
||||
# For better security, set this to your deployment's domain. Comma separated.
|
||||
ALLOWED_HOSTS=*
|
||||
|
||||
# Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended)
|
||||
ACCOUNT_SIGNUPS_ENABLED=False
|
||||
|
||||
# Should user email addresses be verified? Only set this to `required` if you've setup the email settings and allow
|
||||
# public sign-ups; otherwise, it's unnecessary.
|
||||
ACCOUNT_EMAIL_VERIFICATION=none
|
||||
|
||||
# The timezone of the admin panel. Affects how dates are displayed.
|
||||
TIME_ZONE=America/New_York
|
||||
|
||||
# Set to "False" if you will not be serving content over HTTPS
|
||||
SCRIPT_USE_HTTPS=True
|
||||
|
||||
# How frequently should the monitoring script "phone home" (in ms)?
|
||||
SCRIPT_HEARTBEAT_FREQUENCY=5000
|
||||
|
||||
# How much time can elapse between requests from the same user before a new
|
||||
# session is created, in seconds?
|
||||
SESSION_MEMORY_TIMEOUT=1800
|
||||
|
||||
# Should only superusers (admins) be able to create services? This is helpful
|
||||
# when you'd like to invite others to your Shynet instance but don't want
|
||||
# them to be able to create services of their own.
|
||||
ONLY_SUPERUSERS_CREATE=True
|
||||
|
||||
# Whether to perform checks and setup at startup, including applying unapplied
|
||||
# migrations. For most setups, the recommended value is True. Defaults to True.
|
||||
# Will skip only if value is False.
|
||||
PERFORM_CHECKS_AND_SETUP=True
|
||||
|
||||
# The port that Shynet should bind to. Don't set this if you're deploying on Heroku.
|
||||
PORT=8080
|
||||
|
||||
# Redis, queue, and parellization settings; not necessary for single-instance deployments.
|
||||
# Don't uncomment these unless you know what you are doing!
|
||||
# NUM_WORKERS=1
|
||||
# Make sure you set a REDIS_CACHE_LOCATION if you have more than one frontend worker/instance.
|
||||
# REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
|
||||
# If CELERY_BROKER_URL is set, make sure CELERY_TASK_ALWAYS_EAGER is False and
|
||||
# that you have a separate queue consumer running somewhere via `celeryworker.sh`.
|
||||
# CELERY_TASK_ALWAYS_EAGER=False
|
||||
# CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
||||
122
app.json
Normal file
122
app.json
Normal file
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"name": "Shynet",
|
||||
"description":"Modern, privacy-friendly, and detailed web analytics that works without cookies or JS.",
|
||||
"keywords":[
|
||||
"app.json",
|
||||
"shynet",
|
||||
"heroku",
|
||||
"analytics",
|
||||
"privacy",
|
||||
"friendly"
|
||||
],
|
||||
"website": "https://github.com/milesmcc/shynet",
|
||||
"repository": "https://github.com/milesmcc/shynet",
|
||||
"logo": "https://github.com/milesmcc/shynet/raw/master/images/slogo.png",
|
||||
"success_url": "/",
|
||||
"stack": "container",
|
||||
"addons": [
|
||||
"heroku-postgresql:hobby-dev"
|
||||
],
|
||||
"formation": {
|
||||
"web": {
|
||||
"quantity": 1,
|
||||
"size": "free"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"DB_NAME": {
|
||||
"description": "Postgres database name (not required if using Postgres addon)",
|
||||
"value": "shynet",
|
||||
"required": false
|
||||
},
|
||||
"DB_USER": {
|
||||
"description": "Postgres database username (not required if using Postgres addon)",
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"DB_PASSWORD": {
|
||||
"description": "Postgres database password (not required if using Postgres addon)",
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"DB_HOST": {
|
||||
"description": "Postgres database hostname (not required if using Postgres addon)",
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"DB_PORT": {
|
||||
"description": "Postgres database port (not required if using Postgres addon)",
|
||||
"value": "5432",
|
||||
"required": false
|
||||
},
|
||||
"EMAIL_HOST": {
|
||||
"description": "SMTP server hostname (for sending emails)",
|
||||
"value": "smtp.gmail.com",
|
||||
"required": false
|
||||
},
|
||||
"EMAIL_PORT": {
|
||||
"description": "SMTP server port (for sending emails)",
|
||||
"value": "465",
|
||||
"required": false
|
||||
},
|
||||
"EMAIL_HOST_USER": {
|
||||
"description": "SMTP server username (for sending emails)",
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"EMAIL_HOST_PASSWORD": {
|
||||
"description": "SMTP server password (for sending emails)",
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"SERVER_EMAIL": {
|
||||
"description": "Email address (for sending emails)",
|
||||
"value": "<Shynet> noreply@shynet.example.com",
|
||||
"required": false
|
||||
},
|
||||
"DJANGO_SECRET_KEY": {
|
||||
"description": "Django secret key",
|
||||
"generator": "secret"
|
||||
},
|
||||
"ALLOWED_HOSTS": {
|
||||
"description": "For better security, set this to your deployment's domain. (Where you will actually host, not embed, Shynet.) Set to '*' to allow serving all domains.",
|
||||
"value": "*",
|
||||
"required": false
|
||||
},
|
||||
"ACCOUNT_SIGNUPS_ENABLED": {
|
||||
"description": "Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended).",
|
||||
"value": "False",
|
||||
"required": false
|
||||
},
|
||||
"TIME_ZONE": {
|
||||
"description": "The timezone of the admin panel. Affects how dates are displayed.",
|
||||
"value": "America/New_York",
|
||||
"required": false
|
||||
},
|
||||
"SCRIPT_USE_HTTPS": {
|
||||
"description": "Set to 'False' if you will not be serving Shynet over HTTPS.",
|
||||
"value": "True",
|
||||
"required": false
|
||||
},
|
||||
"SCRIPT_HEARTBEAT_FREQUENCY": {
|
||||
"description": "How frequently should the monitoring script 'phone home' (in ms)?",
|
||||
"value": "5000",
|
||||
"required": false
|
||||
},
|
||||
"SESSION_MEMORY_TIMEOUT": {
|
||||
"description": "How much time can elapse between requests from the same user before a new session is created, in seconds?",
|
||||
"value": "1800",
|
||||
"required": false
|
||||
},
|
||||
"ONLY_SUPERUSERS_CREATE": {
|
||||
"description": "Should only superusers (admins) be able to create tracked services?",
|
||||
"value": "True",
|
||||
"required": false
|
||||
},
|
||||
"PERFORM_CHECKS_AND_SETUP": {
|
||||
"description": "Whether to perform checks and setup at startup. Recommended value is 'True' for Heroku users.",
|
||||
"value": "True",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
version: '3'
|
||||
services:
|
||||
shynet:
|
||||
container_name: shynet_main
|
||||
image: milesmcc/shynet:latest
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8080
|
||||
env_file:
|
||||
# Create a file called '.env' if it doesn't already exist.
|
||||
# You can use `TEMPLATE.env` as a guide.
|
||||
- .env
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
networks:
|
||||
- internal
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
container_name: shynet_database
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
- "POSTGRES_USER=${DB_USER}"
|
||||
- "POSTGRES_PASSWORD=${DB_PASSWORD}"
|
||||
- "POSTGRES_DB=${DB_NAME}"
|
||||
volumes:
|
||||
- shynet_db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- internal
|
||||
webserver:
|
||||
container_name: shynet_webserver
|
||||
image: nginx
|
||||
restart: always
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- 8080:80
|
||||
depends_on:
|
||||
- shynet
|
||||
networks:
|
||||
- internal
|
||||
volumes:
|
||||
shynet_db:
|
||||
networks:
|
||||
internal:
|
||||
3
heroku.yml
Normal file
3
heroku.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
build:
|
||||
docker:
|
||||
web: Dockerfile
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 215 KiB |
BIN
images/logo.png
Normal file
BIN
images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
images/service.png
Normal file
BIN
images/service.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 360 KiB |
BIN
images/slogo.png
Normal file
BIN
images/slogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 931 B |
@@ -16,13 +16,12 @@ spec:
|
||||
app: "shynet-webserver"
|
||||
spec:
|
||||
containers:
|
||||
- name: "covideo-webserver"
|
||||
- name: "shynet-webserver"
|
||||
image: "milesmcc/shynet:latest"
|
||||
command: ["./webserver.sh"]
|
||||
imagePullPolicy: Always
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: django-settings
|
||||
name: shynet-settings
|
||||
---
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
@@ -42,43 +41,43 @@ spec:
|
||||
app: "shynet-celeryworker"
|
||||
spec:
|
||||
containers:
|
||||
- name: "covideo-celeryworker"
|
||||
- name: "shynet-celeryworker"
|
||||
image: "milesmcc/shynet:latest"
|
||||
command: ["./celeryworker.sh"]
|
||||
imagePullPolicy: Always
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: django-settings
|
||||
name: shynet-settings
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
name: shynet-redis
|
||||
spec:
|
||||
ports:
|
||||
- port: 6379
|
||||
name: redis
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: redis
|
||||
app: shynet-redis
|
||||
---
|
||||
apiVersion: apps/v1beta2
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: redis
|
||||
name: shynet-redis
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
serviceName: redis
|
||||
app: shynet-redis
|
||||
serviceName: shynet-redis
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
app: shynet-redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
- name: shynet-redis
|
||||
image: redis:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: django-settings
|
||||
name: shynet-settings
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Django settings
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: "*" # For better security, set this to your deployment's domain. Comma separated.
|
||||
DJANGO_SECRET_KEY: ""
|
||||
SIGNUPS_ENABLED: "False"
|
||||
ACCOUNT_SIGNUPS_ENABLED: "False"
|
||||
TIME_ZONE: "America/New_York"
|
||||
|
||||
# Redis configuration (if you use the default Kubernetes config, this will work)
|
||||
REDIS_CACHE_LOCATION: "redis://redis.default.svc.cluster.local/0"
|
||||
CELERY_BROKER_URL: "redis://redis.default.svc.cluster.local/1"
|
||||
REDIS_CACHE_LOCATION: "redis://shynet-redis.default.svc.cluster.local/0"
|
||||
CELERY_BROKER_URL: "redis://shynet-redis.default.svc.cluster.local/1"
|
||||
|
||||
# PostgreSQL settings
|
||||
DB_NAME: ""
|
||||
|
||||
19
nginx.conf
Normal file
19
nginx.conf
Normal 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
109
package-lock.json
generated
Normal 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
27
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@0.1.3/dist/a17t.css">
|
||||
<script async src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||
12
shynet/a17t/templates/a17t/includes/head.html
Normal file
12
shynet/a17t/templates/a17t/includes/head.html
Normal 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>
|
||||
@@ -1,23 +1,23 @@
|
||||
<nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination">
|
||||
<div class="w-full md:w-auto mb-2">
|
||||
{% if page.has_previous %}
|
||||
<a href="?page={{ page.previous_page_number }}{{url_parameters}}" class="button field w-auto mr-1">Previous</a>
|
||||
<a href="?page={{ page.previous_page_number }}{{url_parameters}}" class="button field bg-neutral-000 w-auto mr-1">Previous</a>
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
<a class="button field w-auto" disabled>Next</a>
|
||||
<a class="button field bg-neutral-000 w-auto" disabled>Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<ul class="pagination-list w-full md:w-auto mb-2 flex">
|
||||
{% for pnum in begin %}
|
||||
{% ifequal page.number pnum %}
|
||||
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li>
|
||||
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
<li><span class="pagination-ellipsis">…</span></li>
|
||||
{% for pnum in middle %}
|
||||
{% ifequal page.number pnum %}
|
||||
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li>
|
||||
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -36,9 +36,9 @@
|
||||
<li><span class="pagination-ellipsis">…</span></li>
|
||||
{% for pnum in end %}
|
||||
{% ifequal page.number pnum %}
|
||||
<li><a class="button field w-auto mx-1 text-white bg-gray-700">{{ pnum }}</a></li>
|
||||
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -92,4 +92,6 @@ def is_file(field):
|
||||
def add_class(field, css_class):
|
||||
if len(field.errors) > 0:
|
||||
css_class += " ~critical"
|
||||
if field.field.widget.attrs.get("class") != None:
|
||||
css_class += " " + field.field.widget.attrs["class"]
|
||||
return field.as_widget(attrs={"class": field.css_classes(extra_classes=css_class)})
|
||||
|
||||
18
shynet/analytics/migrations/0003_auto_20200502_1227.py
Normal file
18
shynet/analytics/migrations/0003_auto_20200502_1227.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.5 on 2020-05-02 16:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("analytics", "0002_auto_20200415_1742"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="ip",
|
||||
field=models.GenericIPAddressField(db_index=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -39,7 +39,7 @@ class Session(models.Model):
|
||||
default="OTHER",
|
||||
)
|
||||
os = models.TextField()
|
||||
ip = models.GenericIPAddressField(db_index=True)
|
||||
ip = models.GenericIPAddressField(db_index=True, null=True)
|
||||
|
||||
# GeoIP data
|
||||
asn = models.TextField(blank=True)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
from hashlib import sha256
|
||||
|
||||
import geoip2.database
|
||||
import user_agents
|
||||
@@ -42,7 +44,15 @@ def _geoip2_lookup(ip):
|
||||
|
||||
@shared_task
|
||||
def ingress_request(
|
||||
service_uuid, tracker, time, payload, ip, location, user_agent, dnt=False, identifier=""
|
||||
service_uuid,
|
||||
tracker,
|
||||
time,
|
||||
payload,
|
||||
ip,
|
||||
location,
|
||||
user_agent,
|
||||
dnt=False,
|
||||
identifier="",
|
||||
):
|
||||
try:
|
||||
service = Service.objects.get(pk=service_uuid, status=Service.ACTIVE)
|
||||
@@ -51,34 +61,50 @@ def ingress_request(
|
||||
if dnt and service.respect_dnt:
|
||||
return
|
||||
|
||||
ip_data = _geoip2_lookup(ip)
|
||||
log.debug(f"Found geoip2 data")
|
||||
try:
|
||||
remote_ip = ipaddress.ip_network(ip)
|
||||
for ignored_network in service.get_ignored_networks():
|
||||
if (
|
||||
ignored_network.version == remote_ip.version
|
||||
and ignored_network.supernet_of(remote_ip)
|
||||
):
|
||||
return
|
||||
except ValueError as e:
|
||||
log.exception(e)
|
||||
|
||||
# Validate payload
|
||||
if payload.get("loadTime", 1) <= 0:
|
||||
payload["loadTime"] = None
|
||||
|
||||
# Create or update session
|
||||
session = (
|
||||
Session.objects.filter(
|
||||
service=service,
|
||||
last_seen__gt=timezone.now() - timezone.timedelta(minutes=10),
|
||||
ip=ip,
|
||||
user_agent=user_agent,
|
||||
).first()
|
||||
# We used to check for identifiers, but that can cause issues when people
|
||||
# re-open the page in a new tab, for example. It's better to match sessions
|
||||
# solely based on IP and user agent.
|
||||
association_id_hash = sha256()
|
||||
association_id_hash.update(str(ip).encode("utf-8"))
|
||||
association_id_hash.update(str(user_agent).encode("utf-8"))
|
||||
session_cache_path = (
|
||||
f"session_association_{service.pk}_{association_id_hash.hexdigest()}"
|
||||
)
|
||||
|
||||
# Create or update session
|
||||
session = None
|
||||
if cache.get(session_cache_path) is not None:
|
||||
cache.touch(session_cache_path, settings.SESSION_MEMORY_TIMEOUT)
|
||||
session = Session.objects.filter(
|
||||
pk=cache.get(session_cache_path), service=service
|
||||
).first()
|
||||
if session is None:
|
||||
log.debug("Cannot link to existing session; creating a new one...")
|
||||
ua = user_agents.parse(user_agent)
|
||||
initial = True
|
||||
|
||||
log.debug("Cannot link to existing session; creating a new one...")
|
||||
|
||||
ip_data = _geoip2_lookup(ip)
|
||||
log.debug(f"Found geoip2 data...")
|
||||
|
||||
ua = user_agents.parse(user_agent)
|
||||
device_type = "OTHER"
|
||||
if (
|
||||
ua.is_bot
|
||||
or (ua.browser.family or "").strip().lower() == "googlebot"
|
||||
or (ua.device.family or ua.device.model or "").strip().lower() == "spider"
|
||||
or (ua.device.family or ua.device.model or "").strip().lower()
|
||||
== "spider"
|
||||
):
|
||||
device_type = "ROBOT"
|
||||
elif ua.is_mobile:
|
||||
@@ -87,24 +113,31 @@ def ingress_request(
|
||||
device_type = "TABLET"
|
||||
elif ua.is_pc:
|
||||
device_type = "DESKTOP"
|
||||
if device_type == "ROBOT" and service.ignore_robots:
|
||||
return
|
||||
session = Session.objects.create(
|
||||
service=service,
|
||||
ip=ip,
|
||||
ip=ip if service.collect_ips else None,
|
||||
user_agent=user_agent,
|
||||
identifier=identifier.strip(),
|
||||
browser=ua.browser.family or "",
|
||||
device=ua.device.family or ua.device.model or "",
|
||||
device_type=device_type,
|
||||
os=ua.os.family or "",
|
||||
asn=ip_data.get("asn", ""),
|
||||
country=ip_data.get("country", ""),
|
||||
asn=ip_data.get("asn") or "",
|
||||
country=ip_data.get("country") or "",
|
||||
longitude=ip_data.get("longitude"),
|
||||
latitude=ip_data.get("latitude"),
|
||||
time_zone=ip_data.get("time_zone", ""),
|
||||
time_zone=ip_data.get("time_zone") or "",
|
||||
)
|
||||
cache.set(
|
||||
session_cache_path, session.pk, timeout=settings.SESSION_MEMORY_TIMEOUT
|
||||
)
|
||||
else:
|
||||
log.debug("Updating old session with new data...")
|
||||
initial = False
|
||||
|
||||
log.debug("Updating old session with new data...")
|
||||
|
||||
# Update last seen time
|
||||
session.last_seen = timezone.now()
|
||||
if session.identifier == "" and identifier.strip() != "":
|
||||
@@ -115,9 +148,10 @@ def ingress_request(
|
||||
idempotency = payload.get("idempotency")
|
||||
idempotency_path = f"hit_idempotency_{idempotency}"
|
||||
hit = None
|
||||
|
||||
if idempotency is not None:
|
||||
if cache.get(idempotency_path) is not None:
|
||||
cache.touch(idempotency_path, 10 * 60)
|
||||
cache.touch(idempotency_path, settings.SESSION_MEMORY_TIMEOUT)
|
||||
hit = Hit.objects.filter(
|
||||
pk=cache.get(idempotency_path), session=session
|
||||
).first()
|
||||
@@ -128,6 +162,7 @@ def ingress_request(
|
||||
hit.heartbeats += 1
|
||||
hit.last_seen = timezone.now()
|
||||
hit.save()
|
||||
|
||||
if hit is None:
|
||||
log.debug("Hit is a page load; creating new hit...")
|
||||
# There is no existing hit; create a new one
|
||||
@@ -144,7 +179,9 @@ def ingress_request(
|
||||
)
|
||||
# Set idempotency (if applicable)
|
||||
if idempotency is not None:
|
||||
cache.set(idempotency_path, hit.pk, timeout=10 * 60)
|
||||
cache.set(
|
||||
idempotency_path, hit.pk, timeout=settings.SESSION_MEMORY_TIMEOUT
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise e
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
window.onload = function () {
|
||||
var idempotency =
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15);
|
||||
function sendUpdate() {
|
||||
// This is a lightweight and privacy-friendly analytics script from Shynet, a self-hosted
|
||||
// analytics tool. To give you full visibility into how your data is being monitored, this
|
||||
// file is intentionally not minified or obfuscated. To learn more about Shynet (and to view
|
||||
// its source code), visit <https://github.com/milesmcc/shynet>.
|
||||
//
|
||||
// This script only sends the current URL, the referrer URL, and the page load time. That's it!
|
||||
|
||||
var Shynet = {
|
||||
idempotency: null,
|
||||
heartbeatTaskId: null,
|
||||
sendHeartbeat: function () {
|
||||
try {
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(
|
||||
"POST",
|
||||
@@ -13,7 +22,7 @@ window.onload = function () {
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(
|
||||
JSON.stringify({
|
||||
idempotency: idempotency,
|
||||
idempotency: Shynet.idempotency,
|
||||
referrer: document.referrer,
|
||||
location: window.location.href,
|
||||
loadTime:
|
||||
@@ -21,8 +30,25 @@ window.onload = function () {
|
||||
window.performance.timing.navigationStart,
|
||||
})
|
||||
);
|
||||
} catch {}
|
||||
} catch (e) { }
|
||||
},
|
||||
newPageLoad: function () {
|
||||
if (Shynet.heartbeatTaskId != null) {
|
||||
clearInterval(Shynet.heartbeatTaskId);
|
||||
}
|
||||
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
|
||||
Shynet.sendHeartbeat();
|
||||
}
|
||||
setInterval(sendUpdate, 5000);
|
||||
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 %}
|
||||
@@ -1,8 +1,11 @@
|
||||
import base64
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.shortcuts import render, reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -10,6 +13,8 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import TemplateView, View
|
||||
from ipware import get_client_ip
|
||||
|
||||
from core.models import Service
|
||||
|
||||
from ..tasks import ingress_request
|
||||
|
||||
|
||||
@@ -33,13 +38,50 @@ def ingress(request, service_uuid, identifier, tracker, payload):
|
||||
)
|
||||
|
||||
|
||||
class PixelView(View):
|
||||
class ValidateServiceOriginsMixin:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
service_uuid = self.kwargs.get("service_uuid")
|
||||
origins = cache.get(f"service_origins_{service_uuid}")
|
||||
|
||||
if origins is None:
|
||||
service = Service.objects.get(uuid=service_uuid)
|
||||
origins = service.origins
|
||||
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
|
||||
|
||||
resp = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
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-Headers"
|
||||
] = "Origin, X-Requested-With, Content-Type, Accept, Authorization, Referer"
|
||||
return resp
|
||||
except Service.DoesNotExist:
|
||||
raise Http404()
|
||||
except ValidationError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
class PixelView(ValidateServiceOriginsMixin, View):
|
||||
# Fallback view to serve an unobtrusive 1x1 transparent tracking pixel for browsers with
|
||||
# JavaScript disabled.
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
def get(self, *args, **kwargs):
|
||||
# Extract primary data
|
||||
ingress(
|
||||
request,
|
||||
self.request,
|
||||
self.kwargs.get("service_uuid"),
|
||||
self.kwargs.get("identifier", ""),
|
||||
"PIXEL",
|
||||
@@ -50,22 +92,13 @@ class PixelView(View):
|
||||
"R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||
)
|
||||
resp = HttpResponse(data, content_type="image/gif")
|
||||
resp["Cache-Control"] = "no-cache"
|
||||
resp["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
resp["Access-Control-Allow-Origin"] = "*"
|
||||
return resp
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class ScriptView(View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
resp = super().dispatch(request, *args, **kwargs)
|
||||
resp["Access-Control-Allow-Origin"] = "*"
|
||||
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
|
||||
resp[
|
||||
"Access-Control-Allow-Headers"
|
||||
] = "Origin, X-Requested-With, Content-Type, Accept, Authorization, Referer"
|
||||
return resp
|
||||
|
||||
class ScriptView(ValidateServiceOriginsMixin, View):
|
||||
def get(self, *args, **kwargs):
|
||||
protocol = "https" if settings.SCRIPT_USE_HTTPS else "http"
|
||||
endpoint = (
|
||||
@@ -82,10 +115,18 @@ class ScriptView(View):
|
||||
},
|
||||
)
|
||||
)
|
||||
heartbeat_frequency = settings.SCRIPT_HEARTBEAT_FREQUENCY
|
||||
return render(
|
||||
self.request,
|
||||
"analytics/scripts/page.js",
|
||||
context={"endpoint": endpoint, "protocol": protocol},
|
||||
context=dict(
|
||||
{
|
||||
"endpoint": endpoint,
|
||||
"protocol": protocol,
|
||||
"heartbeat_frequency": heartbeat_frequency,
|
||||
"script_inject": self.get_script_inject(),
|
||||
}
|
||||
),
|
||||
content_type="application/javascript",
|
||||
)
|
||||
|
||||
@@ -101,3 +142,12 @@ class ScriptView(View):
|
||||
return HttpResponse(
|
||||
json.dumps({"status": "OK"}), content_type="application/json"
|
||||
)
|
||||
|
||||
def get_script_inject(self):
|
||||
service_uuid = self.kwargs.get("service_uuid")
|
||||
script_inject = cache.get(f"script_inject_{service_uuid}")
|
||||
if script_inject == 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
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from core.models import User
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils.crypto import get_random_string
|
||||
import uuid
|
||||
import traceback
|
||||
|
||||
from core.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -12,13 +14,18 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"hostname",
|
||||
type=str,
|
||||
"hostname", type=str,
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
site = Site.objects.get(pk=settings.SITE_ID)
|
||||
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()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from core.models import User
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils.crypto import get_random_string
|
||||
import uuid
|
||||
import traceback
|
||||
|
||||
from core.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -12,18 +14,13 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"email",
|
||||
type=str,
|
||||
"email", type=str,
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
email = options.get("email")
|
||||
password = get_random_string()
|
||||
User.objects.create_superuser(
|
||||
str(uuid.uuid4()), email=email, password=password
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Successfully created a Shynet superuser")
|
||||
)
|
||||
User.objects.create_superuser(str(uuid.uuid4()), email=email, password=password)
|
||||
self.stdout.write(self.style.SUCCESS("Successfully created a Shynet superuser"))
|
||||
self.stdout.write(f"Email address: {email}")
|
||||
self.stdout.write(f"Password: {password}")
|
||||
|
||||
56
shynet/core/management/commands/startup_checks.py
Normal file
56
shynet/core/management/commands/startup_checks.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.db.utils import ConnectionHandler, OperationalError
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
from core.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Internal command to perform startup checks."
|
||||
|
||||
def check_migrations(self):
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
|
||||
try:
|
||||
executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
|
||||
except OperationalError:
|
||||
# DB_NAME database not found?
|
||||
return True
|
||||
except ImproperlyConfigured:
|
||||
# No databases are configured (or the dummy one)
|
||||
return True
|
||||
|
||||
if executor.migration_plan(executor.loader.graph.leaf_nodes()):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def handle(self, *args, **options):
|
||||
migration = self.check_migrations()
|
||||
|
||||
admin, hostname, whitelabel = [True] * 3
|
||||
if not migration:
|
||||
admin = not User.objects.all().exists()
|
||||
hostname = (
|
||||
not Site.objects.filter(domain__isnull=False)
|
||||
.exclude(domain__exact="")
|
||||
.exclude(domain__exact="example.com")
|
||||
.exists()
|
||||
)
|
||||
whitelabel = (
|
||||
not Site.objects.filter(name__isnull=False)
|
||||
.exclude(name__exact="")
|
||||
.exclude(name__exact="example.com")
|
||||
.exists()
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"{migration} {admin} {hostname} {whitelabel}")
|
||||
)
|
||||
@@ -1,10 +1,12 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from core.models import User
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils.crypto import get_random_string
|
||||
import uuid
|
||||
import traceback
|
||||
|
||||
from core.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -12,8 +14,7 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"name",
|
||||
type=str,
|
||||
"name", type=str,
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_auto_20200415_1742'),
|
||||
("core", "0002_auto_20200415_1742"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='respect_dnt',
|
||||
model_name="service",
|
||||
name="respect_dnt",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
|
||||
18
shynet/core/migrations/0004_service_collect_ips.py
Normal file
18
shynet/core/migrations/0004_service_collect_ips.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.5 on 2020-05-02 16:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0003_service_respect_dnt"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="service",
|
||||
name="collect_ips",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
22
shynet/core/migrations/0005_service_ignored_ips.py
Normal file
22
shynet/core/migrations/0005_service_ignored_ips.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-07 20:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import core.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0004_service_collect_ips"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="service",
|
||||
name="ignored_ips",
|
||||
field=models.TextField(
|
||||
blank=True, default="", validators=[core.models._validate_network_list]
|
||||
),
|
||||
),
|
||||
]
|
||||
22
shynet/core/migrations/0006_service_hide_referrer_regex.py
Normal file
22
shynet/core/migrations/0006_service_hide_referrer_regex.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-07 21:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import core.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0005_service_ignored_ips"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="service",
|
||||
name="hide_referrer_regex",
|
||||
field=models.TextField(
|
||||
blank=True, default="", validators=[core.models._validate_regex]
|
||||
),
|
||||
),
|
||||
]
|
||||
18
shynet/core/migrations/0007_service_ignore_robots.py
Normal file
18
shynet/core/migrations/0007_service_ignore_robots.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.6 on 2020-06-15 16:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0006_service_hide_referrer_regex"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="service",
|
||||
name="ignore_robots",
|
||||
field=models.BooleanField(default=False),
|
||||
)
|
||||
]
|
||||
23
shynet/core/migrations/0008_auto_20200628_1403.py
Normal file
23
shynet/core/migrations/0008_auto_20200628_1403.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -1,8 +1,11 @@
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.functions import TruncDate
|
||||
from django.db.utils import NotSupportedError
|
||||
@@ -14,6 +17,26 @@ def _default_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def _validate_network_list(networks: str):
|
||||
try:
|
||||
_parse_network_list(networks)
|
||||
except ValueError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
def _validate_regex(regex: str):
|
||||
try:
|
||||
re.compile(regex)
|
||||
except re.error:
|
||||
raise ValidationError(f"'{regex}' is not valid RegEx")
|
||||
|
||||
|
||||
def _parse_network_list(networks: str):
|
||||
if len(networks.strip()) == 0:
|
||||
return []
|
||||
return [ipaddress.ip_network(network.strip()) for network in networks.split(",")]
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
username = models.TextField(default=_default_uuid, unique=True)
|
||||
email = models.EmailField(unique=True)
|
||||
@@ -42,6 +65,15 @@ class Service(models.Model):
|
||||
max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True
|
||||
)
|
||||
respect_dnt = models.BooleanField(default=True)
|
||||
ignore_robots = models.BooleanField(default=False)
|
||||
collect_ips = models.BooleanField(default=True)
|
||||
ignored_ips = models.TextField(
|
||||
default="", blank=True, validators=[_validate_network_list]
|
||||
)
|
||||
hide_referrer_regex = models.TextField(
|
||||
default="", blank=True, validators=[_validate_regex]
|
||||
)
|
||||
script_inject = models.TextField(default="", blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name", "uuid"]
|
||||
@@ -49,6 +81,21 @@ class Service(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_ignored_networks(self):
|
||||
return _parse_network_list(self.ignored_ips)
|
||||
|
||||
def get_ignored_referrer_regex(self):
|
||||
if len(self.hide_referrer_regex.strip()) == 0:
|
||||
return re.compile(r".^") # matches nothing
|
||||
else:
|
||||
try:
|
||||
return re.compile(self.hide_referrer_regex)
|
||||
except re.error:
|
||||
# Regexes are validated in the form, but this is an important
|
||||
# fallback to prevent form validation and malformed source
|
||||
# data from causing all service pages to error
|
||||
return re.compile(r".^")
|
||||
|
||||
def get_daily_stats(self):
|
||||
return self.get_core_stats(
|
||||
start_time=timezone.now() - timezone.timedelta(days=1)
|
||||
@@ -95,12 +142,17 @@ class Service(models.Model):
|
||||
.order_by("-count")
|
||||
)
|
||||
|
||||
referrers = (
|
||||
hits.filter(initial=True)
|
||||
.values("referrer")
|
||||
.annotate(count=models.Count("referrer"))
|
||||
.order_by("-count")
|
||||
)
|
||||
referrer_ignore = self.get_ignored_referrer_regex()
|
||||
referrers = [
|
||||
referrer
|
||||
for referrer in (
|
||||
hits.filter(initial=True)
|
||||
.values("referrer")
|
||||
.annotate(count=models.Count("referrer"))
|
||||
.order_by("-count")
|
||||
)
|
||||
if not referrer_ignore.match(referrer["referrer"])
|
||||
]
|
||||
|
||||
countries = (
|
||||
sessions.values("country")
|
||||
@@ -130,12 +182,6 @@ class Service(models.Model):
|
||||
.order_by("-count")
|
||||
)
|
||||
|
||||
device_types = (
|
||||
sessions.values("device_type")
|
||||
.annotate(count=models.Count("device_type"))
|
||||
.order_by("-count")
|
||||
)
|
||||
|
||||
avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
|
||||
"load_time__avg"
|
||||
]
|
||||
@@ -189,7 +235,9 @@ class Service(models.Model):
|
||||
"session_chart_data": json.dumps(
|
||||
[
|
||||
{"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,
|
||||
|
||||
@@ -1,34 +1,62 @@
|
||||
from allauth.account.admin import EmailAddress
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import Service, User
|
||||
from allauth.account.admin import EmailAddress
|
||||
|
||||
|
||||
class ServiceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = ["name", "link", "respect_dnt", "collaborators"]
|
||||
fields = [
|
||||
"name",
|
||||
"link",
|
||||
"respect_dnt",
|
||||
"collect_ips",
|
||||
"ignored_ips",
|
||||
"ignore_robots",
|
||||
"hide_referrer_regex",
|
||||
"origins",
|
||||
"collaborators",
|
||||
"script_inject"
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"origins": forms.TextInput(),
|
||||
"ignored_ips": forms.TextInput(),
|
||||
"respect_dnt": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
|
||||
"collect_ips": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
|
||||
"ignore_robots": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
|
||||
"hide_referrer_regex": forms.TextInput(),
|
||||
"script_inject": forms.Textarea(attrs={'class':'font-mono', 'rows': 5})
|
||||
}
|
||||
labels = {
|
||||
"origins": "Allowed Hostnames",
|
||||
"origins": "Allowed origins",
|
||||
"respect_dnt": "Respect DNT",
|
||||
"collect_ips": "Collect IP addresses",
|
||||
"ignored_ips": "Ignored IP addresses",
|
||||
"ignore_robots": "Ignore robots",
|
||||
"hide_referrer_regex": "Hide specific referrers",
|
||||
"script_inject": "Additional injected JS",
|
||||
}
|
||||
help_texts = {
|
||||
"name": _("What should the service be called?"),
|
||||
"link": _("What's the service's primary URL?"),
|
||||
"origins": _(
|
||||
"At what hostnames does the service operate? This sets CORS headers, so use '*' if you're not sure (or don't care)."
|
||||
"At what origins does the service operate? Use commas to separate multiple values. This sets CORS headers, so use '*' if you're not sure (or don't care)."
|
||||
),
|
||||
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?",
|
||||
"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').",
|
||||
"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.",
|
||||
}
|
||||
|
||||
collaborators = forms.CharField(help_text="Which users should have read-only access to this service? (Comma separated list of emails.)", required=False)
|
||||
|
||||
collaborators = forms.CharField(
|
||||
help_text="Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)",
|
||||
required=False,
|
||||
)
|
||||
|
||||
def clean_collaborators(self):
|
||||
collaborators = []
|
||||
@@ -36,7 +64,9 @@ class ServiceForm(forms.ModelForm):
|
||||
email = collaborator_email.strip()
|
||||
if email == "":
|
||||
continue
|
||||
collaborator_email_linked = EmailAddress.objects.filter(email__iexact=email).first()
|
||||
collaborator_email_linked = EmailAddress.objects.filter(
|
||||
email__iexact=email
|
||||
).first()
|
||||
if collaborator_email_linked is None:
|
||||
raise forms.ValidationError(f"Email '{email}' is not registered")
|
||||
collaborators.append(collaborator_email_linked.user)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from datetime import time, datetime
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
|
||||
0
shynet/dashboard/static/dashboard/js/base.js
Normal file
0
shynet/dashboard/static/dashboard/js/base.js
Normal file
16
shynet/dashboard/tasks.py
Normal file
16
shynet/dashboard/tasks.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from celery import shared_task
|
||||
from django.core import mail
|
||||
from django.conf import settings
|
||||
import html2text
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_email(to: [str], subject: str, content: str, from_email: str = None):
|
||||
text_content = html2text.html2text(content)
|
||||
mail.send_mail(
|
||||
subject,
|
||||
text_content,
|
||||
from_email or settings.DEFAULT_FROM_EMAIL,
|
||||
to,
|
||||
html_message=content,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load static rules %}
|
||||
{% load static rules helpers %}
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
@@ -7,24 +7,24 @@
|
||||
<title>{% block head_title %}Privacy-oriented analytics{% endblock %} | {{request.site.name}}</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% include 'a17t/head.html' %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/litepicker@1.2.0/dist/js/main.js"
|
||||
integrity="sha256-mOlCEHUNWZPYIrc5OFL4Ab2rsJGzIPld3cy1ok7Cfx0=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.18.1/dist/apexcharts.min.js"
|
||||
integrity="sha256-RalQXBZdisB04aaBsm+6YZ0b/iRYjX1MZn90m19AnCY=" crossorigin="anonymous"></script>
|
||||
{% include 'a17t/includes/head.html' %}
|
||||
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
|
||||
<script src="{% static 'litepicker/dist/js/main.js' %}"></script>
|
||||
<script src="{% static 'turbolinks/dist/turbolinks.js' %}"></script>
|
||||
<script src="{% static 'dashboard/js/base.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
|
||||
{% block extra_head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-200 min-h-full">
|
||||
<body class="bg-neutral-200 min-h-full">
|
||||
{% block body %}
|
||||
|
||||
<section class="max-w-screen-xl mx-auto px-4 md:px-0 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">
|
||||
<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">
|
||||
<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-2x text-purple-600 md:hidden"></i>
|
||||
<i class="fas fa-binoculars fa-3x text-urge-600 hidden md:block"></i>
|
||||
<i class="fas fa-binoculars fa-2x text-urge-600 md:hidden"></i>
|
||||
</a>
|
||||
|
||||
<button class="button ~neutral !low md:hidden"
|
||||
@@ -35,7 +35,7 @@
|
||||
</button>
|
||||
<hr class="sep h-4 md:h-8 w-full">
|
||||
<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 %}
|
||||
<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 %}
|
||||
{% 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 %}
|
||||
|
||||
{% url 'account_email' as url %}
|
||||
@@ -93,6 +93,10 @@
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="Sign Up" url=url %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<hr class="sep h-8">
|
||||
|
||||
{% sidebar_footer %}
|
||||
</div>
|
||||
</aside>
|
||||
<div class="md:w-10/12">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
|
||||
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
|
||||
</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>
|
||||
:root {
|
||||
--litepickerMonthButtonHover: var(--color-urge);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{% load a17t_tags %}
|
||||
|
||||
{{form.name|a17t}}
|
||||
{{form.link|a17t}}
|
||||
{{form.collaborators|a17t}}
|
||||
|
||||
<details {% if form.errors %}open{% endif %}>
|
||||
<summary class="cursor-pointer text-sm">Advanced settings</summary>
|
||||
<hr class="sep h-4">
|
||||
{{form.respect_dnt|a17t}}
|
||||
{{form.collect_ips|a17t}}
|
||||
{{form.ignored_ips|a17t}}
|
||||
{{form.ignore_robots|a17t}}
|
||||
{{form.hide_referrer_regex|a17t}}
|
||||
{{form.origins|a17t}}
|
||||
{{form.script_inject|a17t}}
|
||||
</details>
|
||||
@@ -4,7 +4,7 @@
|
||||
{% with stats=object.stats %}
|
||||
<div class="p-4 md:flex justify-between">
|
||||
<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}}
|
||||
</h3>
|
||||
{% include 'dashboard/includes/stats_status_chip.html' %}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<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}}
|
||||
{% if session.is_currently_active %}
|
||||
<span class="badge ~positive">Online</span>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="pl-2">
|
||||
<p class="support text-gray-600">
|
||||
<a href="https://github.com/milesmcc/shynet">Shynet {{version}}</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div>
|
||||
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-gray-100{% endif %}"
|
||||
href="{{url}}">{{label}}</a>
|
||||
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %}"
|
||||
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{label}}</a>
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
{% has_perm "core.create_service" user as 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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
|
||||
{% has_perm 'core.change_service' user object as can_update %}
|
||||
{% if can_update %}
|
||||
<a href="{% url 'dashboard:service_update' service.uuid %}" class="button field ~neutral w-auto">Manage →</a>
|
||||
<a href="{% url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage →</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
<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="">
|
||||
<p class="label text-gray-400">Sessions</p>
|
||||
<p class="heading">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="p-4">
|
||||
{{form|a17t}}
|
||||
{% include 'dashboard/includes/service_form.html' %}
|
||||
</div>
|
||||
<div class="section ~urge !normal p-4">
|
||||
<button type="submit" class="button ~urge !high">Create</button>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block head_title %}{{object.name}} Session{% endblock %}
|
||||
|
||||
{% block service_actions %}
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral w-auto">Analytics →</a>
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
@@ -59,8 +59,8 @@
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Time Zone</p>
|
||||
<p class="label">{{session.time_zone|default:"Unknown"}}</p>
|
||||
<p>IP</p>
|
||||
<p class="label" title="{{session.ip}}">{{session.ip|default:"Not Collected"|truncatechars:"16"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{% block service_actions %}
|
||||
<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 →</a>
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral bg-neutral-000 w-auto">Analytics →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block head_title %}{{object.name}} Management{% endblock %}
|
||||
|
||||
{% block service_actions %}
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral w-auto">View →</a>
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">View →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
@@ -14,8 +14,8 @@
|
||||
<p>Place the following snippet at the end of the <code><body></code> tag on any page you'd like to track.</p>
|
||||
<div class="card ~neutral !high font-mono text-sm">
|
||||
{% filter force_escape %}<noscript><img
|
||||
src="//{{request.site.domain}}{% url 'ingress:endpoint_pixel' object.uuid %}"></noscript>
|
||||
<script src="//{{request.site.domain}}{% url 'ingress:endpoint_script' object.uuid %}"></script>
|
||||
src="{{script_protocol}}{{request.site.domain}}{% url 'ingress:endpoint_pixel' object.uuid %}"></noscript>
|
||||
<script src="{{script_protocol}}{{request.site.domain}}{% url 'ingress:endpoint_script' object.uuid %}"></script>
|
||||
{% endfilter %}
|
||||
</div>
|
||||
<hr class="sep h-4">
|
||||
@@ -23,7 +23,7 @@
|
||||
<form class="card ~neutral !low p-0" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="p-4">
|
||||
{{form|a17t}}
|
||||
{% include 'dashboard/includes/service_form.html' %}
|
||||
</div>
|
||||
<div class="section ~neutral !normal p-4 flex justify-between">
|
||||
<div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from urllib.parse import urlparse
|
||||
import pycountry
|
||||
|
||||
import flag
|
||||
import pycountry
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import SafeString
|
||||
@@ -41,12 +43,7 @@ def country_name(isocode):
|
||||
|
||||
@register.simple_tag
|
||||
def relative_stat_tone(
|
||||
start,
|
||||
end,
|
||||
good="UP",
|
||||
good_classes=None,
|
||||
bad_classes=None,
|
||||
neutral_classes=None,
|
||||
start, end, good="UP", good_classes=None, bad_classes=None, neutral_classes=None,
|
||||
):
|
||||
good_classes = good_classes or "~positive"
|
||||
bad_classes = bad_classes or "~critical"
|
||||
@@ -85,6 +82,11 @@ def percent_change_display(start, end):
|
||||
return SafeString(direction + pct_change)
|
||||
|
||||
|
||||
@register.inclusion_tag("dashboard/includes/sidebar_footer.html")
|
||||
def sidebar_footer():
|
||||
return {"version": settings.VERSION}
|
||||
|
||||
|
||||
@register.inclusion_tag("dashboard/includes/stat_comparison.html")
|
||||
def compare(
|
||||
start,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, reverse
|
||||
from django.utils import timezone
|
||||
@@ -77,6 +79,21 @@ class ServiceUpdateView(
|
||||
def get_success_url(self):
|
||||
return reverse("dashboard:service", kwargs={"pk": self.object.uuid})
|
||||
|
||||
def form_valid(self, *args, **kwargs):
|
||||
resp = super().form_valid(*args, **kwargs)
|
||||
cache.set(
|
||||
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
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
data = super().get_context_data(*args, **kwargs)
|
||||
data["script_protocol"] = "https://" if settings.SCRIPT_USE_HTTPS else "http://"
|
||||
return data
|
||||
|
||||
|
||||
class ServiceDeleteView(
|
||||
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView
|
||||
@@ -122,6 +139,9 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
||||
context_object_name = "session"
|
||||
permission_required = "core.view_service"
|
||||
|
||||
def get_permission_object(self, **kwargs):
|
||||
return self.get_object().service
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk"))
|
||||
|
||||
7
shynet/entrypoint.sh
Executable file
7
shynet/entrypoint.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ ! $PERFORM_CHECKS_AND_SETUP == False ]]; then
|
||||
./startup_checks.sh && exec ./webserver.sh
|
||||
else
|
||||
exec ./webserver.sh
|
||||
fi
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
Django settings for shynet project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.2.5.
|
||||
Django settings for Shynet.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/topics/settings/
|
||||
@@ -12,9 +10,16 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
|
||||
import os
|
||||
|
||||
# import module sys to get the type of exception
|
||||
import sys
|
||||
import urllib.parse as urlparse
|
||||
|
||||
# Messages
|
||||
from django.contrib.messages import constants as messages
|
||||
|
||||
# Increment on new releases
|
||||
VERSION = "v0.6.3"
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -26,7 +31,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "onlyusethisindev")
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.getenv("DEBUG", "True") == "True"
|
||||
DEBUG = os.getenv("DEBUG", "False") == "True"
|
||||
|
||||
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",")
|
||||
|
||||
@@ -42,6 +47,9 @@ INSTALLED_APPS = [
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.humanize",
|
||||
"health_check",
|
||||
"health_check.db",
|
||||
"health_check.cache",
|
||||
"rules.apps.AutodiscoverRulesConfig",
|
||||
"a17t",
|
||||
"core",
|
||||
@@ -104,9 +112,31 @@ else:
|
||||
"PASSWORD": os.environ.get("DB_PASSWORD"),
|
||||
"HOST": os.environ.get("DB_HOST"),
|
||||
"PORT": os.environ.get("DB_PORT"),
|
||||
"OPTIONS": {"connect_timeout": 5},
|
||||
}
|
||||
}
|
||||
|
||||
# Solution to removal of Heroku DB Injection
|
||||
if "DATABASE_URL" in os.environ:
|
||||
if "DATABASES" not in locals():
|
||||
DATABASES = {}
|
||||
url = urlparse.urlparse(os.environ["DATABASE_URL"])
|
||||
|
||||
# Ensure default database exists.
|
||||
DATABASES["default"] = DATABASES.get("default", {})
|
||||
|
||||
# Update with environment configuration.
|
||||
DATABASES["default"].update(
|
||||
{
|
||||
"NAME": url.path[1:],
|
||||
"USER": url.username,
|
||||
"PASSWORD": url.password,
|
||||
"HOST": url.hostname,
|
||||
"PORT": url.port,
|
||||
}
|
||||
)
|
||||
if url.scheme == "postgres":
|
||||
DATABASES["default"]["ENGINE"] = "django.db.backends.postgresql_psycopg2"
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
@@ -175,6 +205,11 @@ USE_TZ = True
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = "compiledstatic/"
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
STATICFILES_FINDERS = [
|
||||
"npm.finders.NpmFinder",
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
]
|
||||
|
||||
# Redis
|
||||
if not DEBUG and os.getenv("REDIS_CACHE_LOCATION") is not None:
|
||||
@@ -206,6 +241,7 @@ ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = ""
|
||||
ACCOUNT_USER_DISPLAY = lambda k: k.email
|
||||
ACCOUNT_SIGNUPS_ENABLED = os.getenv("ACCOUNT_SIGNUPS_ENABLED", "False") == "True"
|
||||
ACCOUNT_EMAIL_VERIFICATION = os.getenv("ACCOUNT_EMAIL_VERIFICATION", "none")
|
||||
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
@@ -213,7 +249,7 @@ SITE_ID = 1
|
||||
|
||||
# Celery
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = os.getenv("CELERY_TASK_ALWAYS_EAGER", "False") == "True"
|
||||
CELERY_TASK_ALWAYS_EAGER = os.getenv("CELERY_TASK_ALWAYS_EAGER", "True") == "True"
|
||||
|
||||
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL")
|
||||
CELERY_REDIS_SOCKET_TIMEOUT = 15
|
||||
@@ -245,6 +281,20 @@ else:
|
||||
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
|
||||
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
|
||||
|
||||
# Can everyone create services, or only superusers?
|
||||
@@ -256,3 +306,11 @@ ONLY_SUPERUSERS_CREATE = os.getenv("ONLY_SUPERUSERS_CREATE", "True") == "True"
|
||||
# Should the script use HTTPS to send the POST requests? The hostname is from
|
||||
# the django SITE default. (Edit it using the admin panel.)
|
||||
SCRIPT_USE_HTTPS = os.getenv("SCRIPT_USE_HTTPS", "True") == "True"
|
||||
|
||||
# How frequently should the tracking script "phone home" with a heartbeat, in
|
||||
# milliseconds?
|
||||
SCRIPT_HEARTBEAT_FREQUENCY = int(os.getenv("SCRIPT_HEARTBEAT_FREQUENCY", "5000"))
|
||||
|
||||
# How much time can elapse between requests from the same user before a new
|
||||
# session is created, in seconds?
|
||||
SESSION_MEMORY_TIMEOUT = int(os.getenv("SESSION_MEMORY_TIMEOUT", "1800"))
|
||||
|
||||
@@ -21,5 +21,6 @@ urlpatterns = [
|
||||
path("accounts/", include("allauth.urls")),
|
||||
path("ingress/", include(("analytics.ingress_urls", "ingress")), name="ingress"),
|
||||
path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")),
|
||||
path("healthz/", include("health_check.urls")),
|
||||
path("", include(("core.urls", "core"), namespace="core")),
|
||||
]
|
||||
|
||||
10
shynet/ssl.webserver.sh
Executable file
10
shynet/ssl.webserver.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start Gunicorn processes
|
||||
echo Launching Shynet web server...
|
||||
exec gunicorn shynet.wsgi:application \
|
||||
--bind 0.0.0.0:${PORT:-8080} \
|
||||
--workers ${NUM_WORKERS:-1} \
|
||||
--timeout 100 \
|
||||
--certfile=cert.pem \
|
||||
--keyfile=privkey.pem
|
||||
24
shynet/startup_checks.sh
Executable file
24
shynet/startup_checks.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Check if setup is necessary, do setup as needed
|
||||
echo "Performing startup checks..."
|
||||
startup_results=( $(./manage.py startup_checks) )
|
||||
if [[ ${startup_results[0]} == True ]]; then
|
||||
echo "Running migrations (setting up DB)..."
|
||||
{
|
||||
./manage.py migrate && echo "Migrations complete!"
|
||||
} || {
|
||||
echo "Migrations failed, exiting" && exit 1
|
||||
}
|
||||
else
|
||||
echo "Database is ready to go."
|
||||
fi
|
||||
if [[ ${startup_results[1]} == True ]]; then
|
||||
echo "Warning: no admin user available. Consult docs for instructions."
|
||||
fi
|
||||
if [[ ${startup_results[2]} == True ]]; then
|
||||
echo "Warning: Shynet's hostname is not set. The script won't work correctly. Consult docs for instructions."
|
||||
fi
|
||||
if [[ ${startup_results[3]} == True ]]; then
|
||||
echo "Warning: Shynet's whitelabel is not set. Consult docs for instructions."
|
||||
fi
|
||||
echo "Startup checks complete!"
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start Gunicorn processes
|
||||
echo Launching Shynet web server...
|
||||
exec gunicorn shynet.wsgi:application \
|
||||
--bind 0.0.0.0:8080 \
|
||||
--workers 3 \
|
||||
--bind 0.0.0.0:${PORT:-8080} \
|
||||
--workers ${NUM_WORKERS:-1} \
|
||||
--timeout 100
|
||||
13
tests/js.html
Normal file
13
tests/js.html
Normal 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>
|
||||
@@ -7,8 +7,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript><img src="//localhost:8000/ingress/211410e6-b401-4fe8-8740-7926368590be/pixel.gif"></noscript>
|
||||
<script src="//localhost:8000/ingress/211410e6-b401-4fe8-8740-7926368590be/script.js"></script>
|
||||
<img src="http://localhost:8000/ingress/9b2c4e2f-8d29-4418-82d4-b68e06795025/pixel.gif">
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user