Compare commits
54 Commits
first-test
...
api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e48e2dcf5 | ||
|
|
b286c80754 | ||
|
|
c23f44d7b7 | ||
|
|
b7f2e9cfe6 | ||
|
|
77cb1fb37c | ||
|
|
d9bbeea892 | ||
|
|
ca97453c3e | ||
|
|
b87b158aab | ||
|
|
4a6af18765 | ||
|
|
6d84f63130 | ||
|
|
ba91ed561d | ||
|
|
2aaadfe81c | ||
|
|
7f60b3abff | ||
|
|
069b218828 | ||
|
|
80647d960a | ||
|
|
364ef115c9 | ||
|
|
71ec196ec4 | ||
|
|
4834c5722f | ||
|
|
4b4c8f207e | ||
|
|
aed88b7c9a | ||
|
|
bcf94147c9 | ||
|
|
66b841fd86 | ||
|
|
d809ec82d9 | ||
|
|
e577aa4997 | ||
|
|
5966ea2f84 | ||
|
|
a7248cd54b | ||
|
|
1dec03c724 | ||
|
|
ff6933b4de | ||
|
|
1fd46b019c | ||
|
|
e534269c77 | ||
|
|
0d64ef33b0 | ||
|
|
56c82e7d23 | ||
|
|
c71d934c67 | ||
|
|
85ae56fcdb | ||
|
|
cd422ffd71 | ||
|
|
060a9b2a96 | ||
|
|
8d13ccd0fd | ||
|
|
0d46e6d1f4 | ||
|
|
81ae84efb3 | ||
|
|
8302aedaa7 | ||
|
|
e2d438134a | ||
|
|
787ce1775f | ||
|
|
ea5f58fbd3 | ||
|
|
4079a8494a | ||
|
|
780b71083a | ||
|
|
62fbb014e7 | ||
|
|
d62d48c7b4 | ||
|
|
2f8891a843 | ||
|
|
a963694fd0 | ||
|
|
90b2896ded | ||
|
|
bec4b19366 | ||
|
|
32adb64dc0 | ||
|
|
53bc690435 | ||
|
|
04120323a6 |
7
.github/workflows/build-docker-edge.yml
vendored
7
.github/workflows/build-docker-edge.yml
vendored
@@ -9,6 +9,11 @@ jobs:
|
||||
publish_to_docker_hub:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set swap space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 5
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -38,6 +43,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.prep.outputs.tags }}
|
||||
|
||||
7
.github/workflows/build-docker-manual.yml
vendored
7
.github/workflows/build-docker-manual.yml
vendored
@@ -9,6 +9,11 @@ jobs:
|
||||
publish_to_docker_hub:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set swap space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 5
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -38,6 +43,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.prep.outputs.tags }}
|
||||
|
||||
7
.github/workflows/build-docker-release.yml
vendored
7
.github/workflows/build-docker-release.yml
vendored
@@ -9,6 +9,11 @@ jobs:
|
||||
publish_to_docker_hub:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set swap space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 5
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -39,6 +44,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.prep.outputs.tags }}
|
||||
|
||||
37
.github/workflows/run-tests.yml
vendored
Normal file
37
.github/workflows/run-tests.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Run tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
db:
|
||||
image: postgres:12.3-alpine
|
||||
env:
|
||||
POSTGRES_USER: shynet_db_user
|
||||
POSTGRES_PASSWORD: shynet_db_user_password
|
||||
POSTGRES_DB: shynet_db
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Run image
|
||||
uses: abatilo/actions-poetry@v2.0.0
|
||||
with:
|
||||
poetry-version: 1.1.6
|
||||
- name: Install dependencies
|
||||
run: poetry install
|
||||
|
||||
- name: Django Testing project
|
||||
run: |
|
||||
cp TEMPLATE.env .env
|
||||
poetry run ./shynet/manage.py test
|
||||
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Contributing
|
||||
|
||||
This document provides an overview of how to contribute to Shynet. Currently, it focuses on the more technical elements of contributing --- for example, setting up your development environment. Eventually, we will expand this guide to cover the social and governance oriented side of contributing as well.
|
||||
|
||||
## Setting up your development environment
|
||||
|
||||
To contribute to Shynet, you must have a reliable development environment. Because Shynet is intended to be run inside containers, we strongly encourage you to run Shynet in a container in development as well. The development setup described in this guide will use Docker and Docker Compose.
|
||||
|
||||
To begin, clone the Shynet repository to your computer, and ensure that you have Docker and Docker Compose installed.
|
||||
|
||||
Copy `TEMPLATE.env` to a new file called `.env`. This `.env` file will be used in your development environment. Paste `DEBUG=True` into the end of your new `.env` file so that Shynet will know to run in development mode.
|
||||
|
||||
Finally, follow the steps in [GUIDE.md](GUIDE.md) on setting up a Shynet instance with Docker Compose. This is where you'll setup an admin user.
|
||||
|
||||
_Did you have to perform additional steps to setup your environment? Document them here and submit a pull request!_
|
||||
@@ -47,4 +47,5 @@ RUN python manage.py collectstatic --noinput && \
|
||||
# Launch
|
||||
USER appuser
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK CMD bash -c 'wget -o /dev/null -O /dev/null --header "Host: ${ALLOWED_HOSTS%%,*}" "http://127.0.0.1:$PORT/healthz/?format=json"'
|
||||
CMD [ "./entrypoint.sh" ]
|
||||
|
||||
57
GUIDE.md
57
GUIDE.md
@@ -7,7 +7,6 @@
|
||||
- [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)
|
||||
@@ -23,7 +22,7 @@
|
||||
|
||||
## Installation
|
||||
|
||||
Installation of Shynet is easy! Follow the [Basic Installation](#basic-installation) guide or the [Basic Installation with Docker Compose](#basic-installation-with-docker-compose) below for a minimal installation, or if you are going to be running Shynet over HTTPS through a reverse proxy. If you'd like to run Shynet over HTTPS without a reverse proxy, skip ahead to [Installation with SSL](#installation-with-ssl) instead.
|
||||
Installation of Shynet is easy! Follow the [Basic Installation](#basic-installation) guide or the [Basic Installation with Docker Compose](#basic-installation-with-docker-compose) below for a minimal installation, or if you are going to be running Shynet over HTTPS through a reverse proxy.
|
||||
|
||||
> **These commands assume Ubuntu.** If you're installing Shynet on a different platform, the process will be different.
|
||||
|
||||
@@ -35,7 +34,7 @@ Before continuing, please be sure to have the latest version of Docker installed
|
||||
|
||||
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.
|
||||
3. Configure an environment file for Shynet, using [this file](/TEMPLATE.env) as a template. (This file is typically named `.env`.) Make sure you set the database settings, or Shynet won't be able to run. Also consider setting `ALLOWED_HOSTS` inside the environment file to your deployment's domain for better security.
|
||||
|
||||
4. Launch the Shynet server for the first time by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. Provided you're using the default environment information (i.e., `PERFORM_CHECKS_AND_SETUP` is `True`), you'll see a few warnings about not having an admin user or host setup; these are normal. Don't worry — we'll do this in the next step. You only need to stop if you see a stacktrace about being unable to connect to the database.
|
||||
|
||||
@@ -56,7 +55,7 @@ Before continuing, please be sure to have the latest version of Docker installed
|
||||
|
||||
1. Clone the repository.
|
||||
|
||||
2. Using [TEMPLATE.env](/TEMPLATE.env) as a template, confiure the environment for your Shynet instance and place the modified config in a file called `.env` in the root of the repository. Do _not_ change the port number at the end; you can set the public facing port in the next step.
|
||||
2. Using [TEMPLATE.env](/TEMPLATE.env) as a template, configure the environment for your Shynet instance and place the modified config in a file called `.env` in the root of the repository. Do _not_ change the port number at the end; you can set the public facing port in the next step.
|
||||
|
||||
3. On line 2 of the `nginx.conf` file located in the root of the repository, replace `example.com` with your hostname. Then, in the `docker-compose.yml` file, set the port number by replacing `8080` in line 38 ( `- 8080:80` ) with whatever local port you want to bind it to. For example, set the port number to `- 80:80` if you want your site will be available via HTTP (port 80) at `http://<your hostname>`.
|
||||
|
||||
@@ -96,40 +95,6 @@ See the [Render docs](https://render.com/docs/deploy-shynet) for more informatio
|
||||
|
||||
## 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/)!
|
||||
@@ -243,6 +208,22 @@ In a single-page application, the page never reloads. (That's the entire point o
|
||||
|
||||
Fortunately, Shynet offers a simple method you can call from anywhere within your JavaScript to indicate that a new page has been loaded: `Shynet.newPageLoad()`. Add this method call to the code that handles routing in your app, and you'll be ready to go.
|
||||
|
||||
|
||||
### API
|
||||
|
||||
All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/v1/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's parsonal API token (```'Authorization: Token <user API token>'```).
|
||||
|
||||
There are 3 optional query parameters:
|
||||
* `uuid` - to get data only from one service
|
||||
* `startDate` - to set start date in format YYYY-MM-DD
|
||||
* `endDate` - to set end date in format YYYY-MM-DD
|
||||
|
||||
Example in HTTPie:
|
||||
```http get '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{user_api_token}}'```
|
||||
|
||||
Example in cURL:
|
||||
```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01'```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<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 align="center"><a href="#screenshots">Screenshots</a> • <a href="#features">Features</a> • <a href="https://miles.land/officehours/">Office Hours</a></p>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -17,7 +17,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: "shynet-webserver"
|
||||
image: "milesmcc/shynet:latest"
|
||||
image: "milesmcc/shynet:edge" # Change to the version appropriate for you (e.g., :latest)
|
||||
imagePullPolicy: Always
|
||||
envFrom:
|
||||
- secretRef:
|
||||
@@ -42,7 +42,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: "shynet-celeryworker"
|
||||
image: "milesmcc/shynet:latest"
|
||||
image: "milesmcc/shynet:edge" # Change to the version appropriate for you (e.g., :latest)
|
||||
command: ["./celeryworker.sh"]
|
||||
imagePullPolicy: Always
|
||||
envFrom:
|
||||
@@ -95,7 +95,7 @@ spec:
|
||||
selector:
|
||||
app: shynet-webserver
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: shynet-webserver-ingress
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -4,6 +4,7 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "shynet",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
@@ -160,8 +161,7 @@
|
||||
"esprima": "^3.1.3",
|
||||
"estraverse": "^4.2.0",
|
||||
"esutils": "^2.0.2",
|
||||
"optionator": "^0.8.1",
|
||||
"source-map": "~0.6.1"
|
||||
"optionator": "^0.8.1"
|
||||
},
|
||||
"bin": {
|
||||
"escodegen": "bin/escodegen.js",
|
||||
@@ -367,9 +367,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
@@ -480,8 +480,7 @@
|
||||
"esprima": "^4.0.1",
|
||||
"estraverse": "^4.2.0",
|
||||
"esutils": "^2.0.2",
|
||||
"optionator": "^0.8.1",
|
||||
"source-map": "~0.6.1"
|
||||
"optionator": "^0.8.1"
|
||||
},
|
||||
"bin": {
|
||||
"escodegen": "bin/escodegen.js",
|
||||
@@ -993,9 +992,9 @@
|
||||
}
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
},
|
||||
"prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
|
||||
1303
poetry.lock
generated
1303
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ PyYAML = "^5.4.1"
|
||||
user-agents = "^2.2.0"
|
||||
rules = "^3.0"
|
||||
gunicorn = "^20.1.0"
|
||||
psycopg2-binary = "^2.9.1"
|
||||
psycopg2-binary = "^2.9.2"
|
||||
redis = "^3.5.3"
|
||||
django-redis-cache = "^3.0.0"
|
||||
pycountry = "^20.7.3"
|
||||
@@ -26,6 +26,15 @@ django-health-check = "^3.16.4"
|
||||
django-npm = "^1.0.0"
|
||||
python-dotenv = "^0.18.0"
|
||||
django-debug-toolbar = "^3.2.1"
|
||||
django-cors-headers = "^3.11.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest-sugar = "^0.9.4"
|
||||
factory-boy = "^3.2.0"
|
||||
pytest-django = "^4.4.0"
|
||||
django-coverage-plugin = "^2.0.0"
|
||||
django-stubs = "^1.8.0"
|
||||
mypy = "^0.910"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
@@ -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 bg-neutral-000 w-auto mr-1">Previous</a>
|
||||
<a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto mr-1">Previous</a>
|
||||
{% else %}
|
||||
<a class="button field bg-neutral-000 w-auto mr-1" disabled>Previous</a>
|
||||
<a class="button field !low bg-neutral-000 w-auto mr-1" disabled>Previous</a>
|
||||
{% endif %}
|
||||
{% if page.has_next %}
|
||||
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button field bg-neutral-000 w-auto">Next</a>
|
||||
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto">Next</a>
|
||||
{% else %}
|
||||
<a class="button field bg-neutral-000 w-auto" disabled>Next</a>
|
||||
<a class="button field !low 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-neutral-700">{{ pnum }}</a></li>
|
||||
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% 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-neutral-700">{{ pnum }}</a></li>
|
||||
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% 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-neutral-700">{{ pnum }}</a></li>
|
||||
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
0
shynet/api/__init__.py
Normal file
0
shynet/api/__init__.py
Normal file
1
shynet/api/admin.py
Normal file
1
shynet/api/admin.py
Normal file
@@ -0,0 +1 @@
|
||||
# from django.contrib import admin
|
||||
6
shynet/api/apps.py
Normal file
6
shynet/api/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api'
|
||||
0
shynet/api/migrations/__init__.py
Normal file
0
shynet/api/migrations/__init__.py
Normal file
23
shynet/api/mixins.py
Normal file
23
shynet/api/mixins.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
from core.models import User
|
||||
|
||||
|
||||
class ApiTokenRequiredMixin:
|
||||
def _get_user_by_token(self, request):
|
||||
token = request.headers.get('Authorization')
|
||||
if not token or not token.startswith('Token '):
|
||||
return AnonymousUser()
|
||||
|
||||
token = token.split(' ')[1]
|
||||
user = User.objects.filter(api_token=token).first()
|
||||
|
||||
return user if user else AnonymousUser()
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
request.user = self._get_user_by_token(request)
|
||||
if not request.user.is_authenticated:
|
||||
return JsonResponse(data={}, status=403)
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
1
shynet/api/models.py
Normal file
1
shynet/api/models.py
Normal file
@@ -0,0 +1 @@
|
||||
# from django.db import models
|
||||
3
shynet/api/tests.py
Normal file
3
shynet/api/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
7
shynet/api/urls.py
Normal file
7
shynet/api/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("dashboard/", views.DashboardApiView.as_view(), name="services"),
|
||||
]
|
||||
60
shynet/api/views.py
Normal file
60
shynet/api/views.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import uuid
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Q
|
||||
from django.db.models.query import QuerySet
|
||||
from django.views.generic import View
|
||||
|
||||
from dashboard.mixins import DateRangeMixin
|
||||
from core.models import Service
|
||||
|
||||
from .mixins import ApiTokenRequiredMixin
|
||||
|
||||
|
||||
def is_valid_uuid(value):
|
||||
try:
|
||||
uuid.UUID(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
services = Service.objects.filter(
|
||||
Q(owner=request.user) | Q(collaborators__in=[request.user])
|
||||
).distinct()
|
||||
|
||||
uuid = request.GET.get('uuid')
|
||||
if uuid and is_valid_uuid(uuid):
|
||||
services = services.filter(uuid=uuid)
|
||||
|
||||
try:
|
||||
start = self.get_start_date()
|
||||
end = self.get_end_date()
|
||||
except ValueError:
|
||||
return JsonResponse(status=400, data={'error': 'Invalid date format'})
|
||||
|
||||
services_data = [
|
||||
{
|
||||
'name': s.name,
|
||||
'uuid': s.uuid,
|
||||
'link': s.link,
|
||||
'stats': s.get_core_stats(start, end),
|
||||
}
|
||||
for s in services
|
||||
]
|
||||
|
||||
services_data = self._convert_querysets_to_lists(services_data)
|
||||
|
||||
return JsonResponse(data={'services': services_data})
|
||||
|
||||
def _convert_querysets_to_lists(self, services_data):
|
||||
for service_data in services_data:
|
||||
for key, value in service_data['stats'].items():
|
||||
if isinstance(value, QuerySet):
|
||||
service_data['stats'][key] = list(value)
|
||||
for key, value in service_data['stats']['compare'].items():
|
||||
if isinstance(value, QuerySet):
|
||||
service_data['stats']['compare'][key] = list(value)
|
||||
|
||||
return services_data
|
||||
24
shynet/core/migrations/0009_auto_20211117_0217.py
Normal file
24
shynet/core/migrations/0009_auto_20211117_0217.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.5 on 2021-11-17 07:17
|
||||
|
||||
import core.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_auto_20200628_1403'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='api_token',
|
||||
field=models.TextField(default=core.models._default_api_token, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@@ -1,8 +1,9 @@
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from secrets import token_urlsafe
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
@@ -43,9 +44,14 @@ def _parse_network_list(networks: str):
|
||||
return [ipaddress.ip_network(network.strip()) for network in networks.split(",")]
|
||||
|
||||
|
||||
def _default_api_token():
|
||||
return token_urlsafe(32)
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
username = models.TextField(default=_default_uuid, unique=True)
|
||||
email = models.EmailField(unique=True)
|
||||
api_token = models.TextField(default=_default_api_token, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
@@ -203,6 +209,7 @@ class Service(models.Model):
|
||||
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
|
||||
sessions, hits, start_time, end_time, tz_now
|
||||
)
|
||||
|
||||
return {
|
||||
"currently_online": currently_online,
|
||||
"session_count": session_count,
|
||||
|
||||
@@ -45,11 +45,6 @@ class DateRangeMixin:
|
||||
"start": now.replace(day=1),
|
||||
"end": now,
|
||||
},
|
||||
{
|
||||
"name": "Last month",
|
||||
"start": now.replace(day=1, month=now.month - 1),
|
||||
"end": now.replace(day=1, month=now.month) - timezone.timedelta(days=1),
|
||||
},
|
||||
{
|
||||
"name": "This year",
|
||||
"start": now.replace(day=1, month=1),
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
|
||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Change Password" %}{% endblock %}
|
||||
{% block head_title %}{% trans "Change authentication info" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Change authentication info" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" action="{% url 'account_change_password' %}" class="password_change max-w-lg">
|
||||
@@ -11,4 +11,17 @@
|
||||
{{ form|a17t }}
|
||||
<button type="submit" name="action" class="button ~urge !high">{% trans "Change Password" %}</button>
|
||||
</form>
|
||||
<hr class="sep">
|
||||
<div>
|
||||
<p class="label mb-1">Personal API token</p>
|
||||
<div class="flex justify-between">
|
||||
<span class='chip ~info !normal'>{{request.user.api_token}}</span>
|
||||
<form method="POST" action="{% url 'dashboard:api_token_refresh' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="action" class="button ~neutral @high">{% trans "Refresh token" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="support mt-1">To learn more about the API, see our <a href="https://github.com/milesmcc/shynet/blob/master/GUIDE.md#api">API guide</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 bg-neutral-000 cursor-pointer" style="max-width: 200px;" readonly>
|
||||
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral !low bg-neutral-000 cursor-pointer" style="max-width: 200px;" readonly>
|
||||
<style>
|
||||
:root {
|
||||
--litepicker-button-prev-month-color-hover: var(--color-urge);
|
||||
|
||||
@@ -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 bg-neutral-000 w-auto">+ New Service</a>
|
||||
<a href="{% url 'dashboard:service_create' %}" class="button field !low bg-neutral-000 w-auto">+ New Service</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<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="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage →</a>
|
||||
<a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field !low bg-neutral-000 w-auto">Manage →</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block head_title %}{{object.name}} Session{% endblock %}
|
||||
|
||||
{% block service_actions %}
|
||||
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics →</a>
|
||||
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field !low bg-neutral-000 w-auto">Analytics →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{% block service_actions %}
|
||||
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
|
||||
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral bg-neutral-000 w-auto">Analytics →</a>
|
||||
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral !low 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 bg-neutral-000 w-auto">View →</a>
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field !low bg-neutral-000 w-auto">View →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
@@ -30,5 +30,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr class="sep h-4">
|
||||
<h5>API</h5>
|
||||
<div class="card ~neutral !low content">
|
||||
<p>Shynet provides a simple API that you can use to pull data programmatically. You can access this data via this URL:</p>
|
||||
<code class="text-sm">{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}</code>
|
||||
<p>
|
||||
There are 2 optional query parameters:
|
||||
<ul>
|
||||
<li><code class="text-sm">startDate</code> — to set the start date (in format YYYY-MM-DD)</li>
|
||||
<li><code class="text-sm">endDate</code> — to set the end date (in format YYYY-MM-DD)</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>Example using cURL:</p>
|
||||
<code class="text-sm">curl -H 'Authorization: Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01'</code>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
@@ -28,4 +26,9 @@ urlpatterns = [
|
||||
views.ServiceSessionView.as_view(),
|
||||
name="service_session",
|
||||
),
|
||||
path(
|
||||
"api-token-refresh/",
|
||||
views.RefreshApiTokenView.as_view(),
|
||||
name="api_token_refresh",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -3,8 +3,7 @@ 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
|
||||
from django.shortcuts import get_object_or_404, reverse, redirect
|
||||
from django.views.generic import (
|
||||
CreateView,
|
||||
DeleteView,
|
||||
@@ -12,11 +11,12 @@ from django.views.generic import (
|
||||
ListView,
|
||||
TemplateView,
|
||||
UpdateView,
|
||||
View,
|
||||
)
|
||||
from rules.contrib.views import PermissionRequiredMixin
|
||||
|
||||
from analytics.models import Session
|
||||
from core.models import Service
|
||||
from core.models import Service, _default_api_token
|
||||
|
||||
from .forms import ServiceForm
|
||||
from .mixins import DateRangeMixin
|
||||
@@ -155,3 +155,10 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk"))
|
||||
return data
|
||||
|
||||
|
||||
class RefreshApiTokenView(LoginRequiredMixin, View):
|
||||
def post(self, request):
|
||||
request.user.api_token = _default_api_token()
|
||||
request.user.save()
|
||||
return redirect('account_change_password')
|
||||
|
||||
@@ -22,7 +22,7 @@ from django.contrib.messages import constants as messages
|
||||
load_dotenv()
|
||||
|
||||
# Increment on new releases
|
||||
VERSION = "0.11.0"
|
||||
VERSION = "0.12.0"
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -59,16 +59,19 @@ INSTALLED_APPS = [
|
||||
"core",
|
||||
"dashboard.apps.DashboardConfig",
|
||||
"analytics",
|
||||
"api",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"debug_toolbar",
|
||||
"corsheaders",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
@@ -370,3 +373,6 @@ DASHBOARD_PAGE_SIZE = int(os.getenv("DASHBOARD_PAGE_SIZE", "5"))
|
||||
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = (
|
||||
os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True"
|
||||
)
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOW_METHODS = ["GET", "OPTIONS"]
|
||||
|
||||
@@ -25,4 +25,5 @@ urlpatterns = [
|
||||
path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")),
|
||||
path("healthz/", include("health_check.urls")),
|
||||
path("", include(("core.urls", "core"), namespace="core")),
|
||||
path("api/v1/", include(("api.urls", "api"), namespace="api")),
|
||||
]
|
||||
|
||||
16
tests/js.html
Normal file
16
tests/js.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>JS test</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<img src="http://localhost:8000/ingress/0ca733e8-c41f-462b-a11a-4ba0cea29948/pixel.gif">
|
||||
</noscript>
|
||||
<script defer src="http://localhost:8000/ingress/0ca733e8-c41f-462b-a11a-4ba0cea29948/script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
13
tests/pixel.html
Normal file
13
tests/pixel.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Pixel test</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<img src="http://localhost:8000/ingress/9b2c4e2f-8d29-4418-82d4-b68e06795025/pixel.gif">
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user