Compare commits

..

1 Commits

Author SHA1 Message Date
Filip Rysavy a337672e98 Test Sia user Docker image 2021-05-17 17:24:17 +02:00
528 changed files with 91843 additions and 1981 deletions

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @kwypchlo @meeh0w

View File

@ -1,6 +1,26 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: docker - package-ecosystem: npm
directory: "/docker/sia" directory: "/packages/handshake-api"
schedule: schedule:
interval: monthly interval: weekly
time: "10:00"
open-pull-requests-limit: 10
assignees:
- kwypchlo
- package-ecosystem: npm
directory: "/packages/health-check"
schedule:
interval: weekly
time: "10:00"
open-pull-requests-limit: 10
assignees:
- kwypchlo
- package-ecosystem: npm
directory: "/packages/website"
schedule:
interval: weekly
time: "10:00"
open-pull-requests-limit: 10
assignees:
- kwypchlo

View File

@ -0,0 +1,47 @@
name: Continous Integration
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 15.x
- name: Install dependencies
run: yarn
- name: "Static code analysis: handshake-api"
run: yarn workspace handshake-api prettier --check .
- name: "Static code analysis: health-check"
run: yarn workspace health-check prettier --check .
- name: "Static code analysis: website"
run: yarn workspace website prettier --check .
- name: "Static code analysis: dashboard"
run: yarn workspace dashboard prettier --check .
# - name: "Build webapp"
# run: yarn workspace webapp build
# env:
# GATSBY_API_URL: "https://siasky.net"
# - name: Cypress run
# uses: cypress-io/github-action@v2
# env:
# CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# working-directory: packages/webapp
# record: true
# start: npx http-server public --port 8000
# wait-on: "http://localhost:8000"
# - name: Cypress cache prune
# run: yarn cypress cache prune

46
.github/workflows/deploy-website.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Deploy website to Skynet
on:
pull_request:
paths:
- "packages/website/**"
defaults:
run:
working-directory: packages/website
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: 15.x
- name: Install dependencies
run: npm i --force
- name: "Build website"
run: npm run build
- name: "Integration tests"
uses: cypress-io/github-action@v2
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
working-directory: packages/website
install: false
record: true
start: npm run serve
wait-on: "http://127.0.0.1:9000"
- name: "Deploy to Skynet"
uses: kwypchlo/deploy-to-skynet-action@main
with:
upload-dir: packages/website/public
github-token: ${{ secrets.GITHUB_TOKEN }}
registry-seed: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && secrets.WEBSITE_REGISTRY_SEED || '' }}

View File

@ -1,37 +0,0 @@
name: Lint - Python Scripts
on:
push:
paths:
- "**.py"
jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.x"
architecture: x64
- run: pip install black
- run: black --check .
flake8:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.x"
architecture: x64
- run: pip install flake8
# E203: https://www.flake8rules.com/rules/E203.html - Whitespace before ':'
# E501: https://www.flake8rules.com/rules/E501.html - Line too long
# W503: https://www.flake8rules.com/rules/W503.html - Line break occurred before a binary operator
# W605: https://www.flake8rules.com/rules/W605.html - Invalid escape sequence
# E722: https://www.flake8rules.com/rules/E722.html - Do not use bare except, specify exception instead
- run: flake8 --max-line-length 88 --ignore E203,E501,W503,W605,E722

19
.gitignore vendored
View File

@ -53,6 +53,7 @@ typings/
# dotenv environment variable files # dotenv environment variable files
.env* .env*
./docker/kratos/config/kratos.yml
# Mac files # Mac files
.DS_Store .DS_Store
@ -67,6 +68,10 @@ yarn-error.log
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
# Cypress
packages/webapp/cypress/screenshots
packages/webapp/cypress/videos
# Docker data # Docker data
docker/data docker/data
@ -82,10 +87,12 @@ __pycache__
/.idea/ /.idea/
/venv* /venv*
# Luacov file # CockroachDB certificates
luacov.stats.out docker/cockroach/certs/*.crt
luacov.report.out docker/cockroach/certs/*.key
docker/kratos/cr_certs/*.crt
docker/kratos/cr_certs/*.key
# Setup-script log files # Oathkeeper JWKS signing token
setup-scripts/serverload.log docker/kratos/oathkeeper/id_token.jwks.json
setup-scripts/serverload.json /docker/kratos/config/kratos.yml

View File

@ -1,81 +0,0 @@
Version Scheme
--------------
Skynet Webportal uses the following versioning scheme, vX.X.X
- First Digit signifies a major (compatibility breaking) release
- Second Digit signifies a major (non compatibility breaking) release
- Third Digit signifies a minor or patch release
Version History
---------------
Latest:
## Mar 8, 2022:
### v0.1.4
**Key Updates**
- expose generic skylink serving endpoint on domain aliases
- Add abuse scanner service, activated by adding `u` to `PORTAL_MODULES`
- Add malware scanner service, activated by adding `s` to `PORTAL_MODULES`
- Remove ORY Kratos, ORY Oathkeeper, CockroachDB.
- Add `/serverload` endpoint for CPU usage and free disk space
**Bugs Fixed**
- Add missing servers and blocklist command to the manual blocklist script.
- fixed a bug when accessing file from skylink via subdomain with a filename that had escaped characters
- Fix `blocklist-skylink.sh` script that didn't removed blocked skylink from
nginx cache.
- fixed uploaded directory name (was "undefined" before)
- fixed empty directory upload progress (size was not calculated for directories)
**Other**
- add new critical health check that scans config and makes sure that all relevant configurations are set
- Add abuse report configuration
- Remove hardcoded Airtable default values from blocklist script. Portal
operators need to define their own values in portal common config (LastPass).
- Add health check for the blocker container
- Drop `Skynet-Requested-Skylink` header
- Dump disk space usage when health-checker script disables portal due to
critical free disk space.
- Enable the accounting module for skyd
- Add link to supported setup process in Gitbook.
- Set `min_free` parameter on the `proxy_cache_path` directive to `100g`
- Parameterize MongoDB replicaset in `docker-compose.mongodb.yml` via
`SKYNET_DB_REPLICASET` from `.env` file.
- Hot reload Nginx after pruning cache files.
- Added script to prune nginx cache.
- Remove hardcoded server list from `blocklist-skylink.sh` so it removes server
list duplication and can also be called from Ansible.
- Remove outdated portal setup documentation and point to developer docs.
- Block skylinks in batches to improve performance.
- Add trimming Airtable skylinks from Takedown Request table.
- Update handshake to use v3.0.1
## Oct 18, 2021:
### v0.1.3
**Key Updates**
- Change skyd 307 redirect code to 308
- Set caddy dns entry ttl limit to 15 minutes to remove stranded entries.
- Set skyd up to connect to the local mongodb cluster for storing TUS metadata
- Update health check disable command to require reason.
- Move MongoDB to a separate service (use `PORTAL_MODULES=m` to use it without accounts)
- Add proper handling for options response on /skynet/tus endpoint
- added unpinning skylinks from account dashboard
**Bugs Fixed**
- include tus header upload-concat in cors requests
- fixed issue with caddy requesting new certificates instead of using existing ones from file storage
- fixed the latest news link redirect in the news header
- Fix extended checks error by rounding the reported datetime.
**Other**
- Remove outdated references to NebulousLabs
## August 9th, 2021:
### v0.1.1
Monthly release
## March 24th, 2021:
### v0.1.0
Initial versioned release

189
README.md
View File

@ -1,18 +1,191 @@
# Skynet Portal # Skynet Portal
## Latest Setup Documentation ## Web application
Latest Skynet Webportal setup documentation and the setup process Skynet Labs Use `yarn workspace webapp start` to start the development server.
supports is located at https://portal-docs.skynetlabs.com/.
Some scripts and setup documentation contained in this repository Use `yarn workspace webapp build` to compile the application to `/public` directory.
(`skynet-webportal`) may be outdated and generally should not be used.
You can use the below build parameters to customize your web application.
- development example `GATSBY_API_URL=https://siasky.dev yarn workspace webapp start`
- production example `GATSBY_API_URL=https://siasky.net yarn workspace webapp build`
List of available parameters:
- `GATSBY_API_URL`: override api url (defaults to location origin)
## License ## License
Skynet uses a custom [License](./LICENSE.md). The Skynet License is a source code license that allows you to use, modify Skynet uses a custom [License](./LICENSE.md). The Skynet License is a source
and distribute the software, but you must preserve the payment mechanism in the software. code license that allows you to use, modify and distribute the software, but
you must preserve the payment mechanism in the software.
For the purposes of complying with our code license, you can use the following Siacoin address: For the purposes of complying with our code license, you can use the
following Siacoin address:
`fb6c9320bc7e01fbb9cd8d8c3caaa371386928793c736837832e634aaaa484650a3177d6714a` `fb6c9320bc7e01fbb9cd8d8c3caaa371386928793c736837832e634aaaa484650a3177d6714a`
### MongoDB Setup
Mongo needs a couple of extra steps in order to start a secure cluster.
- Open port 27017 on all nodes that will take part in the cluster. Ideally, you would only open the port for the other
nodes in the cluster.
- Manually add a `mgkey` file under `./docker/data/mongo` with the respective secret (
see [Mongo's keyfile access control](https://docs.mongodb.com/manual/tutorial/enforce-keyfile-access-control-in-existing-replica-set/)
for details).
- Manually run an initialisation `docker run` with extra environment variables that will initialise the admin user with
a password (example below).
- During the initialisation run mentioned above, we need to make two extra steps within the container:
- Change the ownership of `mgkey` to `mongodb:mongodb`
- Change its permissions to 400
- After these steps are done we can open a mongo shell on the master node and run `rs.add()` in order to add the new
node to the cluster.
Example initialisation docker run command:
```
docker run \
--rm \
--name mg \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=<admin username> \
-e MONGO_INITDB_ROOT_PASSWORD=<admin password> \
-v /home/user/skynet-webportal/docker/data/mongo/db:/data/db \
-v /home/user/skynet-webportal/docker/data/mongo/mgkey:/data/mgkey \
mongo --keyFile=/data/mgkey --replSet=skynet
```
Regular docker run command:
```
docker run \
--rm \
--name mg \
-p 27017:27017 \
-v /home/user/skynet-webportal/docker/data/mongo/db:/data/db \
-v /home/user/skynet-webportal/docker/data/mongo/mgkey:/data/mgkey \
mongo --keyFile=/data/mgkey --replSet=skynet
```
Cluster initialisation mongo command:
```
rs.initiate(
{
_id : "skynet",
members: [
{ _id : 0, host : "mongo:27017" }
]
}
)
```
Add more nodes when they are ready:
```
rs.add("second.node.net:27017")
```
### Kratos & Oathkeeper Setup
[Kratos](https://www.ory.sh/kratos) is our user management system of choice and
[Oathkeeper](https://www.ory.sh/oathkeeper) is the identity and access proxy.
Most of the needed config is already under `docker/kratos`. The only two things that need to be changed are the config
for Kratos that might contain you email server password, and the JWKS Oathkeeper uses to sign its JWT tokens.
Make sure to create your own`docker/kratos/config/kratos.yml` by copying the `kratos.yml.sample` in the same directory.
Also make sure to never add that file to source control because it will most probably contain your email password in
plain text!
To override the JWKS you will need to directly edit
`docker/kratos/oathkeeper/id_token.jwks.json` and replace it with your generated key set. If you don't know how to
generate a key set you can use this code:
```go
package main
import (
"encoding/json"
"log"
"os"
"github.com/ory/hydra/jwk"
)
func main() {
gen := jwk.RS256Generator{
KeyLength: 2048,
}
jwks, err := gen.Generate("", "sig")
if err != nil {
log.Fatal(err)
}
jsonbuf, err := json.MarshalIndent(jwks, "", " ")
if err != nil {
log.Fatal("failed to generate JSON: %s", err)
}
os.Stdout.Write(jsonbuf)
}
```
While you can directly put the output of this programme into the file mentioned above, you can also remove the public
key from the set and change the `kid` of the private key to not include the prefix `private:`.
### CockroachDB Setup
Kratos uses CockroachDB to store its data. For that data to be shared across all nodes that comprise your portal cluster
setup, we need to set up a CockroachDB cluster, complete with secure communication.
#### Generate the certificates for secure communication
For a detailed walk-through, please check [this guide](https://www.cockroachlabs.com/docs/v20.2/secure-a-cluster.html)
out.
Steps:
1. Start a local cockroach docker instance:
`docker run -d -v "<local dir>:/cockroach/cockroach-secure" --name=crdb cockroachdb/cockroach start --insecure`
1. Get a shall into that instance: `docker exec -it crdb /bin/bash`
1. Go to the directory we which we mapped to a local dir: `cd /cockroach/cockroach-secure`
1. Create the subdirectories in which to create certificates and keys: `mkdir certs my-safe-directory`
1. Create the CA (Certificate Authority) certificate and key
pair: `cockroach cert create-ca --certs-dir=certs --ca-key=my-safe-directory/ca.key`
1. Create a client certificate and key pair for the root
user: `cockroach cert create-client root --certs-dir=certs --ca-key=my-safe-directory/ca.key`
1. Create the certificate and key pair for your
nodes: `cockroach cert create-node cockroach mynode.siasky.net --certs-dir=certs --ca-key=my-safe-directory/ca.key`.
Don't forget the `cockroach` node name - it's needed by our docker-compose setup. If you want to create certificates
for more nodes, just delete the `node.*` files (after you've finished the next steps for this node!) and re-run the
above command with the new node name.
1. Put the contents of the `certs` folder under `docker/cockroach/certs/*` under your portal's root dir and store the
content of `my-safe-directory` somewhere safe.
1. Put _another copy_ of those certificates under `docker/kratos/cr_certs` and change permissions of the `*.key` files,
so they can be read by anyone (644).
#### Configure your CockroachDB node
There is some configuration that needs to be added to your `.env`file, namely:
1. CR_IP - the public IP of your node
1. CR_CLUSTER_NODES - a list of IPs and ports which make up your cluster, e.g.
`95.216.13.185:26257,147.135.37.21:26257,144.76.136.122:26257`. This will be the list of nodes that will make up your
cluster, so make sure those are accurate.
## Contributing
### Testing Your Code
Before pushing your code, you should verify that it will pass our online test suite.
**Cypress Tests**
Verify the Cypress test suite by doing the following:
1. In one terminal screen run `GATSBY_API_URL=https://siasky.net yarn workspace webapp start`
1. In a second terminal screen run `yarn workspace webapp cypress run`
## Setting up complete skynet server
A setup guide with installation scripts can be found in [setup-scripts/README.md](./setup-scripts/README.md).

View File

@ -1,2 +0,0 @@
.init
.DS_Store

View File

@ -1,4 +0,0 @@
# Changelog Generator
For usage of changelog generator please see [readme](https://gitlab.com/NebulousLabs/changelog-generator/-/blob/master/README.md) in Changelog Generator
repository.

View File

@ -1,11 +0,0 @@
Version Scheme
--------------
Skynet Webportal uses the following versioning scheme, vX.X.X
- First Digit signifies a major (compatibility breaking) release
- Second Digit signifies a major (non compatibility breaking) release
- Third Digit signifies a minor or patch release
Version History
---------------
Latest:

View File

@ -1,69 +0,0 @@
## Mar 8, 2022:
### v0.1.4
**Key Updates**
- expose generic skylink serving endpoint on domain aliases
- Add abuse scanner service, activated by adding `u` to `PORTAL_MODULES`
- Add malware scanner service, activated by adding `s` to `PORTAL_MODULES`
- Remove ORY Kratos, ORY Oathkeeper, CockroachDB.
- Add `/serverload` endpoint for CPU usage and free disk space
**Bugs Fixed**
- Add missing servers and blocklist command to the manual blocklist script.
- fixed a bug when accessing file from skylink via subdomain with a filename that had escaped characters
- Fix `blocklist-skylink.sh` script that didn't removed blocked skylink from
nginx cache.
- fixed uploaded directory name (was "undefined" before)
- fixed empty directory upload progress (size was not calculated for directories)
**Other**
- add new critical health check that scans config and makes sure that all relevant configurations are set
- Add abuse report configuration
- Remove hardcoded Airtable default values from blocklist script. Portal
operators need to define their own values in portal common config (LastPass).
- Add health check for the blocker container
- Drop `Skynet-Requested-Skylink` header
- Dump disk space usage when health-checker script disables portal due to
critical free disk space.
- Enable the accounting module for skyd
- Add link to supported setup process in Gitbook.
- Set `min_free` parameter on the `proxy_cache_path` directive to `100g`
- Parameterize MongoDB replicaset in `docker-compose.mongodb.yml` via
`SKYNET_DB_REPLICASET` from `.env` file.
- Hot reload Nginx after pruning cache files.
- Added script to prune nginx cache.
- Remove hardcoded server list from `blocklist-skylink.sh` so it removes server
list duplication and can also be called from Ansible.
- Remove outdated portal setup documentation and point to developer docs.
- Block skylinks in batches to improve performance.
- Add trimming Airtable skylinks from Takedown Request table.
- Update handshake to use v3.0.1
## Oct 18, 2021:
### v0.1.3
**Key Updates**
- Change skyd 307 redirect code to 308
- Set caddy dns entry ttl limit to 15 minutes to remove stranded entries.
- Set skyd up to connect to the local mongodb cluster for storing TUS metadata
- Update health check disable command to require reason.
- Move MongoDB to a separate service (use `PORTAL_MODULES=m` to use it without accounts)
- Add proper handling for options response on /skynet/tus endpoint
- added unpinning skylinks from account dashboard
**Bugs Fixed**
- include tus header upload-concat in cors requests
- fixed issue with caddy requesting new certificates instead of using existing ones from file storage
- fixed the latest news link redirect in the news header
- Fix extended checks error by rounding the reported datetime.
**Other**
- Remove outdated references to NebulousLabs
## August 9th, 2021:
### v0.1.1
Monthly release
## March 24th, 2021:
### v0.1.0
Initial versioned release

View File

@ -1,40 +0,0 @@
#!/usr/bin/env bash
set -e
# Generate CHANGELOG.md from changelog directory
# Requires:
# - curl installed
# Config
main_version='v1.0.1'
export main_filename='generate-changelog-main.sh'
export main_url="https://gitlab.com/NebulousLabs/changelog-generator/-/raw/${main_version}/${main_filename}"
export temp_dir="${HOME}/.nebulous/changelog-generator"
export main_path=${temp_dir}/${main_filename}
# Set working dir to script location
pushd $(dirname "$0") > /dev/null
# If executed in 'changelog-generator' repo, do not use the older released
# version, use the latest local version
repo_dir="$(basename ${PWD%/*})"
if [[ "${repo_dir}" == "changelog-generator" ]]
then
# Call the latest local main script
echo "Executing the latest local version of the main script"
export local_execution=true
chmod +x ../${main_filename}
../${main_filename} "$@"
exit 0
fi
# Download main script to temp dir
mkdir -p ${temp_dir}
curl --show-error --fail -o ${main_path} ${main_url}
# Execute downloaded main script passing arguments to the main script
chmod +x ${main_path}
${main_path} "$@"
popd > /dev/null

View File

@ -1,2 +0,0 @@
- Fix `dashboard-v2` Dockerfile context in `docker-compose.accounts.yml` to
avoid Ansible deploy (docker compose build) `permission denied` issues.

View File

@ -1 +0,0 @@
- Fix missing `logs` dir that is required for backup scripts (cron jobs).

View File

@ -1 +0,0 @@
- Add Pinner service to the portal stack. Activate it by selecting the 'p' module.

63
dc
View File

@ -1,63 +0,0 @@
#!/bin/bash
# The dc command is an alias to docker-compose which also scans the current portal configuration (as defined in .env)
# and selects the right docker-compose files to include in the operation. You can use the command in the same way you
# would use docker-compose with the only difference being that you don't need to specify compose files. For more
# information you can run `./dc` or `./dc help`.
# get current working directory of this script and prefix all files with it to
# be able to call this script from anywhere and not only root directory of
# skynet-webportal project
cwd="$(dirname -- "$0";)";
# get portal modules configuration from .env file (if defined more than once, the last one is used)
if [[ -f "${cwd}/.env" ]]; then
PORTAL_MODULES=$(grep -e "^PORTAL_MODULES=" ${cwd}/.env | tail -1 | sed "s/PORTAL_MODULES=//")
fi
# include base docker compose file
COMPOSE_FILES="-f ${cwd}/docker-compose.yml"
for i in $(seq 1 ${#PORTAL_MODULES}); do
# accounts module - alias "a"
if [[ ${PORTAL_MODULES:i-1:1} == "a" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.accounts.yml"
fi
# blocker module - alias "b"
if [[ ${PORTAL_MODULES:i-1:1} == "b" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.blocker.yml"
fi
# jaeger module - alias "j"
if [[ ${PORTAL_MODULES:i-1:1} == "j" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.jaeger.yml"
fi
# malware-scanner module - alias "s"
if [[ ${PORTAL_MODULES:i-1:1} == "s" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.blocker.yml -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.malware-scanner.yml"
fi
# mongodb module - alias "m"
if [[ ${PORTAL_MODULES:i-1:1} == "m" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml"
fi
# abuse-scanner module - alias "u"
if [[ ${PORTAL_MODULES:i-1:1} == "u" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.blocker.yml -f ${cwd}/docker-compose.abuse-scanner.yml"
fi
# pinner module - alias "p"
if [[ ${PORTAL_MODULES:i-1:1} == "p" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.pinner.yml"
fi
done
# override file if exists
if [[ -f docker-compose.override.yml ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.override.yml"
fi
docker-compose $COMPOSE_FILES $@

View File

@ -1,41 +0,0 @@
version: "3.8"
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
abuse-scanner:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/abuse-scanner.git#main
image: skynetlabs/abuse-scanner:0.4.0
container_name: abuse-scanner
restart: unless-stopped
logging: *default-logging
env_file:
- .env
environment:
- ABUSE_LOG_LEVEL=${ABUSE_LOG_LEVEL}
- ABUSE_MAILADDRESS=${ABUSE_MAILADDRESS}
- ABUSE_MAILBOX=${ABUSE_MAILBOX}
- ABUSE_SPONSOR=${ABUSE_SPONSOR}
- BLOCKER_HOST=10.10.10.110
- BLOCKER_PORT=4000
- EMAIL_SERVER=${EMAIL_SERVER}
- EMAIL_USERNAME=${EMAIL_USERNAME}
- EMAIL_PASSWORD=${EMAIL_PASSWORD}
- SKYNET_DB_HOST=${SKYNET_DB_HOST}
- SKYNET_DB_PORT=${SKYNET_DB_PORT}
- SKYNET_DB_USER=${SKYNET_DB_USER}
- SKYNET_DB_PASS=${SKYNET_DB_PASS}
networks:
shared:
ipv4_address: 10.10.10.120
depends_on:
- mongo
- blocker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /tmp:/tmp

View File

@ -1,4 +1,4 @@
version: "3.8" version: "3.7"
x-logging: &default-logging x-logging: &default-logging
driver: json-file driver: json-file
@ -9,43 +9,40 @@ x-logging: &default-logging
services: services:
nginx: nginx:
environment: environment:
- ACCOUNTS_ENABLED=true - ACCOUNTS_ENABLED=1
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only volumes:
- ./docker/accounts/nginx.account.conf:/etc/nginx/conf.extra.d/nginx.account.conf:ro
depends_on: depends_on:
- accounts - accounts
health-check: health-check:
environment: environment:
- ACCOUNTS_ENABLED=true - ACCOUNTS_ENABLED=1
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only
accounts: accounts:
# uncomment "build" and comment out "image" to build from sources build:
# build: https://github.com/SkynetLabs/skynet-accounts.git#main context: ./docker/accounts
image: skynetlabs/skynet-accounts:1.3.0 dockerfile: Dockerfile
args:
branch: main
container_name: accounts container_name: accounts
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
env_file: env_file:
- .env - .env
environment: environment:
- ACCOUNTS_EMAIL_URI=${ACCOUNTS_EMAIL_URI} - SKYNET_DB_HOST=${SKYNET_DB_HOST}
- ACCOUNTS_JWKS_FILE=/conf/jwks.json - SKYNET_DB_PORT=${SKYNET_DB_PORT}
- SKYNET_DB_USER=${SKYNET_DB_USER}
- SKYNET_DB_PASS=${SKYNET_DB_PASS}
- COOKIE_DOMAIN=${COOKIE_DOMAIN} - COOKIE_DOMAIN=${COOKIE_DOMAIN}
- COOKIE_HASH_KEY=${COOKIE_HASH_KEY} - COOKIE_HASH_KEY=${COOKIE_HASH_KEY}
- COOKIE_ENC_KEY=${COOKIE_ENC_KEY} - COOKIE_ENC_KEY=${COOKIE_ENC_KEY}
- PORTAL_DOMAIN=${PORTAL_DOMAIN}
- SERVER_DOMAIN=${SERVER_DOMAIN}
- SKYNET_DB_HOST=${SKYNET_DB_HOST:-mongo}
- SKYNET_DB_PORT=${SKYNET_DB_PORT:-27017}
- SKYNET_DB_USER=${SKYNET_DB_USER}
- SKYNET_DB_PASS=${SKYNET_DB_PASS}
- STRIPE_API_KEY=${STRIPE_API_KEY} - STRIPE_API_KEY=${STRIPE_API_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
- SKYNET_ACCOUNTS_LOG_LEVEL=${SKYNET_ACCOUNTS_LOG_LEVEL:-info} - SKYNET_ACCOUNTS_LOG_LEVEL=${SKYNET_ACCOUNTS_LOG_LEVEL}
volumes: - KRATOS_ADDR=${KRATOS_ADDR}
- ./docker/data/accounts:/data - OATHKEEPER_ADDR=${OATHKEEPER_ADDR}
- ./docker/accounts/conf:/conf
expose: expose:
- 3000 - 3000
networks: networks:
@ -53,25 +50,119 @@ services:
ipv4_address: 10.10.10.70 ipv4_address: 10.10.10.70
depends_on: depends_on:
- mongo - mongo
- oathkeeper
mongo:
image: mongo:4.4.1
command: --keyFile=/data/mgkey --replSet=skynet
container_name: mongo
restart: unless-stopped
logging: *default-logging
volumes:
- ./docker/data/mongo/db:/data/db
- ./docker/data/mongo/mgkey:/data/mgkey:rw
networks:
shared:
ipv4_address: 10.10.10.71
ports:
- "27017:27017"
kratos-migrate:
image: oryd/kratos:v0.5.5-alpha.1
container_name: kratos-migrate
restart: "no"
logging: *default-logging
environment:
- DSN=cockroach://root@cockroach:26257/defaultdb?max_conns=20&max_idle_conns=4&sslmode=verify-full&sslcert=/certs/node.crt&sslkey=/certs/node.key&sslrootcert=/certs/ca.crt
- SQA_OPT_OUT=true
volumes:
- ./docker/kratos/config:/etc/config/kratos
- ./docker/data/cockroach/sqlite:/var/lib/sqlite
- ./docker/kratos/cr_certs:/certs
command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes
networks:
shared:
ipv4_address: 10.10.10.80
depends_on:
- cockroach
kratos:
image: oryd/kratos:v0.5.5-alpha.1
container_name: kratos
restart: unless-stopped
logging: *default-logging
expose:
- 4433 # public
- 4434 # admin
environment:
- DSN=cockroach://root@cockroach:26257/defaultdb?max_conns=20&max_idle_conns=4&sslmode=verify-full&sslcert=/certs/node.crt&sslkey=/certs/node.key&sslrootcert=/certs/ca.crt
- LOG_LEVEL=trace
- SERVE_PUBLIC_BASE_URL=${SKYNET_DASHBOARD_URL}/.ory/kratos/public/
- SQA_OPT_OUT=true
command: serve -c /etc/config/kratos/kratos.yml
volumes:
- ./docker/kratos/config:/etc/config/kratos
- ./docker/data/cockroach/sqlite:/var/lib/sqlite
- ./docker/kratos/cr_certs:/certs
networks:
shared:
ipv4_address: 10.10.10.81
depends_on:
- kratos-migrate
dashboard: dashboard:
# uncomment "build" and comment out "image" to build from sources build:
# build: context: ./packages/dashboard
# context: https://github.com/SkynetLabs/webportal-accounts-dashboard.git#main dockerfile: Dockerfile
# dockerfile: Dockerfile
image: skynetlabs/webportal-accounts-dashboard:2.1.1
container_name: dashboard container_name: dashboard
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
env_file: env_file:
- .env - .env
volumes: environment:
- ./docker/data/dashboard/.cache:/usr/app/.cache - NEXT_PUBLIC_SKYNET_PORTAL_API=${SKYNET_PORTAL_API}
- ./docker/data/dashboard/public:/usr/app/public - NEXT_PUBLIC_SKYNET_DASHBOARD_URL=${SKYNET_DASHBOARD_URL}
- NEXT_PUBLIC_KRATOS_BROWSER_URL=${SKYNET_DASHBOARD_URL}/.ory/kratos/public
- NEXT_PUBLIC_KRATOS_PUBLIC_URL=http://oathkeeper:4455/.ory/kratos/public
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
networks: networks:
shared: shared:
ipv4_address: 10.10.10.85 ipv4_address: 10.10.10.85
expose: expose:
- 9000 - 3000
depends_on: depends_on:
- mongo - oathkeeper
oathkeeper:
image: oryd/oathkeeper:v0.38
container_name: oathkeeper
expose:
- 4455
- 4456
command: serve proxy -c "/etc/config/oathkeeper/oathkeeper.yml"
environment:
- LOG_LEVEL=debug
volumes:
- ./docker/kratos/oathkeeper:/etc/config/oathkeeper
restart: on-failure
networks:
shared:
ipv4_address: 10.10.10.83
depends_on:
- kratos
cockroach:
image: cockroachdb/cockroach:v20.2.3
container_name: cockroach
env_file:
- .env
command: start --advertise-addr=${CR_IP} --join=${CR_CLUSTER_NODES} --certs-dir=/certs --listen-addr=0.0.0.0:26257 --http-addr=0.0.0.0:8080
volumes:
- ./docker/data/cockroach/sqlite:/cockroach/cockroach-data
- ./docker/cockroach/certs:/certs
ports:
- "4080:8080"
- "26257:26257"
networks:
shared:
ipv4_address: 10.10.10.84

View File

@ -1,33 +0,0 @@
version: "3.8"
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
health-check:
environment:
- BLOCKER_HOST=10.10.10.110
- BLOCKER_PORT=4000
blocker:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/blocker.git#main
image: skynetlabs/blocker:0.1.2
container_name: blocker
restart: unless-stopped
logging: *default-logging
env_file:
- .env
volumes:
- ./docker/data/nginx/blocker:/data/nginx/blocker
expose:
- 4000
networks:
shared:
ipv4_address: 10.10.10.110
depends_on:
- mongo
- sia

View File

@ -1,16 +1,10 @@
version: "3.8" version: '3.7'
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services: services:
sia: sia:
environment: environment:
- JAEGER_DISABLED=${JAEGER_DISABLED:-false} # Enable/Disable tracing - JAEGER_DISABLED=${JAEGER_DISABLED:-true} # Enable/Disable tracing
- JAEGER_SERVICE_NAME=${SERVER_DOMAIN:-Skyd} # change to e.g. eu-ger-1 - JAEGER_SERVICE_NAME=${PORTAL_NAME:-Skyd} # change to e.g. eu-ger-1
# Configuration # Configuration
# See https://github.com/jaegertracing/jaeger-client-go#environment-variables # See https://github.com/jaegertracing/jaeger-client-go#environment-variables
# for all options. # for all options.
@ -21,15 +15,10 @@ services:
- JAEGER_REPORTER_LOG_SPANS=false - JAEGER_REPORTER_LOG_SPANS=false
jaeger-agent: jaeger-agent:
image: jaegertracing/jaeger-agent:1.38.1 image: jaegertracing/jaeger-agent
command: command: [ "--reporter.grpc.host-port=jaeger-collector:14250", "--reporter.grpc.retry.max=1000" ]
[
"--reporter.grpc.host-port=jaeger-collector:14250",
"--reporter.grpc.retry.max=1000",
]
container_name: jaeger-agent container_name: jaeger-agent
restart: on-failure restart: on-failure
logging: *default-logging
expose: expose:
- 6831 - 6831
- 6832 - 6832
@ -43,11 +32,10 @@ services:
- jaeger-collector - jaeger-collector
jaeger-collector: jaeger-collector:
image: jaegertracing/jaeger-collector:1.38.1 image: jaegertracing/jaeger-collector
entrypoint: /wait_to_start.sh entrypoint: /wait_to_start.sh
container_name: jaeger-collector container_name: jaeger-collector
restart: on-failure restart: on-failure
logging: *default-logging
expose: expose:
- 14269 - 14269
- 14268 - 14268
@ -68,11 +56,10 @@ services:
- elasticsearch - elasticsearch
jaeger-query: jaeger-query:
image: jaegertracing/jaeger-query:1.38.1 image: jaegertracing/jaeger-query
entrypoint: /wait_to_start.sh entrypoint: /wait_to_start.sh
container_name: jaeger-query container_name: jaeger-query
restart: on-failure restart: on-failure
logging: *default-logging
ports: ports:
- "127.0.0.1:16686:16686" - "127.0.0.1:16686:16686"
expose: expose:
@ -93,13 +80,11 @@ services:
- elasticsearch - elasticsearch
elasticsearch: elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.6 image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.15
container_name: elasticsearch container_name: elasticsearch
restart: on-failure restart: on-failure
logging: *default-logging
environment: environment:
- discovery.type=single-node - discovery.type=single-node
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
volumes: volumes:
# This dir needs to be chowned to 1000:1000 # This dir needs to be chowned to 1000:1000
- ./docker/data/elasticsearch/data:/usr/share/elasticsearch/data - ./docker/data/elasticsearch/data:/usr/share/elasticsearch/data

View File

@ -1,46 +0,0 @@
version: "3.8"
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
clamav:
image: clamav/clamav:stable_base
container_name: clamav
restart: on-failure
logging: *default-logging
volumes:
- ./docker/data/clamav/clamav/defs:/var/lib/clamav
- ./docker/clamav/clamd.conf:/etc/clamav/clamd.conf:ro
expose:
- 3310 # NEVER expose this outside of the local network!
networks:
shared:
ipv4_address: 10.10.10.100
malware-scanner:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/malware-scanner.git#main
image: skynetlabs/malware-scanner:0.1.0
container_name: malware-scanner
restart: unless-stopped
logging: *default-logging
env_file:
- .env
environment:
- CLAMAV_IP=10.10.10.100
- CLAMAV_PORT=3310
- BLOCKER_IP=10.10.10.110
- BLOCKER_PORT=4000
expose:
- 4000
networks:
shared:
ipv4_address: 10.10.10.101
depends_on:
- mongo
- clamav
- blocker

View File

@ -1,29 +0,0 @@
version: "3.8"
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
sia:
environment:
- MONGODB_URI=mongodb://${SKYNET_DB_HOST}:${SKYNET_DB_PORT}
- MONGODB_USER=${SKYNET_DB_USER}
- MONGODB_PASSWORD=${SKYNET_DB_PASS}
mongo:
image: mongo:4.4.17
command: --keyFile=/data/mgkey --replSet=${SKYNET_DB_REPLICASET:-skynet} --setParameter ShardingTaskExecutorPoolMinSize=10
container_name: mongo
restart: unless-stopped
logging: *default-logging
volumes:
- ./docker/data/mongo/db:/data/db
- ./docker/data/mongo/mgkey:/data/mgkey:rw
networks:
shared:
ipv4_address: 10.10.10.71
ports:
- "${SKYNET_DB_PORT}:27017"

View File

@ -1,30 +0,0 @@
version: "3.8"
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
pinner:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/pinner.git#main
image: skynetlabs/pinner:0.7.8
container_name: pinner
restart: unless-stopped
logging: *default-logging
env_file:
- .env
volumes:
- ./docker/data/pinner/logs:/logs
environment:
- PINNER_LOG_LEVEL=${PINNER_LOG_LEVEL:-info}
expose:
- 4000
networks:
shared:
ipv4_address: 10.10.10.130
depends_on:
- mongo
- sia

View File

@ -0,0 +1,12 @@
version: "3.7"
services:
nginx:
build:
context: ./docker/nginx
dockerfile: Dockerfile.bionic
args:
RESTY_ADD_PACKAGE_BUILDDEPS: git
RESTY_EVAL_PRE_CONFIGURE: git clone https://github.com/fdintino/nginx-upload-module /tmp/nginx-upload-module
RESTY_CONFIG_OPTIONS_MORE: --add-module=/tmp/nginx-upload-module
RESTY_EVAL_POST_MAKE: /usr/local/openresty/luajit/bin/luarocks install luasocket

View File

@ -1,4 +1,4 @@
version: "3.8" version: "3.7"
x-logging: &default-logging x-logging: &default-logging
driver: json-file driver: json-file
@ -15,23 +15,16 @@ networks:
services: services:
sia: sia:
# uncomment "build" and comment out "image" to build from sources build:
# build: context: ./docker/sia
# context: https://github.com/SkynetLabs/docker-skyd.git#main dockerfile: Dockerfile
# dockerfile: scratch/Dockerfile args:
# args: branch: v1.5.5
# branch: master
image: skynetlabs/skyd:1.6.9
command: --disable-api-security --api-addr :9980 --modules gctwra
container_name: sia container_name: sia
restart: unless-stopped restart: unless-stopped
stop_grace_period: 5m
logging: *default-logging logging: *default-logging
environment: environment:
- SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-true} - SIA_MODULES=gctwr
- SKYD_DISK_CACHE_SIZE=${SKYD_DISK_CACHE_SIZE:-53690000000} # 50GB
- SKYD_DISK_CACHE_MIN_HITS=${SKYD_DISK_CACHE_MIN_HITS:-3}
- SKYD_DISK_CACHE_HIT_PERIOD=${SKYD_DISK_CACHE_HIT_PERIOD:-3600} # 1h
env_file: env_file:
- .env - .env
volumes: volumes:
@ -42,79 +35,61 @@ services:
expose: expose:
- 9980 - 9980
certbot: caddy:
# replace this image with the image supporting your dns provider from build:
# https://hub.docker.com/r/certbot/certbot and adjust CERTBOT_ARGS env variable context: ./docker/caddy
# note: you will need to authenticate your dns request so consult the plugin docs dockerfile: Dockerfile
# configuration https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins container_name: caddy
#
# =================================================================================
# example docker-compose.yml changes required for Cloudflare dns provider:
#
# image: certbot/dns-cloudflare
# environment:
# - CERTBOT_ARGS=--dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini
#
# create ./docker/data/certbot/cloudflare.ini file with the following content:
# dns_cloudflare_api_token = <api key generated at https://dash.cloudflare.com/profile/api-tokens>
#
# make sure that the file has 0400 permissions with:
# chmod 0400 ./docker/data/certbot/cloudflare.ini
image: certbot/dns-route53:v1.31.0
entrypoint: sh /entrypoint.sh
container_name: certbot
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
env_file: env_file:
- .env - .env
environment:
- CERTBOT_ARGS=--dns-route53
volumes: volumes:
- ./docker/certbot/entrypoint.sh:/entrypoint.sh - ./docker/data/caddy/data:/data
- ./docker/data/certbot:/etc/letsencrypt - ./docker/data/caddy/config:/config
- ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile
networks:
shared:
ipv4_address: 10.10.10.20
ports:
- "80:80"
- "443:443"
depends_on:
- nginx
nginx: nginx:
# uncomment "build" and comment out "image" to build from sources build:
# build: context: ./docker/nginx
# context: https://github.com/SkynetLabs/webportal-nginx.git#main dockerfile: Dockerfile
# dockerfile: Dockerfile
image: skynetlabs/webportal-nginx:1.0.0
container_name: nginx container_name: nginx
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
env_file: env_file:
- .env - .env
volumes: volumes:
- ./docker/nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
- ./docker/data/nginx/cache:/data/nginx/cache - ./docker/data/nginx/cache:/data/nginx/cache
- ./docker/data/nginx/blocker:/data/nginx/blocker
- ./docker/data/nginx/logs:/usr/local/openresty/nginx/logs - ./docker/data/nginx/logs:/usr/local/openresty/nginx/logs
- ./docker/data/nginx/skynet:/data/nginx/skynet:ro - ./docker/data/nginx/skynet:/data/nginx/skynet:ro
- ./docker/data/sia/apipassword:/data/sia/apipassword:ro - ./docker/data/sia/apipassword:/data/sia/apipassword:ro
- ./docker/data/certbot:/etc/letsencrypt
networks: networks:
shared: shared:
ipv4_address: 10.10.10.30 ipv4_address: 10.10.10.30
ports: expose:
- "443:443" - 80
- "80:80"
depends_on: depends_on:
- sia - sia
- handshake-api - handshake-api
- dnslink-api
- website - website
website: website:
# uncomment "build" and comment out "image" to build from sources build:
# build: context: ./packages/website
# context: https://github.com/SkynetLabs/webportal-website.git#main dockerfile: Dockerfile
# dockerfile: Dockerfile
image: skynetlabs/webportal-website:0.2.3
container_name: website container_name: website
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
volumes:
- ./docker/data/website/.cache:/usr/app/.cache
- ./docker/data/website/.public:/usr/app/public
env_file: env_file:
- .env - .env
networks: networks:
@ -124,11 +99,19 @@ services:
- 9000 - 9000
handshake: handshake:
image: handshakeorg/hsd:4.0.2 build:
command: --chain-migrate=3 --no-wallet --no-auth --compact-tree-on-init --network=main --http-host=0.0.0.0 context: ./docker/handshake
dockerfile: Dockerfile
container_name: handshake container_name: handshake
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
environment:
- HSD_LOG_CONSOLE=false
- HSD_HTTP_HOST=0.0.0.0
- HSD_NETWORK=main
- HSD_PORT=12037
env_file:
- .env
volumes: volumes:
- ./docker/data/handshake/.hsd:/root/.hsd - ./docker/data/handshake/.hsd:/root/.hsd
networks: networks:
@ -138,11 +121,9 @@ services:
- 12037 - 12037
handshake-api: handshake-api:
# uncomment "build" and comment out "image" to build from sources build:
# build: context: ./packages/handshake-api
# context: https://github.com/SkynetLabs/webportal-handshake-api.git#main dockerfile: Dockerfile
# dockerfile: Dockerfile
image: skynetlabs/webportal-handshake-api:0.1.3
container_name: handshake-api container_name: handshake-api
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
@ -161,27 +142,10 @@ services:
depends_on: depends_on:
- handshake - handshake
dnslink-api:
# uncomment "build" and comment out "image" to build from sources
# build:
# context: https://github.com/SkynetLabs/webportal-dnslink-api.git#main
# dockerfile: Dockerfile
image: skynetlabs/webportal-dnslink-api:0.2.1
container_name: dnslink-api
restart: unless-stopped
logging: *default-logging
networks:
shared:
ipv4_address: 10.10.10.55
expose:
- 3100
health-check: health-check:
# uncomment "build" and comment out "image" to build from sources build:
# build: context: ./packages/health-check
# context: https://github.com/SkynetLabs/webportal-health-check.git#main dockerfile: Dockerfile
# dockerfile: Dockerfile
image: skynetlabs/webportal-health-check:1.0.0
container_name: health-check container_name: health-check
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
@ -197,3 +161,5 @@ services:
- STATE_DIR=/usr/app/state - STATE_DIR=/usr/app/state
expose: expose:
- 3100 - 3100
depends_on:
- caddy

View File

@ -0,0 +1,22 @@
FROM golang:1.15
LABEL maintainer="NebulousLabs <devs@nebulous.tech>"
ENV GOOS linux
ENV GOARCH amd64
ARG branch=main
WORKDIR /root
RUN git clone --single-branch --branch ${branch} https://github.com/NebulousLabs/skynet-accounts.git && \
cd skynet-accounts && \
go mod download && \
make release
ENV SKYNET_DB_HOST="localhost"
ENV SKYNET_DB_PORT="27017"
ENV SKYNET_DB_USER="username"
ENV SKYNET_DB_PASS="password"
ENV SKYNET_ACCOUNTS_PORT=3000
ENTRYPOINT ["skynet-accounts"]

View File

@ -0,0 +1,20 @@
server {
listen 80;
listen [::]:80;
server_name account.*;
location / {
proxy_redirect http://127.0.0.1/ https://$host/;
proxy_pass http://oathkeeper:4455;
}
}
server {
listen 80;
listen [::]:80;
server_name secure.*;
if ($host ~ secure.(.*)) {
return 301 $scheme://account.$1$request_uri;
}
}

41
docker/caddy/Caddyfile Normal file
View File

@ -0,0 +1,41 @@
# This block below is optional if you want to generate an internal certificate for the server ip address.
# It is useful in case you have services trying to reach the server through ip and not domain like health checks.
# It will generate an internal certificate so browsers will warn you when connecting but that not a problem.
:443 {
tls internal {
on_demand
}
reverse_proxy nginx:80
}
# Make sure you have SSL_CERTIFICATE_STRING specified in .env file because you need it to fetch correct certificates.
# It needs to have at least 3 parts, the absolute part (ie. example.com), the wildcard part (ie. *.example.com) and
# the hns wildcard part (ie. *.hns.example.com). The resulting string should look like:
# example.com, *.example.com, *.hns.example.com
# In addition, if you are running multiple servers for the single portal like we do on siasky.net, you might want to
# add an aliased string that is going to help you access and distinguish between servers, the result would look like:
# example.com, *.example.com, *.hns.example.com, *.germany.example.com, *.hns.germany.example.com
# Note that you don't need to specify the absolute part for the alias since it's already covered in the wildcard part
# of the original certificate string (*.example.com).
{$SSL_CERTIFICATE_STRING} {
# If you want to use basic http-01 (basic, good for one server setup) certificate challenge
# then uncomment the line below and make sure you have EMAIL_ADDRESS specified in .env file
# and comment the tls block that contains the dns challenge configuration.
# tls {$EMAIL_ADDRESS}
tls {
# We are using route53 as our dns provider and it requires additional AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
# environment variables in .env file. You can use other providers by using specific package from
# https://github.com/caddy-dns in the docker/caddy/Dockerfile instead of our route53 one.
dns route53 {
max_retries 50
}
}
reverse_proxy nginx:80
}

8
docker/caddy/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM caddy:2.3.0-builder AS caddy-builder
# available dns resolvers: https://github.com/caddy-dns
RUN xcaddy build --with github.com/caddy-dns/route53
FROM caddy:2.3.0
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy

View File

@ -1,55 +0,0 @@
#!/bin/bash
# Portal domain requires 3 domain certificates:
# - exact portal domain, ie. example.com
# - wildcard subdomain on portal domain, ie. *.example.com
# used for skylinks served from portal subdomain
# - wildcard subdomain on hns portal domain subdomain, ie. *.hns.example.com
# used for resolving handshake domains
DOMAINS=${PORTAL_DOMAIN},*.${PORTAL_DOMAIN},*.hns.${PORTAL_DOMAIN}
# Add server domain when it is not empty and different from portal domain
if [ ! -z "${SERVER_DOMAIN}" ] && [ "${PORTAL_DOMAIN}" != "${SERVER_DOMAIN}" ]; then
# In case where server domain is not covered by portal domain's
# wildcard certificate, add server domain name to domains list.
# - server-001.example.com is covered by *.example.com
# - server-001.servers.example.com or server-001.example-severs.com
# are not covered by any already requested wildcard certificates
#
# The condition checks whether server domain does not match portal domain
# with exactly one level of subdomain (portal domain wildcard cert):
# (start) [anything but the dot] + [dot] + [portal domain] (end)
if ! printf "${SERVER_DOMAIN}" | grep -q -E "^[^\.]+\.${PORTAL_DOMAIN}$"; then
DOMAINS=${DOMAINS},${SERVER_DOMAIN}
fi
# Server domain requires the same set of domain certificates as portal domain.
# Exact server domain case is handled above.
DOMAINS=${DOMAINS},*.${SERVER_DOMAIN},*.hns.${SERVER_DOMAIN}
fi
# The "wait" will prevent an exit from the script while background tasks are
# still active, so we are adding the line below as a method to prevent orphaning
# the background child processe. The trap fires when docker terminates the container.
trap exit TERM
while :; do
# Execute certbot and generate or maintain certificates for given domain string.
# --non-interactive: we are running this as an automation so we cannot be prompted
# --agree-tos: required flag marking agreement with letsencrypt tos
# --cert-name: output directory name
# --email: required for generating certificates, used for communication with CA
# --domains: comma separated list of domains (will generate one bundled SAN cert)
# Use CERTBOT_ARGS env variable to pass any additional arguments, ie --dns-route53
certbot certonly \
--non-interactive --agree-tos --cert-name skynet-portal \
--email ${EMAIL_ADDRESS} --domains ${DOMAINS} ${CERTBOT_ARGS}
# Run a background sleep process that counts down given time
# Certbot docs advise running maintenance process every 12 hours
sleep 12h &
# Await execution until sleep process is finished (it's a background process)
# Syntax explanation: ${!} expands to a pid of last ran process
wait ${!}
done

View File

@ -1,794 +0,0 @@
##
## Example config file for the Clam AV daemon
## Please read the clamd.conf(5) manual before editing this file.
##
# Comment or remove the line below.
# Example
# Uncomment this option to enable logging.
# LogFile must be writable for the user running daemon.
# A full path is required.
# Default: disabled
LogFile /var/log/clamav/clamd.log
# By default the log file is locked for writing - the lock protects against
# running clamd multiple times (if want to run another clamd, please
# copy the configuration file, change the LogFile variable, and run
# the daemon with --config-file option).
# This option disables log file locking.
# Default: no
#LogFileUnlock yes
# Maximum size of the log file.
# Value of 0 disables the limit.
# You may use 'M' or 'm' for megabytes (1M = 1m = 1048576 bytes)
# and 'K' or 'k' for kilobytes (1K = 1k = 1024 bytes). To specify the size
# in bytes just don't use modifiers. If LogFileMaxSize is enabled, log
# rotation (the LogRotate option) will always be enabled.
# Default: 1M
LogFileMaxSize 50M
# Log time with each message.
# Default: no
LogTime yes
# Also log clean files. Useful in debugging but drastically increases the
# log size.
# Default: no
#LogClean yes
# Use system logger (can work together with LogFile).
# Default: no
#LogSyslog yes
# Specify the type of syslog messages - please refer to 'man syslog'
# for facility names.
# Default: LOG_LOCAL6
#LogFacility LOG_MAIL
# Enable verbose logging.
# Default: no
#LogVerbose yes
# Enable log rotation. Always enabled when LogFileMaxSize is enabled.
# Default: no
#LogRotate yes
# Enable Prelude output.
# Default: no
#PreludeEnable yes
#
# Set the name of the analyzer used by prelude-admin.
# Default: ClamAV
#PreludeAnalyzerName ClamAV
# Log additional information about the infected file, such as its
# size and hash, together with the virus name.
#ExtendedDetectionInfo yes
# This option allows you to save a process identifier of the listening
# daemon (main thread).
# This file will be owned by root, as long as clamd was started by root.
# It is recommended that the directory where this file is stored is
# also owned by root to keep other users from tampering with it.
# Default: disabled
PidFile /run/lock/clamd.pid
# Optional path to the global temporary directory.
# Default: system specific (usually /tmp or /var/tmp).
#TemporaryDirectory /var/tmp
# Path to the database directory.
# Default: hardcoded (depends on installation options)
#DatabaseDirectory /var/lib/clamav
# Only load the official signatures published by the ClamAV project.
# Default: no
#OfficialDatabaseOnly no
# The daemon can work in local mode, network mode or both.
# Due to security reasons we recommend the local mode.
# Path to a local socket file the daemon will listen on.
# Default: disabled (must be specified by a user)
LocalSocket /run/clamav/clamd.sock
# Sets the group ownership on the unix socket.
# Default: disabled (the primary group of the user running clamd)
#LocalSocketGroup virusgroup
# Sets the permissions on the unix socket to the specified mode.
# Default: disabled (socket is world accessible)
#LocalSocketMode 660
# Remove stale socket after unclean shutdown.
# Default: yes
#FixStaleSocket yes
# TCP port address.
# Default: no
TCPSocket 3310
# TCP address.
# By default we bind to INADDR_ANY, probably not wise.
# Enable the following to provide some degree of protection
# from the outside world. This option can be specified multiple
# times if you want to listen on multiple IPs. IPv6 is now supported.
# Default: no
TCPAddr 0.0.0.0
# Maximum length the queue of pending connections may grow to.
# Default: 200
#MaxConnectionQueueLength 30
# Clamd uses FTP-like protocol to receive data from remote clients.
# If you are using clamav-milter to balance load between remote clamd daemons
# on firewall servers you may need to tune the options below.
# Close the connection when the data size limit is exceeded.
# The value should match your MTA's limit for a maximum attachment size.
# Default: 25M
StreamMaxLength 100M
# Limit port range.
# Default: 1024
#StreamMinPort 30000
# Default: 2048
#StreamMaxPort 32000
# Maximum number of threads running at the same time.
# Default: 10
#MaxThreads 20
# Waiting for data from a client socket will timeout after this time (seconds).
# Default: 120
#ReadTimeout 300
# This option specifies the time (in seconds) after which clamd should
# timeout if a client doesn't provide any initial command after connecting.
# Default: 30
#CommandReadTimeout 30
# This option specifies how long to wait (in milliseconds) if the send buffer
# is full.
# Keep this value low to prevent clamd hanging.
#
# Default: 500
#SendBufTimeout 200
# Maximum number of queued items (including those being processed by
# MaxThreads threads).
# It is recommended to have this value at least twice MaxThreads if possible.
# WARNING: you shouldn't increase this too much to avoid running out of file
# descriptors, the following condition should hold:
# MaxThreads*MaxRecursion + (MaxQueue - MaxThreads) + 6< RLIMIT_NOFILE (usual
# max is 1024).
#
# Default: 100
#MaxQueue 200
# Waiting for a new job will timeout after this time (seconds).
# Default: 30
#IdleTimeout 60
# Don't scan files and directories matching regex
# This directive can be used multiple times
# Default: scan all
#ExcludePath ^/proc/
#ExcludePath ^/sys/
# Maximum depth directories are scanned at.
# Default: 15
#MaxDirectoryRecursion 20
# Follow directory symlinks.
# Default: no
#FollowDirectorySymlinks yes
# Follow regular file symlinks.
# Default: no
#FollowFileSymlinks yes
# Scan files and directories on other filesystems.
# Default: yes
#CrossFilesystems yes
# Perform a database check.
# Default: 600 (10 min)
#SelfCheck 600
# Enable non-blocking (multi-threaded/concurrent) database reloads.
# This feature will temporarily load a second scanning engine while scanning
# continues using the first engine. Once loaded, the new engine takes over.
# The old engine is removed as soon as all scans using the old engine have
# completed.
# This feature requires more RAM, so this option is provided in case users are
# willing to block scans during reload in exchange for lower RAM requirements.
# Default: yes
ConcurrentDatabaseReload no
# Execute a command when virus is found. In the command string %v will
# be replaced with the virus name and %f will be replaced with the file name.
# Additionally, two environment variables will be defined: $CLAM_VIRUSEVENT_FILENAME
# and $CLAM_VIRUSEVENT_VIRUSNAME.
# Default: no
#VirusEvent /usr/local/bin/send_sms 123456789 "VIRUS ALERT: %v in %f"
# Run as another user (clamd must be started by root for this option to work)
# Default: don't drop privileges
User clamav
# Stop daemon when libclamav reports out of memory condition.
#ExitOnOOM yes
# Don't fork into background.
# Default: no
#Foreground yes
# Enable debug messages in libclamav.
# Default: no
#Debug yes
# Do not remove temporary files (for debug purposes).
# Default: no
#LeaveTemporaryFiles yes
# Permit use of the ALLMATCHSCAN command. If set to no, clamd will reject
# any ALLMATCHSCAN command as invalid.
# Default: yes
#AllowAllMatchScan no
# Detect Possibly Unwanted Applications.
# Default: no
#DetectPUA yes
# Exclude a specific PUA category. This directive can be used multiple times.
# See https://github.com/vrtadmin/clamav-faq/blob/master/faq/faq-pua.md for
# the complete list of PUA categories.
# Default: Load all categories (if DetectPUA is activated)
#ExcludePUA NetTool
#ExcludePUA PWTool
# Only include a specific PUA category. This directive can be used multiple
# times.
# Default: Load all categories (if DetectPUA is activated)
#IncludePUA Spy
#IncludePUA Scanner
#IncludePUA RAT
# This option causes memory or nested map scans to dump the content to disk.
# If you turn on this option, more data is written to disk and is available
# when the LeaveTemporaryFiles option is enabled.
#ForceToDisk yes
# This option allows you to disable the caching feature of the engine. By
# default, the engine will store an MD5 in a cache of any files that are
# not flagged as virus or that hit limits checks. Disabling the cache will
# have a negative performance impact on large scans.
# Default: no
#DisableCache yes
# In some cases (eg. complex malware, exploits in graphic files, and others),
# ClamAV uses special algorithms to detect abnormal patterns and behaviors that
# may be malicious. This option enables alerting on such heuristically
# detected potential threats.
# Default: yes
#HeuristicAlerts yes
# Allow heuristic alerts to take precedence.
# When enabled, if a heuristic scan (such as phishingScan) detects
# a possible virus/phish it will stop scan immediately. Recommended, saves CPU
# scan-time.
# When disabled, virus/phish detected by heuristic scans will be reported only
# at the end of a scan. If an archive contains both a heuristically detected
# virus/phish, and a real malware, the real malware will be reported
#
# Keep this disabled if you intend to handle "Heuristics.*" viruses
# differently from "real" malware.
# If a non-heuristically-detected virus (signature-based) is found first,
# the scan is interrupted immediately, regardless of this config option.
#
# Default: no
#HeuristicScanPrecedence yes
##
## Heuristic Alerts
##
# With this option clamav will try to detect broken executables (both PE and
# ELF) and alert on them with the Broken.Executable heuristic signature.
# Default: no
#AlertBrokenExecutables yes
# With this option clamav will try to detect broken media file (JPEG,
# TIFF, PNG, GIF) and alert on them with a Broken.Media heuristic signature.
# Default: no
#AlertBrokenMedia yes
# Alert on encrypted archives _and_ documents with heuristic signature
# (encrypted .zip, .7zip, .rar, .pdf).
# Default: no
#AlertEncrypted yes
# Alert on encrypted archives with heuristic signature (encrypted .zip, .7zip,
# .rar).
# Default: no
#AlertEncryptedArchive yes
# Alert on encrypted archives with heuristic signature (encrypted .pdf).
# Default: no
#AlertEncryptedDoc yes
# With this option enabled OLE2 files containing VBA macros, which were not
# detected by signatures will be marked as "Heuristics.OLE2.ContainsMacros".
# Default: no
#AlertOLE2Macros yes
# Alert on SSL mismatches in URLs, even if the URL isn't in the database.
# This can lead to false positives.
# Default: no
#AlertPhishingSSLMismatch yes
# Alert on cloaked URLs, even if URL isn't in database.
# This can lead to false positives.
# Default: no
#AlertPhishingCloak yes
# Alert on raw DMG image files containing partition intersections
# Default: no
#AlertPartitionIntersection yes
##
## Executable files
##
# PE stands for Portable Executable - it's an executable file format used
# in all 32 and 64-bit versions of Windows operating systems. This option
# allows ClamAV to perform a deeper analysis of executable files and it's also
# required for decompression of popular executable packers such as UPX, FSG,
# and Petite. If you turn off this option, the original files will still be
# scanned, but without additional processing.
# Default: yes
#ScanPE yes
# Certain PE files contain an authenticode signature. By default, we check
# the signature chain in the PE file against a database of trusted and
# revoked certificates if the file being scanned is marked as a virus.
# If any certificate in the chain validates against any trusted root, but
# does not match any revoked certificate, the file is marked as trusted.
# If the file does match a revoked certificate, the file is marked as virus.
# The following setting completely turns off authenticode verification.
# Default: no
#DisableCertCheck yes
# Executable and Linking Format is a standard format for UN*X executables.
# This option allows you to control the scanning of ELF files.
# If you turn off this option, the original files will still be scanned, but
# without additional processing.
# Default: yes
#ScanELF yes
##
## Documents
##
# This option enables scanning of OLE2 files, such as Microsoft Office
# documents and .msi files.
# If you turn off this option, the original files will still be scanned, but
# without additional processing.
# Default: yes
#ScanOLE2 yes
# This option enables scanning within PDF files.
# If you turn off this option, the original files will still be scanned, but
# without decoding and additional processing.
# Default: yes
#ScanPDF yes
# This option enables scanning within SWF files.
# If you turn off this option, the original files will still be scanned, but
# without decoding and additional processing.
# Default: yes
#ScanSWF yes
# This option enables scanning xml-based document files supported by libclamav.
# If you turn off this option, the original files will still be scanned, but
# without additional processing.
# Default: yes
#ScanXMLDOCS yes
# This option enables scanning of HWP3 files.
# If you turn off this option, the original files will still be scanned, but
# without additional processing.
# Default: yes
#ScanHWP3 yes
##
## Mail files
##
# Enable internal e-mail scanner.
# If you turn off this option, the original files will still be scanned, but
# without parsing individual messages/attachments.
# Default: yes
#ScanMail yes
# Scan RFC1341 messages split over many emails.
# You will need to periodically clean up $TemporaryDirectory/clamav-partial
# directory.
# WARNING: This option may open your system to a DoS attack.
# Never use it on loaded servers.
# Default: no
#ScanPartialMessages yes
# With this option enabled ClamAV will try to detect phishing attempts by using
# HTML.Phishing and Email.Phishing NDB signatures.
# Default: yes
#PhishingSignatures no
# With this option enabled ClamAV will try to detect phishing attempts by
# analyzing URLs found in emails using WDB and PDB signature databases.
# Default: yes
#PhishingScanURLs no
##
## Data Loss Prevention (DLP)
##
# Enable the DLP module
# Default: No
#StructuredDataDetection yes
# This option sets the lowest number of Credit Card numbers found in a file
# to generate a detect.
# Default: 3
#StructuredMinCreditCardCount 5
# With this option enabled the DLP module will search for valid Credit Card
# numbers only. Debit and Private Label cards will not be searched.
# Default: no
#StructuredCCOnly yes
# This option sets the lowest number of Social Security Numbers found
# in a file to generate a detect.
# Default: 3
#StructuredMinSSNCount 5
# With this option enabled the DLP module will search for valid
# SSNs formatted as xxx-yy-zzzz
# Default: yes
#StructuredSSNFormatNormal yes
# With this option enabled the DLP module will search for valid
# SSNs formatted as xxxyyzzzz
# Default: no
#StructuredSSNFormatStripped yes
##
## HTML
##
# Perform HTML normalisation and decryption of MS Script Encoder code.
# Default: yes
# If you turn off this option, the original files will still be scanned, but
# without additional processing.
#ScanHTML yes
##
## Archives
##
# ClamAV can scan within archives and compressed files.
# If you turn off this option, the original files will still be scanned, but
# without unpacking and additional processing.
# Default: yes
#ScanArchive yes
##
## Limits
##
# The options below protect your system against Denial of Service attacks
# using archive bombs.
# This option sets the maximum amount of time to a scan may take.
# In this version, this field only affects the scan time of ZIP archives.
# Value of 0 disables the limit.
# Note: disabling this limit or setting it too high may result allow scanning
# of certain files to lock up the scanning process/threads resulting in a
# Denial of Service.
# Time is in milliseconds.
# Default: 120000
MaxScanTime 300000
# This option sets the maximum amount of data to be scanned for each input
# file. Archives and other containers are recursively extracted and scanned
# up to this value.
# Value of 0 disables the limit
# Note: disabling this limit or setting it too high may result in severe damage
# to the system.
# Default: 100M
MaxScanSize 1024M
# Files larger than this limit won't be scanned. Affects the input file itself
# as well as files contained inside it (when the input file is an archive, a
# document or some other kind of container).
# Value of 0 disables the limit.
# Note: disabling this limit or setting it too high may result in severe damage
# to the system.
# Technical design limitations prevent ClamAV from scanning files greater than
# 2 GB at this time.
# Default: 25M
MaxFileSize 1024M
# Nested archives are scanned recursively, e.g. if a Zip archive contains a RAR
# file, all files within it will also be scanned. This options specifies how
# deeply the process should be continued.
# Note: setting this limit too high may result in severe damage to the system.
# Default: 17
#MaxRecursion 10
# Number of files to be scanned within an archive, a document, or any other
# container file.
# Value of 0 disables the limit.
# Note: disabling this limit or setting it too high may result in severe damage
# to the system.
# Default: 10000
#MaxFiles 15000
# Maximum size of a file to check for embedded PE. Files larger than this value
# will skip the additional analysis step.
# Note: disabling this limit or setting it too high may result in severe damage
# to the system.
# Default: 10M
#MaxEmbeddedPE 10M
# Maximum size of a HTML file to normalize. HTML files larger than this value
# will not be normalized or scanned.
# Note: disabling this limit or setting it too high may result in severe damage
# to the system.
# Default: 10M
#MaxHTMLNormalize 10M
# Maximum size of a normalized HTML file to scan. HTML files larger than this
# value after normalization will not be scanned.
# Note: disabling this limit or setting it too high may result in severe damage
# to the system.
# Default: 2M
#MaxHTMLNoTags 2M
# Maximum size of a script file to normalize. Script content larger than this
# value will not be normalized or scanned.
# Note: disabling this limit or setting it too high may result in severe damage
# to the system.
# Default: 5M
#MaxScriptNormalize 5M
# Maximum size of a ZIP file to reanalyze type recognition. ZIP files larger
# than this value will skip the step to potentially reanalyze as PE.
# Note: disabling this limit or setting it too high may result in severe damage
# to the system.
# Default: 1M
#MaxZipTypeRcg 1M
# This option sets the maximum number of partitions of a raw disk image to be
# scanned.
# Raw disk images with more partitions than this value will have up to
# the value number partitions scanned. Negative values are not allowed.
# Note: setting this limit too high may result in severe damage or impact
# performance.
# Default: 50
#MaxPartitions 128
# This option sets the maximum number of icons within a PE to be scanned.
# PE files with more icons than this value will have up to the value number
# icons scanned.
# Negative values are not allowed.
# WARNING: setting this limit too high may result in severe damage or impact
# performance.
# Default: 100
#MaxIconsPE 200
# This option sets the maximum recursive calls for HWP3 parsing during
# scanning. HWP3 files using more than this limit will be terminated and
# alert the user.
# Scans will be unable to scan any HWP3 attachments if the recursive limit
# is reached.
# Negative values are not allowed.
# WARNING: setting this limit too high may result in severe damage or impact
# performance.
# Default: 16
#MaxRecHWP3 16
# This option sets the maximum calls to the PCRE match function during
# an instance of regex matching.
# Instances using more than this limit will be terminated and alert the user
# but the scan will continue.
# For more information on match_limit, see the PCRE documentation.
# Negative values are not allowed.
# WARNING: setting this limit too high may severely impact performance.
# Default: 100000
#PCREMatchLimit 20000
# This option sets the maximum recursive calls to the PCRE match function
# during an instance of regex matching.
# Instances using more than this limit will be terminated and alert the user
# but the scan will continue.
# For more information on match_limit_recursion, see the PCRE documentation.
# Negative values are not allowed and values > PCREMatchLimit are superfluous.
# WARNING: setting this limit too high may severely impact performance.
# Default: 2000
#PCRERecMatchLimit 10000
# This option sets the maximum filesize for which PCRE subsigs will be
# executed. Files exceeding this limit will not have PCRE subsigs executed
# unless a subsig is encompassed to a smaller buffer.
# Negative values are not allowed.
# Setting this value to zero disables the limit.
# WARNING: setting this limit too high or disabling it may severely impact
# performance.
# Default: 25M
#PCREMaxFileSize 100M
# When AlertExceedsMax is set, files exceeding the MaxFileSize, MaxScanSize, or
# MaxRecursion limit will be flagged with the virus name starting with
# "Heuristics.Limits.Exceeded".
# Default: no
#AlertExceedsMax yes
##
## On-access Scan Settings
##
# Don't scan files larger than OnAccessMaxFileSize
# Value of 0 disables the limit.
# Default: 5M
#OnAccessMaxFileSize 10M
# Max number of scanning threads to allocate to the OnAccess thread pool at
# startup. These threads are the ones responsible for creating a connection
# with the daemon and kicking off scanning after an event has been processed.
# To prevent clamonacc from consuming all clamd's resources keep this lower
# than clamd's max threads.
# Default: 5
#OnAccessMaxThreads 10
# Max amount of time (in milliseconds) that the OnAccess client should spend
# for every connect, send, and recieve attempt when communicating with clamd
# via curl.
# Default: 5000 (5 seconds)
# OnAccessCurlTimeout 10000
# Toggles dynamic directory determination. Allows for recursively watching
# include paths.
# Default: no
#OnAccessDisableDDD yes
# Set the include paths (all files inside them will be scanned). You can have
# multiple OnAccessIncludePath directives but each directory must be added
# in a separate line.
# Default: disabled
#OnAccessIncludePath /home
#OnAccessIncludePath /students
# Set the exclude paths. All subdirectories are also excluded.
# Default: disabled
#OnAccessExcludePath /home/user
# Modifies fanotify blocking behaviour when handling permission events.
# If off, fanotify will only notify if the file scanned is a virus,
# and not perform any blocking.
# Default: no
#OnAccessPrevention yes
# When using prevention, if this option is turned on, any errors that occur
# during scanning will result in the event attempt being denied. This could
# potentially lead to unwanted system behaviour with certain configurations,
# so the client defaults this to off and prefers allowing access events in
# case of scan or connection error.
# Default: no
#OnAccessDenyOnError yes
# Toggles extra scanning and notifications when a file or directory is
# created or moved.
# Requires the DDD system to kick-off extra scans.
# Default: no
#OnAccessExtraScanning yes
# Set the mount point to be scanned. The mount point specified, or the mount
# point containing the specified directory will be watched. If any directories
# are specified, this option will preempt (disable and ignore all options
# related to) the DDD system. This option will result in verdicts only.
# Note that prevention is explicitly disallowed to prevent common, fatal
# misconfigurations. (e.g. watching "/" with prevention on and no exclusions
# made on vital system directories)
# It can be used multiple times.
# Default: disabled
#OnAccessMountPath /
#OnAccessMountPath /home/user
# With this option you can exclude the root UID (0). Processes run under
# root with be able to access all files without triggering scans or
# permission denied events.
# Note that if clamd cannot check the uid of the process that generated an
# on-access scan event (e.g., because OnAccessPrevention was not enabled, and
# the process already exited), clamd will perform a scan. Thus, setting
# OnAccessExcludeRootUID is not *guaranteed* to prevent every access by the
# root user from triggering a scan (unless OnAccessPrevention is enabled).
# Default: no
#OnAccessExcludeRootUID no
# With this option you can exclude specific UIDs. Processes with these UIDs
# will be able to access all files without triggering scans or permission
# denied events.
# This option can be used multiple times (one per line).
# Using a value of 0 on any line will disable this option entirely.
# To exclude the root UID (0) please enable the OnAccessExcludeRootUID
# option.
# Also note that if clamd cannot check the uid of the process that generated an
# on-access scan event (e.g., because OnAccessPrevention was not enabled, and
# the process already exited), clamd will perform a scan. Thus, setting
# OnAccessExcludeUID is not *guaranteed* to prevent every access by the
# specified uid from triggering a scan (unless OnAccessPrevention is enabled).
# Default: disabled
#OnAccessExcludeUID -1
# This option allows exclusions via user names when using the on-access
# scanning client. It can be used multiple times.
# It has the same potential race condition limitations of the
# OnAccessExcludeUID option.
# Default: disabled
#OnAccessExcludeUname clamav
# Number of times the OnAccess client will retry a failed scan due to
# connection problems (or other issues).
# Default: 0
#OnAccessRetryAttempts 3
##
## Bytecode
##
# With this option enabled ClamAV will load bytecode from the database.
# It is highly recommended you keep this option on, otherwise you'll miss
# detections for many new viruses.
# Default: yes
#Bytecode yes
# Set bytecode security level.
# Possible values:
# None - No security at all, meant for debugging.
# DO NOT USE THIS ON PRODUCTION SYSTEMS.
# This value is only available if clamav was built
# with --enable-debug!
# TrustSigned - Trust bytecode loaded from signed .c[lv]d files, insert
# runtime safety checks for bytecode loaded from other sources.
# Paranoid - Don't trust any bytecode, insert runtime checks for all.
# Recommended: TrustSigned, because bytecode in .cvd files already has these
# checks.
# Note that by default only signed bytecode is loaded, currently you can only
# load unsigned bytecode in --enable-debug mode.
#
# Default: TrustSigned
#BytecodeSecurity TrustSigned
# Allow loading bytecode from outside digitally signed .c[lv]d files.
# **Caution**: You should NEVER run bytecode signatures from untrusted sources.
# Doing so may result in arbitrary code execution.
# Default: no
#BytecodeUnsigned yes
# Set bytecode timeout in milliseconds.
#
# Default: 5000
# BytecodeTimeout 1000

View File

@ -0,0 +1,2 @@
This directory needs to contain all certificates needed by this cockroachdb node. Those can be generated by the steps
outlined in the README in the root directory, under "Setting up CockroachDB".

View File

@ -0,0 +1,11 @@
FROM node:16.1.0-alpine
WORKDIR /opt/hsd
RUN apk update && apk add bash unbound-dev gmp-dev g++ gcc make python2 git
RUN git clone https://github.com/handshake-org/hsd.git /opt/hsd
RUN npm install --production
ENV PATH="${PATH}:/opt/hsd/bin:/opt/hsd/node_modules/.bin"
ENTRYPOINT ["hsd"]

View File

@ -0,0 +1,31 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"recovery": {
"via": "email"
}
}
}
},
"required": ["email"],
"additionalProperties": true
}
}
}

View File

@ -0,0 +1,86 @@
version: v0.5.5-alpha.1
dsn: memory
serve:
public:
base_url: http://127.0.0.1/
cors:
enabled: true
admin:
base_url: http://127.0.0.1/admin/
selfservice:
default_browser_return_url: http://127.0.0.1/
whitelisted_return_urls:
- http://127.0.0.1/
methods:
password:
enabled: true
flows:
error:
ui_url: http://127.0.0.1/error
settings:
ui_url: http://127.0.0.1/settings
privileged_session_max_age: 15m
recovery:
enabled: true
ui_url: http://127.0.0.1/recovery
verification:
enabled: true
ui_url: http://127.0.0.1/verify
after:
default_browser_return_url: http://127.0.0.1/
logout:
after:
default_browser_return_url: http://127.0.0.1/auth/login
login:
ui_url: http://127.0.0.1/auth/login
lifespan: 10m
registration:
lifespan: 10m
ui_url: http://127.0.0.1/auth/registration
after:
password:
hooks:
- hook: session
log:
level: debug
format: text
leak_sensitive_values: true
password:
max_breaches: 100
secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
session:
cookie:
domain: account.siasky.net
lifespan: "720h"
hashers:
argon2:
parallelism: 1
memory: 131072
iterations: 2
salt_length: 16
key_length: 16
identity:
default_schema_url: file:///etc/config/kratos/identity.schema.json
courier:
smtp:
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true

View File

@ -0,0 +1,37 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
},
"website": {
"type": "object"
}
},
"required": ["website", "email"],
"additionalProperties": false
}
}
}

View File

@ -0,0 +1,17 @@
local claims = {
email_verified: false
} + std.extVar('claims');
{
identity: {
traits: {
// Allowing unverified email addresses enables account
// enumeration attacks, especially if the value is used for
// e.g. verification or as a password login identifier.
//
// Therefore we only return the email if it (a) exists and (b) is marked verified
// by GitHub.
[if "email" in claims && claims.email_verified then "email" else null]: claims.email,
},
},
}

View File

@ -0,0 +1,7 @@
This directory needs to contain all certificates needed by this cockroachdb node. Those can be generated by the steps
outlined in the README in the root directory, under "Setting up CockroachDB".
The only difference between the files here and those under
`docker/cockroach/certs` is that the files here need to be readable by anyone, while the files under `cockroach` need to
have their original access rights
(all *.key files should be 600 instead of 644 there).

View File

@ -0,0 +1,116 @@
- id: "ory:kratos:public"
upstream:
preserve_host: true
url: "http://kratos:4433"
strip_path: /.ory/kratos/public
match:
url: "http://oathkeeper:4455/.ory/kratos/public/<**>"
methods:
- GET
- POST
- PUT
- DELETE
- PATCH
authenticators:
- handler: noop
authorizer:
handler: allow
mutators:
- handler: noop
- id: "dashboard:anonymous"
upstream:
preserve_host: true
url: "http://dashboard:3000"
match:
url: "http://oathkeeper:4455/<{_next/**,auth/**,recovery,verify,error,favicon.ico}{/,}>"
methods:
- GET
authenticators:
- handler: anonymous
authorizer:
handler: allow
mutators:
- handler: noop
- id: "dashboard:protected"
upstream:
preserve_host: true
url: "http://dashboard:3000"
match:
url: "http://oathkeeper:4455/<{,api/**,settings,uploads,downloads,payments}>"
methods:
- GET
- POST
- PUT
- DELETE
- PATCH
authenticators:
- handler: cookie_session
authorizer:
handler: allow
mutators:
- handler: id_token
- handler: header
config:
headers:
X-User: "{{ print .Subject }}"
errors:
- handler: redirect
config:
to: http://127.0.0.1/auth/login
- id: "accounts:anonymous"
upstream:
preserve_host: true
url: "http://accounts:3000"
match:
url: "http://oathkeeper<{,:4455}>/<{health,stripe/prices,stripe/webhook}>"
methods:
- GET
- POST
authenticators:
- handler: anonymous
authorizer:
handler: allow
mutators:
- handler: noop
- id: "accounts:public"
upstream:
preserve_host: true
url: "http://accounts:3000"
match:
url: "http://oathkeeper<{,:4455}>/<{user/limits}>"
methods:
- GET
authenticators:
- handler: cookie_session
- handler: noop
authorizer:
handler: allow
mutators:
- handler: id_token
- id: "accounts:protected"
upstream:
preserve_host: true
url: "http://accounts:3000"
match:
url: "http://oathkeeper<{,:4455}>/<{login,logout,user,user/uploads,user/downloads,user/stats}>"
methods:
- GET
- POST
- PUT
- DELETE
- PATCH
authenticators:
- handler: cookie_session
authorizer:
handler: allow
mutators:
- handler: id_token
errors:
- handler: redirect
config:
to: http://127.0.0.1/auth/login

View File

@ -0,0 +1,94 @@
log:
level: debug
format: json
serve:
proxy:
cors:
enabled: true
allowed_origins:
- "*"
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
allowed_headers:
- Authorization
- Content-Type
exposed_headers:
- Content-Type
allow_credentials: true
debug: true
errors:
fallback:
- json
handlers:
redirect:
enabled: true
config:
to: http://127.0.0.1/auth/login
when:
- error:
- unauthorized
- forbidden
request:
header:
accept:
- text/html
json:
enabled: true
config:
verbose: true
access_rules:
matching_strategy: glob
repositories:
- file:///etc/config/oathkeeper/access-rules.yml
authenticators:
anonymous:
enabled: true
config:
subject: guest
cookie_session:
enabled: true
config:
check_session_url: http://kratos:4433/sessions/whoami
preserve_path: true
extra_from: "@this"
subject_from: "identity.id"
only:
- ory_kratos_session
noop:
enabled: true
authorizers:
allow:
enabled: true
mutators:
noop:
enabled: true
header:
enabled: true
config:
headers:
X-User: "{{ print .Subject }}"
id_token:
enabled: true
config:
issuer_url: http://oathkeeper:4455/
jwks_url: file:///etc/config/oathkeeper/id_token.jwks.json
ttl: 720h
claims: |
{
"session": {{ .Extra | toJson }}
}

7
docker/nginx/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM openresty/openresty:1.19.3.1-8-bionic
# RUN apt-get update -qq && apt-get install cron logrotate -qq
RUN luarocks install luasocket
# CMD ["sh", "-c", "service cron start;", "/usr/local/openresty/bin/openresty -g daemon off;"]
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]

View File

@ -0,0 +1,185 @@
# Dockerfile - Ubuntu Bionic
# https://github.com/openresty/docker-openresty
ARG RESTY_IMAGE_BASE="ubuntu"
ARG RESTY_IMAGE_TAG="bionic"
FROM ${RESTY_IMAGE_BASE}:${RESTY_IMAGE_TAG}
LABEL maintainer="Evan Wies <evan@neomantra.net>"
# Docker Build Arguments
ARG RESTY_IMAGE_BASE="ubuntu"
ARG RESTY_IMAGE_TAG="bionic"
ARG RESTY_VERSION="1.19.3.1"
ARG RESTY_LUAROCKS_VERSION="3.5.0"
ARG RESTY_OPENSSL_VERSION="1.1.1i"
ARG RESTY_OPENSSL_PATCH_VERSION="1.1.1f"
ARG RESTY_OPENSSL_URL_BASE="https://www.openssl.org/source"
ARG RESTY_PCRE_VERSION="8.44"
ARG RESTY_J="1"
ARG RESTY_CONFIG_OPTIONS="\
--with-compat \
--with-file-aio \
--with-http_addition_module \
--with-http_auth_request_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_geoip_module=dynamic \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_image_filter_module=dynamic \
--with-http_mp4_module \
--with-http_random_index_module \
--with-http_realip_module \
--with-http_secure_link_module \
--with-http_slice_module \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-http_v2_module \
--with-http_xslt_module=dynamic \
--with-ipv6 \
--with-mail \
--with-mail_ssl_module \
--with-md5-asm \
--with-pcre-jit \
--with-sha1-asm \
--with-stream \
--with-stream_ssl_module \
--with-threads \
"
ARG RESTY_CONFIG_OPTIONS_MORE=""
ARG RESTY_LUAJIT_OPTIONS="--with-luajit-xcflags='-DLUAJIT_NUMMODE=2 -DLUAJIT_ENABLE_LUA52COMPAT'"
ARG RESTY_ADD_PACKAGE_BUILDDEPS=""
ARG RESTY_ADD_PACKAGE_RUNDEPS=""
ARG RESTY_EVAL_PRE_CONFIGURE=""
ARG RESTY_EVAL_POST_MAKE=""
# These are not intended to be user-specified
ARG _RESTY_CONFIG_DEPS="--with-pcre \
--with-cc-opt='-DNGX_LUA_ABORT_AT_PANIC -I/usr/local/openresty/pcre/include -I/usr/local/openresty/openssl/include' \
--with-ld-opt='-L/usr/local/openresty/pcre/lib -L/usr/local/openresty/openssl/lib -Wl,-rpath,/usr/local/openresty/pcre/lib:/usr/local/openresty/openssl/lib' \
"
LABEL resty_image_base="${RESTY_IMAGE_BASE}"
LABEL resty_image_tag="${RESTY_IMAGE_TAG}"
LABEL resty_version="${RESTY_VERSION}"
LABEL resty_luarocks_version="${RESTY_LUAROCKS_VERSION}"
LABEL resty_openssl_version="${RESTY_OPENSSL_VERSION}"
LABEL resty_openssl_patch_version="${RESTY_OPENSSL_PATCH_VERSION}"
LABEL resty_openssl_url_base="${RESTY_OPENSSL_URL_BASE}"
LABEL resty_pcre_version="${RESTY_PCRE_VERSION}"
LABEL resty_config_options="${RESTY_CONFIG_OPTIONS}"
LABEL resty_config_options_more="${RESTY_CONFIG_OPTIONS_MORE}"
LABEL resty_config_deps="${_RESTY_CONFIG_DEPS}"
LABEL resty_add_package_builddeps="${RESTY_ADD_PACKAGE_BUILDDEPS}"
LABEL resty_add_package_rundeps="${RESTY_ADD_PACKAGE_RUNDEPS}"
LABEL resty_eval_pre_configure="${RESTY_EVAL_PRE_CONFIGURE}"
LABEL resty_eval_post_make="${RESTY_EVAL_POST_MAKE}"
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
gettext-base \
libgd-dev \
libgeoip-dev \
libncurses5-dev \
libperl-dev \
libreadline-dev \
libxslt1-dev \
make \
perl \
unzip \
zlib1g-dev \
${RESTY_ADD_PACKAGE_BUILDDEPS} \
${RESTY_ADD_PACKAGE_RUNDEPS} \
&& cd /tmp \
&& if [ -n "${RESTY_EVAL_PRE_CONFIGURE}" ]; then eval $(echo ${RESTY_EVAL_PRE_CONFIGURE}); fi \
&& curl -fSL "${RESTY_OPENSSL_URL_BASE}/openssl-${RESTY_OPENSSL_VERSION}.tar.gz" -o openssl-${RESTY_OPENSSL_VERSION}.tar.gz \
&& tar xzf openssl-${RESTY_OPENSSL_VERSION}.tar.gz \
&& cd openssl-${RESTY_OPENSSL_VERSION} \
&& if [ $(echo ${RESTY_OPENSSL_VERSION} | cut -c 1-5) = "1.1.1" ] ; then \
echo 'patching OpenSSL 1.1.1 for OpenResty' \
&& curl -s https://raw.githubusercontent.com/openresty/openresty/master/patches/openssl-${RESTY_OPENSSL_PATCH_VERSION}-sess_set_get_cb_yield.patch | patch -p1 ; \
fi \
&& if [ $(echo ${RESTY_OPENSSL_VERSION} | cut -c 1-5) = "1.1.0" ] ; then \
echo 'patching OpenSSL 1.1.0 for OpenResty' \
&& curl -s https://raw.githubusercontent.com/openresty/openresty/ed328977028c3ec3033bc25873ee360056e247cd/patches/openssl-1.1.0j-parallel_build_fix.patch | patch -p1 \
&& curl -s https://raw.githubusercontent.com/openresty/openresty/master/patches/openssl-${RESTY_OPENSSL_PATCH_VERSION}-sess_set_get_cb_yield.patch | patch -p1 ; \
fi \
&& ./config \
no-threads shared zlib -g \
enable-ssl3 enable-ssl3-method \
--prefix=/usr/local/openresty/openssl \
--libdir=lib \
-Wl,-rpath,/usr/local/openresty/openssl/lib \
&& make -j${RESTY_J} \
&& make -j${RESTY_J} install_sw \
&& cd /tmp \
&& curl -fSL https://ftp.pcre.org/pub/pcre/pcre-${RESTY_PCRE_VERSION}.tar.gz -o pcre-${RESTY_PCRE_VERSION}.tar.gz \
&& tar xzf pcre-${RESTY_PCRE_VERSION}.tar.gz \
&& cd /tmp/pcre-${RESTY_PCRE_VERSION} \
&& ./configure \
--prefix=/usr/local/openresty/pcre \
--disable-cpp \
--enable-jit \
--enable-utf \
--enable-unicode-properties \
&& make -j${RESTY_J} \
&& make -j${RESTY_J} install \
&& cd /tmp \
&& curl -fSL https://openresty.org/download/openresty-${RESTY_VERSION}.tar.gz -o openresty-${RESTY_VERSION}.tar.gz \
&& tar xzf openresty-${RESTY_VERSION}.tar.gz \
&& cd /tmp/openresty-${RESTY_VERSION} \
&& eval ./configure -j${RESTY_J} ${_RESTY_CONFIG_DEPS} ${RESTY_CONFIG_OPTIONS} ${RESTY_CONFIG_OPTIONS_MORE} ${RESTY_LUAJIT_OPTIONS} \
&& make -j${RESTY_J} \
&& make -j${RESTY_J} install \
&& cd /tmp \
&& rm -rf \
openssl-${RESTY_OPENSSL_VERSION}.tar.gz openssl-${RESTY_OPENSSL_VERSION} \
pcre-${RESTY_PCRE_VERSION}.tar.gz pcre-${RESTY_PCRE_VERSION} \
openresty-${RESTY_VERSION}.tar.gz openresty-${RESTY_VERSION} \
&& curl -fSL https://luarocks.github.io/luarocks/releases/luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz -o luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
&& tar xzf luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
&& cd luarocks-${RESTY_LUAROCKS_VERSION} \
&& ./configure \
--prefix=/usr/local/openresty/luajit \
--with-lua=/usr/local/openresty/luajit \
--lua-suffix=jit-2.1.0-beta3 \
--with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1 \
&& make build \
&& make install \
&& cd /tmp \
&& if [ -n "${RESTY_EVAL_POST_MAKE}" ]; then eval $(echo ${RESTY_EVAL_POST_MAKE}); fi \
&& rm -rf luarocks-${RESTY_LUAROCKS_VERSION} luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
&& if [ -n "${RESTY_ADD_PACKAGE_BUILDDEPS}" ]; then DEBIAN_FRONTEND=noninteractive apt-get remove -y --purge ${RESTY_ADD_PACKAGE_BUILDDEPS} ; fi \
&& DEBIAN_FRONTEND=noninteractive apt-get autoremove -y \
&& mkdir -p /var/run/openresty \
&& ln -sf /dev/stdout /usr/local/openresty/nginx/logs/access.log \
&& ln -sf /dev/stderr /usr/local/openresty/nginx/logs/error.log
# Add additional binaries into PATH for convenience
ENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin
# Add LuaRocks paths
# If OpenResty changes, these may need updating:
# /usr/local/openresty/bin/resty -e 'print(package.path)'
# /usr/local/openresty/bin/resty -e 'print(package.cpath)'
ENV LUA_PATH="/usr/local/openresty/site/lualib/?.ljbc;/usr/local/openresty/site/lualib/?/init.ljbc;/usr/local/openresty/lualib/?.ljbc;/usr/local/openresty/lualib/?/init.ljbc;/usr/local/openresty/site/lualib/?.lua;/usr/local/openresty/site/lualib/?/init.lua;/usr/local/openresty/lualib/?.lua;/usr/local/openresty/lualib/?/init.lua;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua"
ENV LUA_CPATH="/usr/local/openresty/site/lualib/?.so;/usr/local/openresty/lualib/?.so;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so"
# Copy nginx configuration files
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY nginx.vh.default.conf /etc/nginx/conf.d/default.conf
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
# Use SIGQUIT instead of default SIGTERM to cleanly drain requests
# See https://github.com/openresty/docker-openresty/blob/master/README.md#tips--pitfalls
STOPSIGNAL SIGQUIT

View File

@ -0,0 +1,536 @@
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=skynet:10m max_size=10g inactive=24h use_temp_path=off;
# this runs before forking out nginx worker processes
init_by_lua_block {
require "cjson"
require "socket.http"
}
# ratelimit specified IPs
geo $limit {
default 0;
include /etc/nginx/conf.d/include/ratelimited;
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $binary_remote_addr zone=uploads_by_ip:10m rate=10r/s;
limit_req_zone $limit_key zone=uploads_by_ip_throttled:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=registry_access_by_ip:10m rate=60r/m;
limit_req_zone $limit_key zone=registry_access_by_ip_throttled:10m rate=20r/m;
limit_conn_zone $binary_remote_addr zone=upload_conn:10m;
limit_conn_zone $limit_key zone=upload_conn_rl:10m;
limit_conn_zone $binary_remote_addr zone=downloads_by_ip:10m;
limit_req_status 429;
limit_conn_status 429;
# since we are proxying request to nginx from caddy, access logs will contain caddy's ip address
# as the request address so we need to use real_ip_header module to use ip address from
# X-Forwarded-For header as a real ip address of the request
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 127.0.0.1/32;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
# skynet-jwt contains dash so we cannot use $cookie_skynet-jwt
# https://richardhart.me/2012/03/18/logging-nginx-cookies-with-dashes/
map $http_cookie $skynet_jwt {
default '';
~skynet-jwt=(?<match>[^\;]+) $match;
}
upstream siad {
server sia:9980;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
# understand the regex https://regex101.com/r/BGQvi6/6
server_name "~^(((?<base32_subdomain>([a-z0-9]{55}))|(?<hns_domain>[^\.]+)\.hns)\.)?((?<portal_domain>[^.]+)\.)?(?<domain>[^.]+)\.(?<tld>[^.]+)$";
# ddos protection: closing slow connections
client_body_timeout 5s;
client_header_timeout 5s;
# Increase the body buffer size, to ensure the internal POSTs can always
# parse the full POST contents into memory.
client_body_buffer_size 128k;
client_max_body_size 128k;
# legacy endpoint rewrite
rewrite ^/portals /skynet/portals permanent;
rewrite ^/stats /skynet/stats permanent;
rewrite ^/skynet/blacklist /skynet/blocklist permanent;
rewrite ^/account/(.*) https://account.$domain.$tld/$1 permanent;
# This is only safe workaround to reroute based on some conditions
# See https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/
recursive_error_pages on;
# redirect links with base32 encoded skylink in subdomain
error_page 460 = @base32_subdomain;
if ($base32_subdomain != "") {
return 460;
}
# redirect links with handshake domain on hns subdomain
error_page 461 = @hns_domain;
if ($hns_domain != "") {
return 461;
}
location / {
include /etc/nginx/conf.d/include/cors;
proxy_pass http://website:9000;
}
location /docs {
proxy_pass https://skynetlabs.github.io/skynet-docs;
}
location /skynet/blocklist {
include /etc/nginx/conf.d/include/cors;
proxy_cache skynet;
proxy_cache_valid any 1m; # cache blocklist for 1 minute
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://siad/skynet/blocklist;
}
location /skynet/portals {
include /etc/nginx/conf.d/include/cors;
proxy_cache skynet;
proxy_cache_valid any 1m; # cache portals for 1 minute
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://siad/skynet/portals;
}
location /skynet/stats {
include /etc/nginx/conf.d/include/cors;
set $response_body ''; # we need a variable for full response body (not chuncked)
# modify the response to add numfiles and totalsize to account for node rotation
# example prevstats.lua: 'return { numfiles = 12345, totalsize = 123456789 }'
body_filter_by_lua_block {
local file_exists = io.open("/data/nginx/skynet/prevstats.lua")
if file_exists then
file_exists.close()
-- because response data is chunked, we need to concat ngx.arg[1] until
-- last chunk is received (when ngx.arg[2] is set to true)
ngx.var.response_body = ngx.var.response_body .. ngx.arg[1]
if ngx.arg[2] then
local json = require('cjson')
local prevstats = require('/data/nginx/skynet/prevstats')
local stats = json.decode(ngx.var.response_body)
stats.uploadstats.numfiles = stats.uploadstats.numfiles + prevstats.numfiles
stats.uploadstats.totalsize = stats.uploadstats.totalsize + prevstats.totalsize
ngx.arg[1] = json.encode(stats)
else
-- do not send any data in this chunk (wait for last chunk)
ngx.arg[1] = nil
end
end
}
proxy_cache skynet;
proxy_cache_valid any 1m; # cache stats for 1 minute
proxy_set_header User-Agent: Sia-Agent;
proxy_read_timeout 5m; # extend the read timeout
proxy_pass http://siad/skynet/stats;
}
location /health-check {
include /etc/nginx/conf.d/include/cors;
access_log off; # do not log traffic to health-check endpoint
proxy_pass http://10.10.10.60:3100; # hardcoded ip because health-check waits for nginx
}
location /hns {
include /etc/nginx/conf.d/include/proxy-buffer;
# variable definititions - we need to define a variable to be able to access it in lua by ngx.var.something
set $skylink ''; # placeholder for the raw 46 bit skylink
set $rest ''; # placeholder for the rest of the url that gets appended to skylink (path and args)
# resolve handshake domain by requesting to /hnsres endpoint and assign correct values to $skylink and $rest
access_by_lua_block {
local json = require('cjson')
-- match the request_uri and extract the hns domain and anything that is passed in the uri after it
-- example: /hns/something/foo/bar?baz=1 matches:
-- > hns_domain_name: something
-- > request_uri_rest: /foo/bar/?baz=1
local hns_domain_name, request_uri_rest = string.match(ngx.var.request_uri, "/hns/([^/?]+)(.*)")
-- make a get request to /hnsres endpoint with the domain name from request_uri
local hnsres_res = ngx.location.capture("/hnsres/" .. hns_domain_name)
-- we want to fail with a generic 404 when /hnsres returns anything but 200 OK with a skylink
if hnsres_res.status ~= ngx.HTTP_OK then
ngx.exit(ngx.HTTP_NOT_FOUND)
end
-- since /hnsres endpoint response is a json, we need to decode it before we access it
-- example response: '{"skylink":"sia://XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"}'
local hnsres_json = json.decode(hnsres_res.body)
-- define local variable containing rest of the skylink if provided
local skylink_rest
if hnsres_json.skylink then
-- try to match the skylink with sia:// prefix
skylink, skylink_rest = string.match(hnsres_json.skylink, "sia://([^/?]+)(.*)")
-- in case the skylink did not match, assume that there is no sia:// prefix and try to match again
if skylink == nil then
skylink, skylink_rest = string.match(hnsres_json.skylink, "/?([^/?]+)(.*)")
end
elseif hnsres_json.registry then
local publickey = hnsres_json.registry.publickey
local datakey = hnsres_json.registry.datakey
-- make a get request to /skynet/registry endpoint with the credentials from text record
local registry_res = ngx.location.capture("/skynet/registry/cached?publickey=" .. publickey .. "&datakey=" .. datakey)
-- we want to fail with a generic 404 when /skynet/registry returns anything but 200 OK
if registry_res.status ~= ngx.HTTP_OK then
ngx.exit(ngx.HTTP_NOT_FOUND)
end
-- since /skynet/registry endpoint response is a json, we need to decode it before we access it
local registry_json = json.decode(registry_res.body)
-- response will contain a hex encoded skylink, we need to decode it
local data = (registry_json.data:gsub('..', function (cc)
return string.char(tonumber(cc, 16))
end))
skylink = data
end
-- fail with a generic 404 if skylink has not been extracted from a valid /hnsres response for some reason
if not skylink then
ngx.exit(ngx.HTTP_NOT_FOUND)
end
ngx.var.skylink = skylink
if request_uri_rest == "/" and skylink_rest ~= nil and skylink_rest ~= "" and skylink_rest ~= "/" then
ngx.var.rest = skylink_rest
else
ngx.var.rest = request_uri_rest
end
}
# we proxy to another nginx location rather than directly to siad because we don't want to deal with caching here
proxy_pass http://127.0.0.1/$skylink$rest;
# in case siad returns location header, we need to replace the skylink with the domain name
header_filter_by_lua_block {
if ngx.header.location then
-- match hns domain from the request_uri
local hns_domain_name = string.match(ngx.var.request_uri, "/hns/([^/?]+)")
-- match location redirect part after the skylink
local location_rest = string.match(ngx.header.location, "[^/?]+(.*)");
-- because siad will set the location header to ie. XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg/index.html
-- we need to replace the skylink with the domain_name so we are not redirected to skylink
ngx.header.location = hns_domain_name .. location_rest
end
}
}
location /hnsres {
include /etc/nginx/conf.d/include/cors;
proxy_pass http://handshake-api:3100;
}
# internal registry endpoint that caches calls for a certain period of time
# it is not suitable for every registry call but some requests might be cached
# and we are using it currently for caching registry resolutions from /hns calls
location /skynet/registry/cached {
internal; # internal endpoint only
access_log off; # do not log traffic
proxy_cache skynet;
proxy_cache_key publickey=$arg_publickey&datakey=$arg_datakey; # cache based on publickey and datakey
proxy_cache_valid 200 30s; # cache only 200 responses and only for 30 seconds
proxy_cache_lock on; # queue cache requests for the same resource until it is fully cached
proxy_cache_bypass $cookie_nocache $arg_nocache; # add cache bypass option
proxy_pass http://127.0.0.1/skynet/registry$is_args$args;
}
location /skynet/registry {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/sia-auth;
limit_req zone=registry_access_by_ip burst=600 nodelay;
limit_req zone=registry_access_by_ip_throttled burst=200 nodelay;
proxy_set_header User-Agent: Sia-Agent;
proxy_read_timeout 600; # siad should timeout with 404 after 5 minutes
proxy_pass http://siad/skynet/registry;
access_by_lua_block {
-- this block runs only when accounts are enabled
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
local res = ngx.location.capture("/accounts/user/limits", { copy_all_vars = true })
if res.status == ngx.HTTP_OK then
local json = require('cjson')
local limits = json.decode(res.body)
if limits.registry > 0 then
ngx.sleep(limits.registry / 1000)
end
end
}
# register the registry access in accounts service (cookies should contain jwt)
log_by_lua_block {
-- this block runs only when accounts are enabled
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
if ngx.status == ngx.HTTP_OK or ngx.status == ngx.HTTP_NOT_FOUND then
local http = require("socket.http")
local method = ngx.req.get_method() == ngx.HTTP_GET and "read" or "write"
local ok, statusCode, headers, statusText = http.request {
url = "http://accounts:3000/track/registry/" .. method,
method = "POST",
headers = ngx.req.get_headers()
}
if statusCode ~= ngx.HTTP_NO_CONTENT and statusCode ~= ngx.HTTP_UNAUTHORIZED then
ngx.log(ngx.ERR, "accounts endpoint /track/registry/" .. method .. " failed with error " .. statusCode)
end
end
}
}
location /skynet/skyfile {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/sia-auth;
limit_req zone=uploads_by_ip burst=100 nodelay;
limit_req zone=uploads_by_ip_throttled;
limit_conn upload_conn 10;
limit_conn upload_conn_rl 1;
client_max_body_size 1000M; # make sure to limit the size of upload to a sane value
proxy_read_timeout 600;
proxy_request_buffering off; # stream uploaded files through the proxy as it comes in
proxy_set_header Expect $http_expect;
proxy_set_header User-Agent: Sia-Agent;
# Extract 2 sets of 2 characters from $request_id and assign to $dir1, $dir2
# respectfully. The rest of the $request_id is going to be assigned to $dir3.
# We use those variables to automatically generate a unique path for the uploaded file.
# This ensures that not all uploaded files end up in the same directory, which is something
# that causes performance issues in the renter.
# Example path result: /af/24/9bc5ec894920ccc45634dc9a8065
if ($request_id ~* "(\w{2})(\w{2})(\w+)") {
set $dir1 $1;
set $dir2 $2;
set $dir3 $3;
}
# access_by_lua_block {
# -- this block runs only when accounts are enabled
# if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
# ngx.var.upload_limit_rate = 5 * 1024 * 1024
# local res = ngx.location.capture("/accounts/user", { copy_all_vars = true })
# if res.status == ngx.HTTP_OK then
# local json = require('cjson')
# local user = json.decode(res.body)
# ngx.var.upload_limit_rate = ngx.var.upload_limit_rate * (user.tier + 1)
# end
# }
# proxy this call to siad endpoint (make sure the ip is correct)
proxy_pass http://siad/skynet/skyfile/$dir1/$dir2/$dir3$is_args$args;
# register the upload in accounts service (cookies should contain jwt)
log_by_lua_block {
-- this block runs only when accounts are enabled
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
local skylink = ngx.header["Skynet-Skylink"]
if skylink and ngx.status >= ngx.HTTP_OK and ngx.status < ngx.HTTP_SPECIAL_RESPONSE then
local http = require("socket.http")
local ok, statusCode, headers, statusText = http.request {
url = "http://accounts:3000/track/upload/" .. skylink,
method = "POST",
headers = ngx.req.get_headers()
}
if statusCode ~= ngx.HTTP_NO_CONTENT and statusCode ~= ngx.HTTP_UNAUTHORIZED then
ngx.log(ngx.ERR, "accounts endpoint /track/upload/" .. skylink .. " failed with error " .. statusCode)
end
end
}
}
# endpoing implementing resumable file uploads open protocol https://tus.io
location /skynet/tus {
include /etc/nginx/conf.d/include/cors;
client_max_body_size 1000M; # make sure to limit the size of upload to a sane value
proxy_read_timeout 600;
proxy_request_buffering off; # stream uploaded files through the proxy as it comes in
proxy_set_header Expect $http_expect;
# proxy /skynet/tus requests to siad endpoint with all arguments
proxy_pass http://siad;
}
location /skynet/metadata {
include /etc/nginx/conf.d/include/cors;
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://siad;
}
location ~ "^/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/proxy-buffer;
include /etc/nginx/conf.d/include/proxy-cache-downloads;
# redirect purge calls to separate location
error_page 462 = @purge;
if ($request_method = PURGE) {
return 462;
}
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
# we need to explicitly use set directive here because $1 will contain the skylink with
# decoded whitespaces and set will re-encode it for us before passing it to proxy_pass
set $skylink $1;
access_by_lua_block {
-- this block runs only when accounts are enabled
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
local res = ngx.location.capture("/accounts/user/limits", { copy_all_vars = true })
if res.status == ngx.HTTP_OK then
local json = require('cjson')
local limits = json.decode(res.body)
ngx.var.limit_rate = limits.download
end
}
# register the download in accounts service (cookies should contain jwt)
log_by_lua_block {
-- this block runs only when accounts are enabled
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
local skylink = ngx.header["Skynet-Skylink"]
if skylink and ngx.status >= ngx.HTTP_OK and ngx.status < ngx.HTTP_SPECIAL_RESPONSE then
local http = require("socket.http")
local query = table.concat({ "status=" .. ngx.status, "bytes=" .. ngx.var.body_bytes_sent }, "&")
local ok, statusCode, headers, statusText = http.request {
url = "http://accounts:3000/track/download/" .. skylink .. "?" .. query,
method = "POST",
headers = ngx.req.get_headers()
}
if statusCode ~= ngx.HTTP_NO_CONTENT and statusCode ~= ngx.HTTP_UNAUTHORIZED then
ngx.log(ngx.ERR, "accounts endpoint /track/download/" .. skylink .. " failed with error " .. statusCode)
end
end
}
proxy_read_timeout 600;
proxy_set_header User-Agent: Sia-Agent;
# proxy this call to siad /skynet/skylink/ endpoint (make sure the ip is correct)
proxy_pass http://siad/skynet/skylink/$skylink$is_args$args;
}
location @base32_subdomain {
include /etc/nginx/conf.d/include/proxy-buffer;
proxy_pass http://127.0.0.1/$base32_subdomain/$request_uri;
}
location @hns_domain {
include /etc/nginx/conf.d/include/proxy-buffer;
proxy_pass http://127.0.0.1/hns/$hns_domain/$request_uri;
}
location @purge {
allow 10.0.0.0/8;
allow 127.0.0.1/32;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
set $lua_purge_path "/data/nginx/cache/";
content_by_lua_file /etc/nginx/conf.d/scripts/purge-multi.lua;
}
location ~ "^/file/([a-zA-Z0-9-_]{46}(/.*)?)$" {
include /etc/nginx/conf.d/include/proxy-buffer;
rewrite /file/(.*) $1 break; # drop the /file/ prefix from uri
proxy_pass http://127.0.0.1/$uri?attachment=true&$args;
}
location /__internal/do/not/use/authenticated {
include /etc/nginx/conf.d/include/cors;
charset utf-8;
charset_types application/json;
default_type application/json;
content_by_lua_block {
local json = require('cjson')
-- this block runs only when accounts are enabled
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then
ngx.say(json.encode{authenticated = false})
return ngx.exit(ngx.HTTP_OK)
end
local res = ngx.location.capture("/accounts/user", { copy_all_vars = true })
if res.status == ngx.HTTP_OK then
local limits = json.decode(res.body)
ngx.say(json.encode{authenticated = limits.tier > 0})
return ngx.exit(ngx.HTTP_OK)
end
ngx.say(json.encode{authenticated = false})
return ngx.exit(ngx.HTTP_OK)
}
}
location /accounts {
internal; # internal endpoint only
access_log off; # do not log traffic
proxy_cache skynet; # use general nginx cache
proxy_cache_key $uri+$skynet_jwt; # include skynet-jwt cookie (mapped to skynet_jwt)
proxy_cache_valid 200 401 1m; # cache success and unauthorized responses for 1 minute
rewrite /accounts(.*) $1 break; # drop the /accounts prefix from uri
proxy_pass http://10.10.10.70:3000; # hardcoded ip because accounts might not be available
}
# include custom locations, specific to the server
include /etc/nginx/conf.d/server-override/*;
}

View File

@ -0,0 +1,18 @@
# enables gzip compression
gzip on;
# set the gzip compression level (1-9)
gzip_comp_level 6;
# tells proxies to cache both gzipped and regular versions of a resource
gzip_vary on;
# informs NGINX to not compress anything smaller than the defined size
gzip_min_length 256;
# compress data even for clients that are connecting via proxies if a response header includes
# the "expired", "no-cache", "no-store", "private", and "Authorization" parameters
gzip_proxied expired no-cache no-store private auth;
# enables the types of files that can be compressed
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

View File

@ -0,0 +1,16 @@
if ($request_method = 'OPTIONS') {
more_set_headers 'Access-Control-Allow-Origin: $http_origin';
more_set_headers 'Access-Control-Allow-Credentials: true';
more_set_headers 'Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE';
more_set_headers 'Access-Control-Allow-Headers: DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,X-HTTP-Method-Override,upload-offset,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,location';
more_set_headers 'Access-Control-Max-Age: 1728000';
more_set_headers 'Content-Type: text/plain; charset=utf-8';
more_set_headers 'Content-Length: 0';
return 204;
}
more_set_headers 'Access-Control-Allow-Origin: $http_origin';
more_set_headers 'Access-Control-Allow-Credentials: true';
more_set_headers 'Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE';
more_set_headers 'Access-Control-Allow-Headers: DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,X-HTTP-Method-Override,upload-offset,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,location';
more_set_headers 'Access-Control-Expose-Headers: Content-Length,Content-Range,Skynet-File-Metadata,Skynet-Skylink,Skynet-Portal-Api,upload-offset,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,location';

View File

@ -0,0 +1,5 @@
# if you are expecting large headers (ie. Skynet-Skyfile-Metadata), tune these values to your needs
# read more: https://www.getpagespeed.com/server-setup/nginx/tuning-proxy_buffer_size-in-nginx
proxy_buffer_size 4096k;
proxy_buffers 64 256k;
proxy_busy_buffers_size 4096k; # at least as high as proxy_buffer_size

View File

@ -0,0 +1,10 @@
proxy_cache skynet;
slice 1m;
proxy_http_version 1.1; # upgrade if necessary because 1.0 does not support byte-range requests
proxy_set_header Range $slice_range; # pass slice range to proxy
proxy_cache_key $uri$arg_format$arg_attachment$slice_range; # use just the uri path, format and attachment args and slice range
proxy_cache_min_uses 3; # cache responses after 3 requests of the same file
proxy_cache_valid 200 206 24h; # cache 200 and 206 responses for 24 hours
proxy_cache_lock on; # queue cache requests for the same resource until it is fully cached
proxy_cache_bypass $cookie_nocache $arg_nocache; # add cache bypass option
add_header X-Proxy-Cache $upstream_cache_status; # add response header to indicate cache hits and misses

View File

@ -0,0 +1,6 @@
# Add a list of IPs here that should be severely rate limited on upload.
# Note that it is possible to add IP ranges as well as the full IP address.
#
# Examples:
# 192.168.0.0/24 1;
# 79.85.222.247 1;

View File

@ -0,0 +1,15 @@
rewrite_by_lua_block {
local b64 = require("ngx.base64")
-- open apipassword file for reading (b flag is required for some reason)
-- (file /etc/.sia/apipassword has to be mounted from the host system)
local apipassword_file = io.open("/data/sia/apipassword", "rb")
-- read apipassword file contents and trim newline (important)
local apipassword = apipassword_file:read("*all"):gsub("%s+", "")
-- make sure to close file after reading the password
apipassword_file.close()
-- encode the user:password authorization string
-- (in our case user is empty so it is just :password)
local content = b64.encode_base64url(":" .. apipassword)
-- set authorization header with proper base64 encoded string
ngx.req.set_header("Authorization", "Basic " .. content)
}

View File

@ -0,0 +1,68 @@
-- Tit Petric, Monotek d.o.o., Tue 03 Jan 2017 06:54:56 PM CET
--
-- Delete nginx cached assets with a PURGE request against an endpoint
-- supports extended regular expression PURGE requests (/upload/.*)
--
-- https://scene-si.org/2017/01/08/improving-nginx-lua-cache-purge/
--
function file_exists(name)
local f = io.open(name, "r")
if f~=nil then io.close(f) return true else return false end
end
function explode(d, p)
local t, ll
t={}
ll=0
if(#p == 1) then return {p} end
while true do
l=string.find(p, d, ll, true) -- find the next d in the string
if l~=nil then -- if "not not" found then..
table.insert(t, string.sub(p, ll, l-1)) -- Save it in our array.
ll=l+1 -- save just after where we found it for searching next time.
else
table.insert(t, string.sub(p, ll)) -- Save what's left in our array.
break -- Break at end, as it should be, according to the lua manual.
end
end
return t
end
function purge(filename)
if (file_exists(filename)) then
os.remove(filename)
end
end
function trim(s)
return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
function exec(cmd)
local handle = io.popen(cmd)
local result = handle:read("*all")
handle:close()
return trim(result)
end
function list_files(cache_path, purge_pattern)
local result = exec("/usr/bin/find " .. cache_path .. " -type f | /usr/bin/xargs --no-run-if-empty -n1000 /bin/grep -El -m 1 '^KEY: " .. purge_pattern .. "' 2>&1")
if result == "" then
return {}
end
return explode("\n", result)
end
if ngx ~= nil then
-- list all cached items matching uri
local files = list_files(ngx.var.lua_purge_path, ngx.var.uri)
ngx.header["Content-type"] = "text/plain; charset=utf-8"
ngx.header["X-Purged-Count"] = table.getn(files)
for k, v in pairs(files) do
purge(v)
end
ngx.say("OK")
ngx.exit(ngx.OK)
end

View File

@ -0,0 +1,7 @@
# Every file from within this directory will be included in the server block
# of the nginx configuration, at the very end. See client.conf.
#
# Example:
# location /blog {
# root /var/www/blog;
# }

72
docker/nginx/nginx.conf Normal file
View File

@ -0,0 +1,72 @@
# nginx.conf -- docker-openresty
#
# This file is installed to:
# `/usr/local/openresty/nginx/conf/nginx.conf`
# and is the file loaded by nginx at startup,
# unless the user specifies otherwise.
#
# It tracks the upstream OpenResty's `nginx.conf`, but removes the `server`
# section and adds this directive:
# `include /etc/nginx/conf.d/*.conf;`
#
# The `docker-openresty` file `nginx.vh.default.conf` is copied to
# `/etc/nginx/conf.d/default.conf`. It contains the `server section
# of the upstream `nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
#
user root;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
env SKYNET_PORTAL_API; # declare env variable to use it in config
env ACCOUNTS_ENABLED; # declare env variable to use it in config
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $upstream_response_time '
'$upstream_bytes_sent $upstream_bytes_received '
'"$upstream_http_content_type" "$upstream_cache_status" '
'"$portal_domain" "$upstream_http_skynet_skylink" '
'$upstream_connect_time $upstream_header_time '
'$request_time "$hns_domain"';
access_log logs/access.log main;
# See Move default writable paths to a dedicated directory (#119)
# https://github.com/openresty/docker-openresty/issues/119
client_body_temp_path /var/run/openresty/nginx-client-body;
proxy_temp_path /var/run/openresty/nginx-proxy;
fastcgi_temp_path /var/run/openresty/nginx-fastcgi;
uwsgi_temp_path /var/run/openresty/nginx-uwsgi;
scgi_temp_path /var/run/openresty/nginx-scgi;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
# include skynet-portal-api header on every request
header_filter_by_lua 'ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API")';
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/conf.extra.d/*.conf;
}

View File

@ -0,0 +1,58 @@
# nginx.vh.default.conf -- docker-openresty
#
# This file is installed to:
# `/etc/nginx/conf.d/default.conf`
#
# It tracks the `server` section of the upstream OpenResty's `nginx.conf`.
#
# This config (and any other configs in `etc/nginx/conf.d/`) is loaded by
# default by the `include` directive in `/usr/local/openresty/nginx/conf/nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
#
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/local/openresty/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/local/openresty/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root /usr/local/openresty/nginx/html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

14
docker/sia/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM golang AS sia-builder
ENV GOOS linux
ENV GOARCH amd64
ARG branch=portal-latest
RUN git clone https://gitlab.com/SkynetLabs/skyd.git Sia --single-branch --branch ${branch}
RUN make release --directory Sia
FROM firyx/sia-dev-user:latest
COPY --from=sia-builder /go/bin/skyd /usr/bin/siad
COPY --from=sia-builder /go/bin/skyc /usr/bin/siac

View File

@ -1,2 +0,0 @@
Purpose of this file is that `logs` dir can be commited to git and it will be
present on portal servers. The rest of files in `logs` dir are git ignored.

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"private": true,
"license": "SEE LICENSE IN LICENSE.md",
"workspaces": [
"packages/*"
],
"dependencies": {
"@tailwindcss/forms": "^0.2.1",
"autoprefixer": "^10.2.4",
"postcss": "^8.2.6",
"sharp": "^0.27.2",
"tailwindcss": "^2.0.4"
}
}

4
packages/dashboard/.env Normal file
View File

@ -0,0 +1,4 @@
NEXT_PUBLIC_SKYNET_PORTAL_API=https://siasky.net
NEXT_PUBLIC_SKYNET_DASHBOARD_URL=https://account.siasky.net
NEXT_PUBLIC_KRATOS_BROWSER_URL=https://account.siasky.net/.ory/kratos/public
NEXT_PUBLIC_KRATOS_PUBLIC_URL=https://account.siasky.net/.ory/kratos/public

38
packages/dashboard/.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env defaults
!.env
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
.next

View File

@ -0,0 +1,3 @@
.next
package.json
package-lock.json

View File

@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@ -0,0 +1,17 @@
FROM node:16.1.0-alpine
WORKDIR /usr/app
COPY package.json .
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn --no-lockfile
COPY public ./public
COPY src ./src
COPY styles ./styles
COPY next.config.js .
COPY postcss.config.js .
COPY tailwind.config.js .
CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|KRATOS|STRIPE' > .env.local && yarn build && yarn start"]

View File

@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -0,0 +1,5 @@
module.exports = {
future: {
webpack5: true,
},
};

View File

@ -0,0 +1,39 @@
{
"name": "dashboard",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@fontsource/metropolis": "4.3.0",
"@ory/kratos-client": "0.5.4-alpha.1",
"@stripe/react-stripe-js": "1.4.0",
"@stripe/stripe-js": "1.14.0",
"@tailwindcss/forms": "0.3.2",
"autoprefixer": "10.2.5",
"classnames": "2.3.1",
"clipboardy": "2.3.0",
"dayjs": "1.10.4",
"express-jwt": "6.0.0",
"fast-levenshtein": "3.0.0",
"formik": "2.2.6",
"http-status-codes": "2.1.4",
"ky": "0.25.1",
"next": "10.2.0",
"postcss": "8.2.14",
"prettier": "2.3.0",
"pretty-bytes": "5.6.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"skynet-js": "3.0.2",
"square": "10.0.0",
"stripe": "8.148.0",
"superagent": "6.1.0",
"swr": "0.5.6",
"tailwindcss": "2.1.2",
"yup": "0.32.9"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,62 @@
const types = {
error: {
backgroundColor: "bg-red-50",
titleColor: "text-red-800",
detailsColor: "text-red-700",
iconColor: "text-red-400",
icon: (
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
),
},
info: {
backgroundColor: "bg-blue-50",
titleColor: "text-blue-800",
detailsColor: "text-blue-700",
iconColor: "text-blue-400",
icon: (
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
),
},
};
export default function Message({ type = "info", title, items = [] }) {
const { backgroundColor, titleColor, detailsColor, iconColor, icon } = types[type];
return (
<div className={`rounded-md ${backgroundColor} p-4`}>
<div className="flex">
<div className="flex-shrink-0">
<svg
className={`h-5 w-5 ${iconColor}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
{icon}
</svg>
</div>
<div className="ml-3">
{title && <h3 className={`text-sm font-medium ${titleColor}`}>{title}</h3>}
{items.length > 0 && (
<div className={`${title ? "mt-2" : ""} text-sm ${detailsColor}`}>
<ul className={`${items.length > 1 ? "list-disc pl-5 space-y-1" : ""}`}>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,105 @@
import { useFormik, getIn, setIn } from "formik";
import classnames from "classnames";
import SelfServiceMessages from "./SelfServiceMessages";
export default function SelfServiceForm({ flow, config, fieldsConfig, title, button = "Submit" }) {
const fields = config.fields
.filter((field) => !field.name.startsWith("traits.name")) // drop name fields
.map((field) => ({ ...field, ...fieldsConfig[field.name] }))
.sort((a, b) => (a.position < b.position ? -1 : 1));
const formik = useFormik({
initialValues: fields.reduce((acc, field) => setIn(acc, field.name, field.value ?? ""), {}),
});
return (
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
{title && <h3 className="pb-5 text-lg leading-6 font-medium text-gray-900">{title}</h3>}
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form className="space-y-6" action={config.action} method={config.method}>
{fields.map((field) => (
<div key={field.name} className={classnames({ hidden: field.type === "hidden" })}>
<label htmlFor={field.name} className="block text-sm font-medium text-gray-700 mb-1">
{fieldsConfig[field.name]?.label ?? field.name}
</label>
<div>
<input
id={field.type === "hidden" ? null : field.name}
name={field.name}
type={field.type}
autoComplete={field.autoComplete}
required={field.required}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={getIn(formik.values, field.name) ?? ""}
className={classnames(
"appearance-none block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none sm:text-sm",
{
"border-gray-300 placeholder-gray-400 focus:ring-green-500 focus:border-green-500": !Boolean(
field?.messages?.length
),
"border-red-300 text-red-900 placeholder-red-300 focus:ring-red-500 focus:border-red-500": Boolean(
field?.messages?.length
),
}
)}
/>
<SelfServiceMessages messages={field.messages} />
{field.checks && (
<div className="mt-4">
<ul className="space-y-1">
{field.checks.map((check, index) => (
<li
key={index}
className={
check.validate(formik.values, field.name) ? "text-green-600 font-medium" : "text-gray-600"
}
>
<div className="flex space-x-3 items-center">
<span className="flex items-center justify-center ">
<svg
className="h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20.618 5.984A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016zM12 9v2m0 4h.01"
/>
</svg>
</span>
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<h3 className="text-xs">{check.label}</h3>
</div>
</div>
</div>
</li>
))}
</ul>
</div>
)}
</div>
</div>
))}
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
{button}
</button>
<SelfServiceMessages messages={config.messages} />
{flow && <SelfServiceMessages messages={flow.messages} />}
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
import classnames from "classnames";
// const types = {
// error: {
// backgroundColor: "bg-red-50",
// titleColor: "text-red-800",
// detailsColor: "text-red-700",
// iconColor: "text-red-400",
// icon: (
// <path
// fillRule="evenodd"
// d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
// clipRule="evenodd"
// />
// ),
// },
// info: {
// backgroundColor: "bg-blue-50",
// titleColor: "text-blue-800",
// detailsColor: "text-blue-700",
// iconColor: "text-blue-400",
// icon: (
// <path
// fillRule="evenodd"
// d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
// clipRule="evenodd"
// />
// ),
// },
// };
export default function SelfServiceMessages({ messages = [] }) {
if (!messages) return null; // make sure we don't throw on invalid data
return messages.map(({ text, type }) => (
<p
className={classnames("mt-2 text-sm", {
"text-red-600": type === "error",
"text-blue-600": type === "info",
})}
>
{text}
</p>
));
}

View File

@ -0,0 +1,327 @@
import Link from "next/link";
import { useRouter } from "next/router";
import Head from "next/head";
import ky from "ky/umd";
import { useState } from "react";
import config from "../../src/config";
export default function Layout({ title, children }) {
const [menuOpen, openMenu] = useState(false);
const [avatarDropdownOpen, openAvatarDropdown] = useState(false);
const router = useRouter();
const handleSignOut = async (e) => {
e.preventDefault();
try {
await ky.post("/logout");
window.location = `${config.kratos.browser}/self-service/browser/flows/logout`;
} catch (error) {
console.log(error); // todo: handle errors with a message
}
};
return (
<div>
<Head>
<title key="title">Skynet - {title}</title>
</Head>
<div className="bg-gray-800 pb-32">
<nav className="bg-gray-800">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="border-b border-gray-700">
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
<div className="flex items-center">
<Link href="/">
<a className="flex-shrink-0">
<svg
viewBox="19.88800048828125 37.1175193787 132.07760620117188 132.07760620117188"
width={33}
height={33}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 116.388 139.371 C 92.969 148.816 66.759 134.5 62.048 109.691 L 46.308 98.821 C 43.843 141.32 88.308 170.55 126.346 151.435 C 130.805 149.195 134.94 146.361 138.638 143.011 L 138.698 143.011 C 141.248 140.637 140.685 136.456 137.598 134.841 L 19.888 72.671 Z"
style={{ fill: "rgb(88, 181, 96)" }}
/>
<path
d="M 149.398 127.121 L 149.398 127.021 C 150.067 124.651 148.83 122.161 146.538 121.261 L 67.478 90.011 L 142.478 130.011 C 145.178 131.489 148.552 130.08 149.398 127.121 Z"
style={{ fill: "rgb(88, 181, 96)" }}
/>
<path
d="M 151.848 109.801 C 152.508 94.561 150.578 79.801 141.228 67.721 C 130.128 53.411 111.498 47.801 96.588 49.081 C 95.428 49.181 94.268 49.351 93.108 49.451 C 77.448 50.901 62.598 59.941 53.728 75.301 C 52.968 76.621 52.278 77.971 51.638 79.301 C 51.238 79.841 50.838 80.371 50.458 80.931 L 63.838 88.061 C 64.463 86.395 65.194 84.772 66.028 83.201 C 80.584 55.935 119.197 54.651 135.532 80.889 C 140.199 88.386 142.264 97.212 141.408 106.001 L 91.518 92.621 L 145.258 113.861 C 148.274 115.053 151.585 112.994 151.848 109.761 Z"
style={{ fill: "rgb(88, 181, 96)" }}
/>
</svg>
</a>
</Link>
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
<Link href="/">
<a
className={`${
router.pathname === "/"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} px-3 py-2 rounded-md text-sm font-medium`}
>
Dashboard
</a>
</Link>
<Link href="/uploads">
<a
className={`${
router.pathname === "/uploads"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} px-3 py-2 rounded-md text-sm font-medium`}
>
Your uploads
</a>
</Link>
<Link href="/downloads">
<a
className={`${
router.pathname === "/downloads"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} px-3 py-2 rounded-md text-sm font-medium`}
>
Your downloads
</a>
</Link>
<a
href={process.env.NEXT_PUBLIC_SKYNET_PORTAL_API}
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium flex items-center"
target="_blank"
rel="noopener noreferrer"
>
Upload files
<svg
className="flex-shrink-0 h-4 w-4 ml-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
</div>
</div>
<div className="hidden md:block">
<div className="ml-4 flex items-center md:ml-6">
{/* Profile dropdown */}
<div className="ml-3 relative">
<div>
<button
className="max-w-xs bg-gray-800 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
id="user-menu"
aria-haspopup="true"
onClick={() => openAvatarDropdown(!avatarDropdownOpen)}
>
<span className="sr-only">Open user menu</span>
<span className="inline-block h-8 w-8 rounded-full overflow-hidden bg-gray-100">
<svg className="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
</button>
</div>
{/*
Profile dropdown panel, show/hide based on dropdown state.
Entering: "transition ease-out duration-100"
From: "transform opacity-0 scale-95"
To: "transform opacity-100 scale-100"
Leaving: "transition ease-in duration-75"
From: "transform opacity-100 scale-100"
To: "transform opacity-0 scale-95"
*/}
{avatarDropdownOpen && (
<div
className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu"
>
<Link href="/settings">
<a className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
Settings
</a>
</Link>
<Link href="/payments">
<a className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
Payments
</a>
</Link>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
role="menuitem"
onClick={handleSignOut}
>
Sign out
</a>
</div>
)}
</div>
</div>
</div>
<div className="-mr-2 flex md:hidden">
{/* Mobile menu button */}
<button
className="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
onClick={() => openMenu(!menuOpen)}
>
<span className="sr-only">Open main menu</span>
<svg
className={`${menuOpen ? "hidden" : "block"} h-6 w-6`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg
className={`${menuOpen ? "block" : "hidden"} h-6 w-6`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
</div>
<div className={`${menuOpen ? "block" : "hidden"} border-b border-gray-700 md:hidden`}>
<div className="px-2 py-3 space-y-1 sm:px-3">
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
<Link href="/">
<a
className={`${
router.pathname === "/"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} block px-3 py-2 rounded-md text-base font-medium`}
>
Dashboard
</a>
</Link>
<Link href="/uploads">
<a
className={`${
router.pathname === "/uploads"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} block px-3 py-2 rounded-md text-base font-medium`}
>
Your uploads
</a>
</Link>
<Link href="/downloads">
<a
className={`${
router.pathname === "/downloads"
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white"
} block px-3 py-2 rounded-md text-base font-medium`}
>
Your downloads
</a>
</Link>
<a
href={process.env.NEXT_PUBLIC_SKYNET_PORTAL_API}
className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium flex items-center"
target="_blank"
rel="noopener noreferrer"
>
Upload files
<svg
className="flex-shrink-0 h-4 w-4 ml-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
<div className="pt-4 pb-3 border-t border-gray-700">
{/* <div className="flex items-center px-5">
<div className="flex-shrink-0">
<span className="inline-block h-10 w-10 rounded-full overflow-hidden bg-gray-100">
<svg className="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
</div>
<div className="ml-3">
<div className="text-base font-medium leading-none text-white">John Doe</div>
<div className="text-sm font-medium leading-none text-gray-400">john@example.com</div>
</div>
</div> */}
<div className="mt-3 px-2 space-y-1">
<Link href="/settings">
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">
Settings
</a>
</Link>
<Link href="/payments">
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">
Payments
</a>
</Link>
<a
href="#"
onClick={handleSignOut}
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700 cursor-pointer"
>
Sign out
</a>
</div>
</div>
</div>
</nav>
<header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-white">{title}</h1>
</div>
</header>
</div>
<main className="-mt-32">
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
{children || (
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96" />
</div>
)}
</div>
</main>
<footer className="max-w-7xl mx-auto py-4 sm:py-6 px-4 sm:px-6 md:flex md:items-center md:justify-between lg:px-8">
<p className="text-center text-sm text-gray-400">© 2021 Skynet Labs Inc. All rights reserved.</p>
</footer>
</div>
);
}

View File

@ -0,0 +1,128 @@
import { useEffect } from "react";
import classnames from "classnames";
function Button({ children, disabled, className, ...props }) {
return (
<button
type="button"
className={classnames(
"inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white",
{
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
"cursor-auto opacity-50": disabled,
},
className
)}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
export default function Table({ items, count, headers, actions, offset, setOffset, pageSize = 10 }) {
useEffect(() => {
if (offset < 0) setOffset(0);
else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize);
else if (offset % pageSize) setOffset(offset - (offset % pageSize));
}, [offset, pageSize, setOffset]);
return (
<div className="flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{headers.map(({ key, name }) => (
<th
key={key}
scope="col"
className="px-6 py-3 whitespace-nowrap text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{name}
</th>
))}
{actions.map(({ key, name }) => (
<th key={key} scope="col" className="relative px-6 py-3">
<span className="sr-only">{name}</span>
</th>
))}
</tr>
</thead>
<tbody>
{items && items.length ? (
items.map((row, index) => (
<tr className={index % 2 ? "bg-gray-100" : "bg-white"} key={index}>
{headers.map(({ key, formatter, href, nowrap = true }) => (
<td
key={key}
className={`${nowrap ? "whitespace-nowrap" : ""} px-6 py-4 text-sm font-medium text-gray-900`}
>
{(formatter ? (
formatter(row, key)
) : href ? (
<a
href={href(row, key)}
className="text-green-600 hover:text-green-900"
target="_blank"
rel="noopener noreferrer"
>
{row[key]}
</a>
) : (
row[key]
)) || <>&mdash;</>}
</td>
))}
{actions.map(({ key, name, action }) => (
<td key={key} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="#" className="text-green-600 hover:text-green-900" onClick={action}>
{name}
</a>
</td>
))}
</tr>
))
) : (
<tr className="bg-white">
<td colspan={headers.length + actions.length} className="text-center py-6 text-sm text-gray-500">
no entries
</td>
</tr>
)}
</tbody>
</table>
{/* This example requires Tailwind CSS v2.0+ */}
<nav
className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{count ? offset + 1 : 0}</span> to{" "}
<span className="font-medium">{offset + pageSize > count ? count : offset + pageSize}</span> of{" "}
<span className="font-medium">{count}</span> results
</p>
</div>
<div className="flex-1 flex justify-between sm:justify-end">
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)}>
Previous
</Button>
<Button
className="ml-3"
disabled={offset + pageSize >= count}
onClick={() => setOffset(offset + pageSize)}
>
Next
</Button>
</div>
</nav>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,14 @@
export default {
// https://github.com/ory/kratos-selfservice-ui-node#configuration
kratos: {
// The URL where ORY Kratos's Public API is located at. If this app and ORY Kratos are running in the same
// private network, this should be the private network address (e.g. kratos-public.svc.cluster.local)
public: process.env.NEXT_PUBLIC_KRATOS_PUBLIC_URL.replace(/\/+$/, ""),
// The URL where ORY Kratos's public API is located, when accessible from the public internet via ORY Oathkeeper.
// This could be for example http://kratos.my-app.com/.
browser: process.env.NEXT_PUBLIC_KRATOS_BROWSER_URL.replace(/\/+$/, ""),
},
tiers: {
starter: { id: "starter", tier: 1, name: "Free", description: "Pin up to 100GB" },
},
};

View File

@ -0,0 +1,21 @@
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import Head from "next/head";
import "tailwindcss/tailwind.css";
import "@fontsource/metropolis/all.css";
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
function MyApp({ Component, pageProps }) {
return (
<Elements stripe={stripePromise}>
<Head>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<title key="title">Skynet</title>
</Head>
<Component {...pageProps} />
</Elements>
);
}
export default MyApp;

View File

@ -0,0 +1,19 @@
import superagent from "superagent";
export default async (req, res) => {
if (req.cookies.ory_kratos_session) {
try {
const { header } = await superagent
.post("http://oathkeeper:4455/login")
.set("cookie", `ory_kratos_session=${req.cookies.ory_kratos_session}`);
res.setHeader("Set-Cookie", header["set-cookie"]);
res.redirect(req.query.return_to ?? "/");
} catch (error) {
// credentials were correct but accounts service failed
res.redirect("/.ory/kratos/public/self-service/browser/flows/logout");
}
} else {
res.redirect("/auth/login"); // redirect to login page if kratos session is missing
}
};

View File

@ -0,0 +1,28 @@
import ky from "ky/umd";
import Stripe from "stripe";
import { StatusCodes } from "http-status-codes";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const getStripeCustomer = (stripeCustomerId = null) => {
if (stripeCustomerId) {
return stripe.customers.retrieve(stripeCustomerId);
}
return stripe.customers.create();
};
export default async (req, res) => {
try {
const authorization = req.headers.authorization; // authorization header from request
const { stripeCustomerId } = await ky("http://accounts:3000/user", { headers: { authorization } }).json();
const customer = await getStripeCustomer(stripeCustomerId);
const session = await stripe.billingPortal.sessions.create({
customer: customer.id,
return_url: `${process.env.SKYNET_DASHBOARD_URL}/payments`,
});
res.redirect(session.url);
} catch ({ message }) {
res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
}
};

View File

@ -0,0 +1,58 @@
import ky from "ky/umd";
import Stripe from "stripe";
import { StatusCodes } from "http-status-codes";
import { isPaidTier } from "../../../services/tiers";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const getStripeCustomer = async (user, authorization) => {
if (user.stripeCustomerId) {
return stripe.customers.retrieve(user.stripeCustomerId);
}
const customer = await stripe.customers.create();
// update user instance and include the customer id once created
await ky.put(`http://accounts:3000/user`, { headers: { authorization }, json: { stripeCustomerId: customer.id } });
return customer;
};
export default async (req, res) => {
if (req.method !== "POST") {
return res.status(StatusCodes.NOT_FOUND).end();
}
const { price } = req.body;
if (!price) {
return res.status(StatusCodes.BAD_REQUEST).json({ error: { message: "Missing 'price' attribute" } });
}
try {
const authorization = req.headers.authorization; // authorization header from request
const user = await ky("http://accounts:3000/user", { headers: { authorization } }).json();
if (isPaidTier(user.tier)) {
const message = `Customer can have only one active subscription at a time, use Stripe Customer Portal to manage active subscription`;
return res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
}
const customer = await getStripeCustomer(user, authorization);
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price, quantity: 1 }],
customer: customer.id,
client_reference_id: user.sub,
allow_promotion_codes: true,
success_url: `${process.env.SKYNET_DASHBOARD_URL}/payments?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.SKYNET_DASHBOARD_URL}/payments`,
});
res.json({ sessionId: session.id });
} catch (error) {
res.status(StatusCodes.BAD_REQUEST).json({ error: { message: error.message } });
}
};

View File

@ -0,0 +1,23 @@
import ky from "ky/umd";
import Stripe from "stripe";
import { StatusCodes } from "http-status-codes";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export default async (req, res) => {
try {
const authorization = req.headers.authorization; // authorization header from request
const { stripeCustomerId } = await ky("http://accounts:3000/user", { headers: { authorization } }).json();
const stripeCustomer = await stripe.customers.retrieve(stripeCustomerId, { expand: ["subscriptions"] });
const { subscriptions } = stripeCustomer;
// todo: find a better way to get current subscription
if (subscriptions.total_count) {
return res.json(subscriptions.data[0]);
}
res.status(StatusCodes.NO_CONTENT).end();
} catch ({ message }) {
res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
}
};

View File

@ -0,0 +1,37 @@
export default (req, res) => {
res.json([
{
id: "price_1IReYFIzjULiPWN6DqN2DwjN",
name: "Skynet Extreme",
description: "Skynet Extreme description",
tier: 4,
price: 80,
currency: "usd",
stripe: "price_1IReYFIzjULiPWN6DqN2DwjN",
productId: "prod_J3m6IuVyh3XOc5",
livemode: false,
},
{
id: "price_1IReY5IzjULiPWN6AxPytHEG",
name: "Skynet Pro",
description: "Skynet Pro description",
tier: 3,
price: 20,
currency: "usd",
stripe: "price_1IReY5IzjULiPWN6AxPytHEG",
productId: "prod_J3m6ioQg90kZj5",
livemode: false,
},
{
id: "price_1IReXpIzjULiPWN66PvsxHL4",
name: "Skynet Plus",
description: "Skynet Plus description",
tier: 2,
price: 5,
currency: "usd",
stripe: "price_1IReXpIzjULiPWN66PvsxHL4",
productId: "prod_J3m6xMfDiz2LGE",
livemode: false,
},
]);
};

View File

@ -0,0 +1,5 @@
import user from "./user.json";
export default (req, res) => {
res.json(user);
};

View File

@ -0,0 +1,12 @@
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"sub": "ab776d6d-f324-4fa7-4k21-7587d5215481",
"tier": 1,
"subscribedUntil": "0001-01-01T00:00:00Z",
"subscriptionStatus": "active",
"subscriptionCancelAt": "2021-04-21T00:00:00Z",
"subscriptionCancelAtPeriodEnd": true,
"stripeCustomerId": "cus_J0iYnAp6LRgsTI"
}

View File

@ -0,0 +1,8 @@
import items from "./downloads.json";
export default (req, res) => {
const offset = parseInt(req.query?.offset ?? 0, 10);
const pageSize = parseInt(req.query?.pageSize ?? 10, 10);
res.json({ items: items.slice(offset, offset + pageSize), count: items.length, pageSize, offset });
};

View File

@ -0,0 +1,44 @@
[
{
"id": 1111,
"skylink": "PAL0w4SdA5rFCDGEutgpeQ50Om-YkBabtXVOJAkmedslKw",
"name": "ugabuga.pdf",
"size": 123123,
"downloadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 2222,
"skylink": "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg",
"name": "ugabuga.pdf",
"size": 8912739812,
"downloadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 3333,
"skylink": "IADUs8d9CQjUO34LmdaaNPK_STuZo24rpKVfYW3wPPM2uQ",
"name": "ugabuga.pdf",
"size": 123123,
"downloadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 4444,
"skylink": "_A2zt5SKoqwnnZU4cBF8uBycSKULXMyeg1c5ZISBr2Q3dA",
"name": "ugabuga.pdf",
"size": 83943,
"downloadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 5555,
"skylink": "AAC0uO43g64ULpyrW0zO3bjEknSFbAhm8c-RFP21EQlmSQ",
"name": "ugabuga.pdf",
"size": 3290489120,
"downloadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 6666,
"skylink": "CACqf4NlIMlA0CCCieYGjpViPGyfyJ4v1x3bmuCKZX8FKA",
"name": "ugabuga.pdf",
"size": 1290389,
"downloadedOn": "2020-04-02T08:02:17-05:00"
}
]

View File

@ -0,0 +1,5 @@
import stats from "./stats.json";
export default (req, res) => {
res.json(stats);
};

View File

@ -0,0 +1,13 @@
{
"storageUsed": 809500672,
"numRegReads": 0,
"numRegWrites": 0,
"numUploads": 13,
"numDownloads": 78,
"totalUploadsSize": 618649028,
"totalDownloadsSize": 32307956843,
"bwUploads": 2810183680,
"bwDownloads": 32323934976,
"bwRegReads": 0,
"bwRegWrites": 0
}

View File

@ -0,0 +1,8 @@
import items from "./uploads.json";
export default (req, res) => {
const offset = parseInt(req.query?.offset ?? 0, 10);
const pageSize = parseInt(req.query?.pageSize ?? 10, 10);
res.json({ items: items.slice(offset, offset + pageSize), count: items.length, pageSize, offset });
};

View File

@ -0,0 +1,44 @@
[
{
"id": 1111,
"skylink": "PAL0w4SdA5rFCDGEutgpeQ50Om-YkBabtXVOJAkmedslKw",
"name": "ugabuga.pdf",
"size": 123123,
"uploadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 2222,
"skylink": "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg",
"name": "ugabuga.pdf",
"size": 8912739812,
"uploadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 3333,
"skylink": "IADUs8d9CQjUO34LmdaaNPK_STuZo24rpKVfYW3wPPM2uQ",
"name": "ugabuga.pdf",
"size": 123123,
"uploadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 4444,
"skylink": "_A2zt5SKoqwnnZU4cBF8uBycSKULXMyeg1c5ZISBr2Q3dA",
"name": "ugabuga.pdf",
"size": 83943,
"uploadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 5555,
"skylink": "AAC0uO43g64ULpyrW0zO3bjEknSFbAhm8c-RFP21EQlmSQ",
"name": "ugabuga.pdf",
"size": 3290489120,
"uploadedOn": "2020-04-02T08:02:17-05:00"
},
{
"id": 6666,
"skylink": "CACqf4NlIMlA0CCCieYGjpViPGyfyJ4v1x3bmuCKZX8FKA",
"name": "ugabuga.pdf",
"size": 1290389,
"uploadedOn": "2020-04-02T08:02:17-05:00"
}
]

View File

@ -0,0 +1,96 @@
import Link from "next/link";
import { Configuration, PublicApi } from "@ory/kratos-client";
import config from "../../config";
import SelfServiceForm from "../../components/Form/SelfServiceForm";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export async function getServerSideProps(context) {
const flow = context.query.flow;
const redirect = encodeURIComponent(`/api/accounts/login?return_to=${context.query.return_to ?? "/"}`);
if (process.env.NODE_ENV === "development") {
return { props: { flow: require("../../../stubs/login.json") } };
}
// The flow is used to identify the login and registration flow and
// return data like the csrf_token and so on.
if (!flow || typeof flow !== "string") {
// No flow ID found in URL, initializing login flow.
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/login/browser?return_to=${redirect}`,
},
};
}
try {
const { status, data } = await kratos.getSelfServiceLoginFlow(flow);
if (status === 200) return { props: { flow: data } };
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/login/browser?return_to=${redirect}`,
},
};
}
}
const fieldsConfig = {
identifier: {
label: "Email address",
autoComplete: "email",
position: 0,
},
password: {
label: "Password",
autoComplete: "current-password",
position: 1,
},
csrf_token: {
position: 99,
},
};
export default function Login({ flow }) {
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
or{" "}
<Link href="/auth/registration">
<a className="font-medium text-green-600 hover:text-green-500">sign up</a>
</Link>{" "}
if you don't have one yet
</p>
</div>
<SelfServiceForm flow={flow} config={flow.methods.password.config} fieldsConfig={fieldsConfig} button="Sign in" />
<div className="sm:mx-auto sm:w-full sm:max-w-md text-center mt-2">
<Link href="/recovery">
<a className="text-sm font-medium text-green-600 hover:text-green-500">Forgot your password?</a>
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,113 @@
import Link from "next/link";
import { Configuration, PublicApi } from "@ory/kratos-client";
import { getIn } from "formik";
import config from "../../config";
import levenshtein from "fast-levenshtein";
import lcs from "../../services/longestCommonSequence";
import SelfServiceForm from "../../components/Form/SelfServiceForm";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export async function getServerSideProps(context) {
const flow = context.query.flow;
const redirect = encodeURIComponent(`/api/accounts/login?return_to=${context.query.return_to ?? "/"}`);
if (process.env.NODE_ENV === "development") {
return { props: { flow: require("../../../stubs/registration.json") } };
}
// The flow is used to identify the login and registration flow and
// return data like the csrf_token and so on.
if (!flow || typeof flow !== "string") {
// No flow ID found in URL, initializing registration flow.
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/registration/browser?return_to=${redirect}`,
},
};
}
try {
const { status, data } = await kratos.getSelfServiceRegistrationFlow(flow);
if (status === 200) return { props: { flow: data } };
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/registration/browser?return_to=${redirect}`,
},
};
}
}
const fieldsConfig = {
"traits.email": {
label: "Email address",
autoComplete: "email",
position: 0,
},
password: {
label: "Password",
autoComplete: "new-password",
position: 1,
checks: [
{
label: "At least 6 characters long",
validate: (values, field) => {
const value = getIn(values, field);
return value && value.length > 5;
},
},
{
label: "Significantly different from the email",
validate: (values, field) => {
const value = getIn(values, field);
const email = getIn(values, "traits.email");
// levenshtein distance higher than 5 and longest common sequence shorter than half of the password
return value && email && levenshtein.get(value, email) > 5 && lcs(value, email).length / value.length <= 0.5;
},
},
],
},
csrf_token: {
position: 99,
},
};
export default function Registration({ flow }) {
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign up for a new account</h2>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
or{" "}
<Link href="/auth/login">
<a className="font-medium text-green-600 hover:text-green-500">sign in</a>
</Link>{" "}
if you already have one
</p>
</div>
<SelfServiceForm flow={flow} config={flow.methods.password.config} fieldsConfig={fieldsConfig} button="Sign up" />
</div>
);
}

View File

@ -0,0 +1,44 @@
import dayjs from "dayjs";
import prettyBytes from "pretty-bytes";
import { useState } from "react";
import Layout from "../components/Layout";
import Table from "../components/Table";
import authServerSideProps from "../services/authServerSideProps";
import { SkynetClient } from "skynet-js";
import useAccountsApi from "../services/useAccountsApi";
const skynetClient = new SkynetClient(process.env.NEXT_PUBLIC_SKYNET_PORTAL_API);
const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss");
const headers = [
{ key: "name", name: "Name", nowrap: false, href: getSkylinkLink },
{ key: "skylink", name: "Skylink" },
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
{ key: "downloadedOn", name: "Accessed on", formatter: getRelativeDate },
];
const actions = [];
export const getServerSideProps = authServerSideProps(async (context, api) => {
const initialData = await api.get("user/downloads?pageSize=10&offset=0").json();
return { props: { initialData } };
});
export default function Downloads({ initialData }) {
const [offset, setOffset] = useState(0);
const { data } = useAccountsApi(`${apiPrefix}/user/downloads?pageSize=10&offset=${offset}`, {
initialData: offset === 0 ? initialData : undefined,
revalidateOnMount: true,
});
// preload next page if it exists (based on the response from the current page query)
const nextPageOffset = data && data.offset + data.pageSize < data.count ? data.offset + data.pageSize : offset;
useAccountsApi(`${apiPrefix}/user/downloads?pageSize=10&offset=${nextPageOffset}`);
return (
<Layout title="Your downloads">
<Table {...data} headers={headers} actions={actions} setOffset={setOffset} />
</Layout>
);
}

View File

@ -0,0 +1,68 @@
import Link from "next/link";
import { Configuration, PublicApi } from "@ory/kratos-client";
import config from "../config";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export async function getServerSideProps(context) {
const error = context.query.error;
// No error was send, redirecting back to home.
if (!error || typeof error !== "string") {
console.log("No error ID found in URL, redirecting to homepage.");
return { redirect: { permanent: false, destination: "/" } };
}
try {
const { status, data } = await kratos.getSelfServiceError(error);
if ("errors" in data) return { props: { errors: data.errors } };
throw new Error(`Expected error ${error} to contain "errors" but got ${JSON.stringify(data)}`);
} catch (error) {
return { redirect: { permanent: false, destination: "/" } };
}
}
export default function Error({ errors }) {
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">An error occurred</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
{errors.map((error, index) => (
<div className={`${index > 1 ? "mt-3 sm:mt-5" : ""} text-center`}>
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
{error.code} - {error.message}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">{error.reason}</p>
</div>
</div>
))}
</div>
</div>
<div className="text-center mt-8">
<Link href="/">
<a className="font-medium text-green-600 hover:text-green-500">back to homepage</a>
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,278 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import prettyBytes from "pretty-bytes";
import Link from "next/link";
import Layout from "../components/Layout";
import authServerSideProps from "../services/authServerSideProps";
import { SkynetClient } from "skynet-js";
import config from "../config";
import useAccountsApi from "../services/useAccountsApi";
import { isFreeTier } from "../services/tiers";
import { write } from "clipboardy";
dayjs.extend(relativeTime);
const skynetClient = new SkynetClient(process.env.NEXT_PUBLIC_SKYNET_PORTAL_API);
const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
export const getServerSideProps = authServerSideProps(async (context, api) => {
const stripe = await api.get("stripe/prices").json();
const plans = [config.tiers.starter, ...stripe].sort((a, b) => a.tier - b.tier);
return { props: { plans } };
});
function SkylinkList({ items = [], timestamp }) {
return (
<ul className="divide-y divide-gray-200">
{items.slice(0, 3).map((item) => (
<li key={item.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<a
href={skynetClient.getSkylinkUrl(item.skylink)}
className="text-sm font-medium text-green-600 hover:text-green-900 truncate"
target="_blank"
rel="noopener noreferrer"
>
{item.name || "— file name not available —"}
</a>
<abbr
className="text-xs text-gray-400 whitespace-nowrap ml-2 cursor-pointer"
title="Click to copy"
onClick={() => write(`sia://${item.skylink}`)}
>
sia://{item.skylink.substr(0, 5)}…{item.skylink.substr(-5)}
</abbr>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex">
<p className="flex items-center text-sm text-gray-500">
<svg
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
/>
</svg>
{prettyBytes(item.size)}
</p>
</div>
<div className="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
<svg
className="flex-shrink-0 mr-1.5 h-4 w-4 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{item[timestamp] && <time dateTime={item[timestamp]}>{dayjs(item[timestamp]).fromNow()}</time>}
</div>
</div>
</div>
</li>
))}
{!items.length && (
<li>
<div className="px-4 py-4 sm:px-6">
<p className="text-sm text-gray-500">no entries yet</p>
</div>
</li>
)}
</ul>
);
}
export default function Home({ plans }) {
const { data: user } = useAccountsApi(`${apiPrefix}/user`);
const { data: stats } = useAccountsApi(`${apiPrefix}/user/stats`);
const { data: downloads } = useAccountsApi(`${apiPrefix}/user/downloads?pageSize=3&offset=0`);
const { data: uploads } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=3&offset=0`);
const activePlan = plans.find(({ tier }) => (user ? user.tier === tier : isFreeTier(tier)));
return (
<Layout title="Dashboard">
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-2">
<div className="flex flex-col bg-white overflow-hidden shadow rounded-lg">
<div className="flex-grow px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-green-500 rounded-md p-3">
{/* Heroicon name: outline/users */}
<svg
className="h-6 w-6 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"
/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dt className="text-sm font-medium text-gray-500 truncate">Current plan</dt>
<dd className="flex items-baseline">
<div className="text-2xl font-semibold text-gray-900">{activePlan.name}</div>
</dd>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-4 sm:px-6">
<div className="text-sm">
<Link href="/payments">
<a className="font-medium text-green-600 hover:text-green-500 flex items-center">
Check upgrade options
<svg
className="h-4 w-4 ml-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
<svg
className="h-4 w-4 ml-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
</a>
</Link>
</div>
</div>
</div>
<div className="flex flex-col bg-white overflow-hidden shadow rounded-lg">
<div className="flex-grow px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-green-500 rounded-md p-3">
<svg
className="h-6 w-6 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dt className="text-sm font-medium text-gray-500 truncate">Storage used</dt>
<dd className="flex items-baseline">
<div className="text-2xl font-semibold text-grey-900">
{prettyBytes(stats?.totalUploadsSize ?? 0)}
</div>
</dd>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-4 sm:px-6">
<div className="text-sm">
<Link href="/uploads">
<a className="font-medium text-green-600 hover:text-green-500">View all uploads</a>
</Link>
</div>
</div>
</div>
{/* <div className="flex flex-col bg-white overflow-hidden shadow rounded-lg">
<div className="flex-grow px-4 py-5 sm:p-6">
<div className="flex items-center">
<div className="flex-shrink-0 bg-green-500 rounded-md p-3">
<svg
className="h-6 w-6 text-white transform rotate-45"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
/>
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dt className="text-sm font-medium text-gray-500 truncate">Bandwidth used</dt>
<dd className="flex items-baseline">
<div className="text-2xl font-semibold text-grey-900">{prettyBytes(stats?.bwDownloads ?? 0)}</div>
</dd>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-4 sm:px-6">
<div className="text-sm">
<Link href="/downloads">
<a className="font-medium text-green-600 hover:text-green-500">View all downloads</a>
</Link>
</div>
</div>
</div> */}
</dl>
{/* ============ */}
<div className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2">
<div className="flex flex-col">
<h3 className="pb-5 text-lg leading-6 font-medium text-gray-900">Recent downloads</h3>
{/* This example requires Tailwind CSS v2.0+ */}
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<SkylinkList items={downloads?.items} timestamp="downloadedOn" />
</div>
</div>
<div className="flex flex-col">
<h3 className="pb-5 text-lg leading-6 font-medium text-gray-900">Recent uploads</h3>
{/* This example requires Tailwind CSS v2.0+ */}
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<SkylinkList items={uploads?.items} timestamp="uploadedOn" />
</div>
</div>
</div>
</div>
</Layout>
);
}

View File

@ -0,0 +1,195 @@
import dayjs from "dayjs";
import Layout from "../components/Layout";
import ky from "ky/umd";
import { useEffect, useState } from "react";
import authServerSideProps from "../services/authServerSideProps";
import classnames from "classnames";
import prettyBytes from "pretty-bytes";
import config from "../config";
import useAccountsApi from "../services/useAccountsApi";
import { isFreeTier, isPaidTier } from "../services/tiers";
const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
const ActiveBadge = () => {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs bg-green-200 text-green-800 ml-3">
active
</span>
);
};
export const getServerSideProps = authServerSideProps(async (context, api) => {
const [user, stats, stripe] = await Promise.all([
api.get("user").json(),
api.get("user/stats").json(),
api.get("stripe/prices").json(),
]);
const plans = [config.tiers.starter, ...stripe].sort((a, b) => a.tier - b.tier);
return { props: { plans, user, stats } };
});
export default function Payments({ plans, user: initialUserData, stats: initialStatsData }) {
const { data: user } = useAccountsApi(`${apiPrefix}/user`, { initialData: initialUserData });
const { data: stats } = useAccountsApi(`${apiPrefix}/user/stats`, { initialData: initialStatsData });
const [selectedPlan, setSelectedPlan] = useState(plans.find(({ tier }) => isFreeTier(tier)));
const activePlan = plans.find(({ tier }) => (user ? user.tier === tier : isFreeTier(tier)));
const handleSubscribe = async () => {
try {
const price = selectedPlan.stripe;
const { sessionId } = await ky.post("/api/stripe/checkout", { json: { price } }).json();
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
await stripe.redirectToCheckout({ sessionId });
} catch (error) {
console.log(error); // todo: handle error
}
};
useEffect(() => {
if (activePlan && isPaidTier(activePlan.tier)) {
setSelectedPlan(activePlan);
}
}, [activePlan, selectedPlan, setSelectedPlan]);
return (
<Layout title="Payments">
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<div className="space-y-6 sm:px-6 lg:px-0 lg:col-span-9">
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<dt className="text-sm font-medium text-gray-500 truncate">Current plan</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">{activePlan?.name || "—"}</dd>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<dt className="text-sm font-medium text-gray-500 truncate">Subscription status</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900 capitalize">
{isFreeTier(activePlan?.tier) ? "—" : user?.subscriptionStatus}
</dd>
</div>
{user?.subscriptionCancelAtPeriodEnd && (
<div className="bg-gray-50 px-4 py-4 sm:px-6">
<div className="text-sm">
Your plan will be cancelled on {dayjs(user.subscriptionCancelAt).format("D MMM YYYY")}.
</div>
</div>
)}
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<dt className="text-sm font-medium text-gray-500 truncate">Storage used</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">
{prettyBytes(stats?.totalUploadsSize ?? 0)}
</dd>
</div>
</div>
</dl>
<section aria-labelledby="plan_heading">
<form action="#" method="POST">
<div className="shadow sm:rounded-md sm:overflow-hidden">
<div className="bg-white py-6 px-4 space-y-6 sm:p-6">
<div className="-ml-4 -mt-2 flex items-center justify-between flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-2">
<h3 id="plan_heading" className="text-lg leading-6 font-medium text-gray-900">
Plan
</h3>
</div>
</div>
<fieldset>
<legend className="sr-only">Pricing plans</legend>
<ul className="relative bg-white rounded-md -space-y-px">
{plans.map((plan, index) => (
<li key={plan.id}>
<label
className={`${classnames({
"rounded-tl-md rounded-tr-md": index === 0,
"rounded-bl-md rounded-br-md": index === plans.length - 1,
"bg-green-50 border-green-200 z-10": plan === selectedPlan,
"border-gray-200": plan !== selectedPlan,
"cursor-pointer": isFreeTier(user?.tier),
})} relative border p-4 flex flex-col md:pl-4 md:pr-6 md:grid md:grid-cols-3`}
>
<span className="flex items-center text-sm">
{isFreeTier(activePlan?.tier) && (
<input
name="pricing_plan"
type="radio"
className="h-4 w-4 text-orange-500 focus:ring-gray-900 border-gray-300"
aria-describedby="plan-option-pricing-0 plan-option-limit-0"
checked={plan === selectedPlan}
onChange={() => setSelectedPlan(plan)}
/>
)}
<span
className={classnames("ml-3 font-medium", {
"text-green-900": plan === selectedPlan,
"text-gray-900": plan !== selectedPlan,
})}
>
{plan.name}
</span>
{activePlan === plan && <ActiveBadge />}
</span>
<p id="plan-option-pricing-0" className="ml-6 pl-1 text-sm md:ml-0 md:pl-0 md:text-center">
<span
className={classnames("font-medium", {
"text-green-900": plan === selectedPlan,
"text-gray-900": plan !== selectedPlan,
})}
>
{plan.price ? `$${plan.price} / mo` : "no cost"}
</span>
</p>
<p
className={classnames("ml-6 pl-1 text-sm md:ml-0 md:pl-0 md:text-right", {
"text-green-700": plan === selectedPlan,
"text-gray-500": plan !== selectedPlan,
})}
>
{plan.description}
</p>
</label>
</li>
))}
</ul>
</fieldset>
</div>
<div className="px-4 py-3 bg-gray-50 sm:px-6 flex flex-col">
{user && isPaidTier(user.tier) ? (
<div className="text-sm text-gray-500 flex justify-between items-center space-x-4 md:space-x-0 flex-col md:flex-row space-y-4 md:space-y-0">
<span className="text-center md:text-left">
Use Stripe Customer Portal to manage your active subscription, payment methods and view your
billing history
</span>
<a
href="/api/stripe/billing"
className="text-right flex-shrink-0 w-full md:w-auto bg-green-800 disabled:bg-gray-300 disabled:text-gray-400 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-green-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900"
>
Stripe Customer Portal
</a>
</div>
) : (
<button
type="button"
onClick={handleSubscribe}
disabled={activePlan === selectedPlan}
className="self-end text-right w-full md:w-auto bg-green-800 disabled:bg-gray-300 disabled:text-gray-400 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-green-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900"
>
Subscribe
</button>
)}
</div>
</div>
</form>
</section>
</div>
</div>
</Layout>
);
}

View File

@ -0,0 +1,389 @@
import Layout from "../components/Layout";
export default function Payments() {
return (
<Layout title="Pricing Plans">
{/* This example requires Tailwind CSS v2.0+ */}
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:flex-col sm:align-center">
<p className="mt-5 text-xl text-gray-500 sm:text-center">
Start using for free, then add a plan to improve the experience. Account plans unlock additional features.
</p>
<div className="relative self-center mt-6 bg-gray-100 rounded-lg p-0.5 flex sm:mt-8">
<button
type="button"
className="relative w-1/2 bg-white border-gray-200 rounded-md shadow-sm py-2 text-sm font-medium text-gray-700 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-green-500 focus:z-10 sm:w-auto sm:px-8"
>
Monthly billing
</button>
<button
type="button"
className="ml-0.5 relative w-1/2 border border-transparent rounded-md py-2 text-sm font-medium text-gray-700 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-green-500 focus:z-10 sm:w-auto sm:px-8"
>
Yearly billing
</button>
</div>
</div>
<div className="mt-12 space-y-4 sm:mt-16 sm:space-y-0 sm:grid sm:grid-cols-2 sm:gap-6 lg:max-w-4xl lg:mx-auto xl:max-w-none xl:mx-0 xl:grid-cols-4">
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Free</h2>
<p className="mt-4 text-sm text-gray-500">All the basics for starting a new business</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">no cost</span>
</p>
<a
href="#"
className="opacity-50 mt-8 block w-full bg-gray-400 border border-gray-400 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
>
Active
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Potenti felis, in cras at at ligula nunc.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Orci neque eget pellentesque.</span>
</li>
</ul>
</div>
</div>
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Skynet Plus</h2>
<p className="mt-4 text-sm text-gray-500">All the basics for starting a new business</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">$5</span>
<span className="text-base font-medium text-gray-500">/mo</span>
</p>
<a
href="#"
className="mt-8 block w-full bg-gray-800 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
>
Buy Skynet Plus
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Potenti felis, in cras at at ligula nunc. </span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Orci neque eget pellentesque.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Donec mauris sit in eu tincidunt etiam.</span>
</li>
</ul>
</div>
</div>
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Skynet Pro</h2>
<p className="mt-4 text-sm text-gray-500">All the basics for starting a new business</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">$20</span>
<span className="text-base font-medium text-gray-500">/mo</span>
</p>
<a
href="#"
className="mt-8 block w-full bg-gray-800 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
>
Buy Skynet Pro
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Potenti felis, in cras at at ligula nunc. </span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Orci neque eget pellentesque.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Donec mauris sit in eu tincidunt etiam.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Faucibus volutpat magna.</span>
</li>
</ul>
</div>
</div>
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Skynet Extreme</h2>
<p className="mt-4 text-sm text-gray-500">All the basics for starting a new business</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">$80</span>
<span className="text-base font-medium text-gray-500">/mo</span>
</p>
<a
href="#"
className="mt-8 block w-full bg-gray-800 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900"
>
Buy Skynet Extreme
</a>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
<ul className="mt-6 space-y-4">
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Potenti felis, in cras at at ligula nunc. </span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Orci neque eget pellentesque.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Donec mauris sit in eu tincidunt etiam.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Faucibus volutpat magna.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Id sed tellus in varius quisque.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Risus egestas faucibus.</span>
</li>
<li className="flex space-x-3">
{/* Heroicon name: solid/check */}
<svg
className="flex-shrink-0 h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm text-gray-500">Risus cursus ullamcorper.</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}

View File

@ -0,0 +1,95 @@
import Link from "next/link";
import { Configuration, PublicApi } from "@ory/kratos-client";
import config from "../config";
import SelfServiceForm from "../components/Form/SelfServiceForm";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export async function getServerSideProps(context) {
const flow = context.query.flow;
if (process.env.NODE_ENV === "development") {
return { props: { flow: require("../../stubs/recovery.json") } };
}
// The flow is used to identify the login and registration flow and
// return data like the csrf_token and so on.
if (!flow || typeof flow !== "string") {
// No flow ID found in URL, initializing recovery flow.
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/recovery/browser`,
},
};
}
try {
const { status, data } = await kratos.getSelfServiceRecoveryFlow(flow);
if (status === 200) return { props: { flow: data } };
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/recovery/browser`,
},
};
}
}
const fieldsConfig = {
email: {
label: "Your email",
autoComplete: "email",
position: 0,
},
csrf_token: {
position: 99,
},
};
export default function Recovery({ flow }) {
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Recover your account</h2>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
<Link href="/auth/login">
<a className="font-medium text-green-600 hover:text-green-500">sign in</a>
</Link>{" "}
if you suddenly remembered your password
</p>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
or{" "}
<Link href="/auth/registration">
<a className="font-medium text-green-600 hover:text-green-500">sign up</a>
</Link>{" "}
for a new account
</p>
</div>
<SelfServiceForm
flow={flow}
config={flow.methods.link.config}
fieldsConfig={fieldsConfig}
button="Send recovery link"
/>
</div>
);
}

View File

@ -0,0 +1,79 @@
import { Configuration, PublicApi } from "@ory/kratos-client";
import Layout from "../components/Layout";
import config from "../config";
import SelfServiceForm from "../components/Form/SelfServiceForm";
import authServerSideProps from "../services/authServerSideProps";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export const getServerSideProps = authServerSideProps(async (context) => {
const flow = context.query.flow;
if (process.env.NODE_ENV === "development") {
return { props: { flow: require("../../stubs/settings.json") } };
}
// The flow is used to identify the login and registration flow and
// return data like the csrf_token and so on.
if (!flow || typeof flow !== "string") {
// No flow ID found in URL, initializing settings flow.
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/settings/browser`,
},
};
}
try {
const { status, data } = await kratos.getSelfServiceSettingsFlow(flow, {
headers: { cookie: context.req.headers.cookie },
});
if (status === 200) return { props: { flow: data } };
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/settings/browser`,
},
};
}
});
const fieldsConfig = {
"traits.email": {
label: "Email address",
autoComplete: "email",
position: 0,
},
password: {
label: "Password",
autoComplete: "new-password",
position: 1,
},
csrf_token: {
position: 99,
},
};
export default function Settings({ flow }) {
const profileConfig = flow.methods.profile.config;
const passwordConfig = flow.methods.password.config;
return (
<Layout title="Settings">
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6 grid grid-cols-1 gap-5 sm:grid-cols-2">
<SelfServiceForm config={profileConfig} fieldsConfig={fieldsConfig} title="Account settings" button="Update" />
<SelfServiceForm
config={passwordConfig}
fieldsConfig={fieldsConfig}
title="Authentication settings"
button="Update"
/>
</div>
</Layout>
);
}

View File

@ -0,0 +1,44 @@
import dayjs from "dayjs";
import prettyBytes from "pretty-bytes";
import { useState } from "react";
import Layout from "../components/Layout";
import Table from "../components/Table";
import authServerSideProps from "../services/authServerSideProps";
import { SkynetClient } from "skynet-js";
import useAccountsApi from "../services/useAccountsApi";
const skynetClient = new SkynetClient(process.env.NEXT_PUBLIC_SKYNET_PORTAL_API);
const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss");
const headers = [
{ key: "name", name: "Name", nowrap: false, href: getSkylinkLink },
{ key: "skylink", name: "Skylink" },
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
{ key: "uploadedOn", name: "Uploaded on", formatter: getRelativeDate },
];
const actions = [];
export const getServerSideProps = authServerSideProps(async (context, api) => {
const initialData = await api.get("user/uploads?pageSize=10&offset=0").json();
return { props: { initialData } };
});
export default function Uploads({ initialData }) {
const [offset, setOffset] = useState(0);
const { data } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${offset}`, {
initialData: offset === 0 ? initialData : undefined,
revalidateOnMount: true,
});
// preload next page if it exists (based on the response from the current page query)
const nextPageOffset = data && data.offset + data.pageSize < data.count ? data.offset + data.pageSize : offset;
useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${nextPageOffset}`);
return (
<Layout title="Your uploads">
<Table {...data} headers={headers} actions={actions} setOffset={setOffset} />
</Layout>
);
}

View File

@ -0,0 +1,103 @@
import Link from "next/link";
import { Configuration, PublicApi } from "@ory/kratos-client";
import config from "../config";
import SelfServiceForm from "../components/Form/SelfServiceForm";
import { useEffect } from "react";
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
export async function getServerSideProps(context) {
const flow = context.query.flow;
// if (process.env.NODE_ENV === "development") {
// return { props: { flow: require("../../stubs/recovery.json") } };
// }
if (!flow || typeof flow !== "string") {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/verification/browser`,
},
};
}
try {
const { status, data } = await kratos.getSelfServiceVerificationFlow(flow);
if (status === 200) return { props: { flow: data } };
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) {
return {
redirect: {
permanent: false,
destination: `${config.kratos.browser}/self-service/verification/browser`,
},
};
}
}
const fieldsConfig = {
email: {
label: "Your email",
autoComplete: "email",
position: 0,
},
csrf_token: {
position: 99,
},
};
export default function Verify({ flow }) {
const state = flow.state;
useEffect(() => {
if (state === "passed_challenge") {
setTimeout(() => (window.location = "/"), 5000);
}
}, [state]);
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
width="169"
height="39"
viewBox="0 0 169 39"
className="mx-auto h-12 w-auto"
>
<path
d="M160.701 31.245c-2.736 0-4.788-.706-6.156-2.118-1.369-1.411-2.053-3.424-2.053-6.039V2.674h7.487v6.33H169v6.15h-9.02v7.355c0 1.753.886 2.63 2.66 2.63h6.134v6.106h-8.073zm-24.942 0c-1.744 0-3.352-.268-4.826-.803-1.473-.535-2.751-1.292-3.833-2.273a10.275 10.275 0 01-2.526-3.521c-.602-1.367-.902-2.882-.902-4.546 0-1.635.3-3.135.902-4.502a10.275 10.275 0 012.526-3.521c1.082-.98 2.36-1.738 3.833-2.273 1.474-.535 3.082-.803 4.826-.803h5.728c1.293 0 2.42.179 3.383.535.962.357 1.767.847 2.413 1.471a5.823 5.823 0 011.443 2.251c.316.877.474 1.835.474 2.875 0 1.961-.571 3.432-1.714 4.412-1.143.981-3.022 1.471-5.638 1.471h-8.028v-4.056h6.45c1.172 0 1.759-.52 1.759-1.56 0-1.1-.572-1.649-1.714-1.649h-4.6c-1.354 0-2.451.46-3.293 1.382-.842.921-1.263 2.228-1.263 3.922s.42 2.964 1.263 3.811c.842.847 1.94 1.27 3.292 1.27h12.268v6.107H135.76zm-22.867 0V19.567c0-2.526-1.308-3.789-3.924-3.789h-7.532v15.467H93.86V9.003h15.29c3.728 0 6.54.922 8.434 2.764 1.894 1.842 2.841 4.457 2.841 7.844v11.634h-7.532zM67.293 39v-6.418h2.39c.963 0 1.715-.193 2.256-.58a4.58 4.58 0 001.308-1.426L62.422 9.003h7.623l6.675 14.263 6.855-14.263h7.668L78.749 33.43c-.48.95-.984 1.775-1.51 2.473a7.85 7.85 0 01-1.782 1.739 7.11 7.11 0 01-2.233 1.025c-.827.223-1.781.334-2.864.334h-3.067zm-14.207-7.755l-7.713-9.316a4.647 4.647 0 01-.54-.914 2.306 2.306 0 01-.181-.913v-.535c0-.654.225-1.278.676-1.872l7.487-8.692h8.48l-9.02 10.787 9.47 11.455h-8.66zm-17.41 0V0h7.532v31.245h-7.532zm-35.315 0v-7.488h21.92c.571 0 1.037-.171 1.398-.513.36-.342.541-.825.541-1.449 0-.624-.18-1.114-.541-1.47-.36-.357-.827-.535-1.398-.535H9.38c-1.353 0-2.608-.216-3.766-.647-1.157-.43-2.15-1.025-2.976-1.782a8.005 8.005 0 01-1.94-2.742C.233 13.55 0 12.361 0 11.054 0 9.746.233 8.55.7 7.466A8.184 8.184 0 012.638 4.68c.826-.773 1.819-1.367 2.976-1.783 1.158-.416 2.413-.624 3.766-.624h22.281v7.444H9.742c-.571 0-1.03.163-1.375.49-.346.327-.52.802-.52 1.426 0 .594.174 1.07.52 1.426.345.357.804.535 1.375.535h12.9c1.383 0 2.646.216 3.788.647a9.097 9.097 0 012.977 1.805 8.038 8.038 0 011.962 2.785c.466 1.085.7 2.266.7 3.544 0 1.337-.234 2.548-.7 3.632a8.267 8.267 0 01-1.962 2.808 8.62 8.62 0 01-2.977 1.806c-1.142.416-2.405.624-3.788.624H.36z"
fill="#57B560"
fillRule="evenodd"
/>
</svg>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
{flow.state === "passed_challenge" ? "Verification successful!" : "Account verification"}
</h2>
{flow.state === "passed_challenge" && (
<>
<p className="mt-2 text-center text-sm text-gray-600 max-w">You will be redirected automatically</p>
<p className="mt-2 text-center text-sm text-gray-600 max-w">
<Link href="/">
<a className="font-medium text-green-600 hover:text-green-500">go to dashboard</a>
</Link>
</p>
</>
)}
</div>
{flow.state !== "passed_challenge" && (
<SelfServiceForm
flow={flow}
config={flow.methods.link.config}
fieldsConfig={fieldsConfig}
button="Resend verification link"
/>
)}
</div>
);
}

View File

@ -0,0 +1,27 @@
import ky from "ky/umd";
const isProduction = process.env.NODE_ENV === "production";
export default function authServerSideProps(getServerSideProps) {
return function authenticate(context) {
if (isProduction && (!("ory_kratos_session" in context.req.cookies) || !("skynet-jwt" in context.req.cookies))) {
return {
redirect: {
permanent: false,
destination: `/api/accounts/login?return_to=${encodeURIComponent(context.resolvedUrl)}`,
},
};
}
if (getServerSideProps) {
const api = ky.create({
headers: { cookie: context.req.headers.cookie },
prefixUrl: isProduction ? "http://oathkeeper:4455" : "http://localhost:3000/api/stubs",
});
return getServerSideProps(context, api);
}
return { props: {} };
};
}

View File

@ -0,0 +1,65 @@
// @source https://github.com/trekhleb/javascript-algorithms/blob/master/src/algorithms/sets/longest-common-subsequence/longestCommonSubsequence.js
// @license MIT https://github.com/trekhleb/javascript-algorithms/blob/master/LICENSE
/**
* @param {string[]} set1
* @param {string[]} set2
* @return {string[]}
*/
export default function longestCommonSubsequence(set1, set2) {
// Init LCS matrix.
const lcsMatrix = Array(set2.length + 1)
.fill(null)
.map(() => Array(set1.length + 1).fill(null));
// Fill first row with zeros.
for (let columnIndex = 0; columnIndex <= set1.length; columnIndex += 1) {
lcsMatrix[0][columnIndex] = 0;
}
// Fill first column with zeros.
for (let rowIndex = 0; rowIndex <= set2.length; rowIndex += 1) {
lcsMatrix[rowIndex][0] = 0;
}
// Fill rest of the column that correspond to each of two strings.
for (let rowIndex = 1; rowIndex <= set2.length; rowIndex += 1) {
for (let columnIndex = 1; columnIndex <= set1.length; columnIndex += 1) {
if (set1[columnIndex - 1] === set2[rowIndex - 1]) {
lcsMatrix[rowIndex][columnIndex] = lcsMatrix[rowIndex - 1][columnIndex - 1] + 1;
} else {
lcsMatrix[rowIndex][columnIndex] = Math.max(
lcsMatrix[rowIndex - 1][columnIndex],
lcsMatrix[rowIndex][columnIndex - 1]
);
}
}
}
// Calculate LCS based on LCS matrix.
if (!lcsMatrix[set2.length][set1.length]) {
// If the length of largest common string is zero then return empty string.
return [""];
}
const longestSequence = [];
let columnIndex = set1.length;
let rowIndex = set2.length;
while (columnIndex > 0 || rowIndex > 0) {
if (set1[columnIndex - 1] === set2[rowIndex - 1]) {
// Move by diagonal left-top.
longestSequence.unshift(set1[columnIndex - 1]);
columnIndex -= 1;
rowIndex -= 1;
} else if (lcsMatrix[rowIndex][columnIndex] === lcsMatrix[rowIndex][columnIndex - 1]) {
// Move left.
columnIndex -= 1;
} else {
// Move up.
rowIndex -= 1;
}
}
return longestSequence;
}

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