Compare commits
1 Commits
master
...
fil/sia-us
Author | SHA1 | Date |
---|---|---|
Filip Rysavy | a337672e98 |
|
@ -1 +0,0 @@
|
|||
* @kwypchlo @meeh0w
|
|
@ -1,6 +1,26 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker/sia"
|
||||
- package-ecosystem: npm
|
||||
directory: "/packages/handshake-api"
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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 || '' }}
|
|
@ -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
|
|
@ -53,6 +53,7 @@ typings/
|
|||
|
||||
# dotenv environment variable files
|
||||
.env*
|
||||
./docker/kratos/config/kratos.yml
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
@ -67,6 +68,10 @@ yarn-error.log
|
|||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Cypress
|
||||
packages/webapp/cypress/screenshots
|
||||
packages/webapp/cypress/videos
|
||||
|
||||
# Docker data
|
||||
docker/data
|
||||
|
||||
|
@ -82,10 +87,12 @@ __pycache__
|
|||
/.idea/
|
||||
/venv*
|
||||
|
||||
# Luacov file
|
||||
luacov.stats.out
|
||||
luacov.report.out
|
||||
# CockroachDB certificates
|
||||
docker/cockroach/certs/*.crt
|
||||
docker/cockroach/certs/*.key
|
||||
docker/kratos/cr_certs/*.crt
|
||||
docker/kratos/cr_certs/*.key
|
||||
|
||||
# Setup-script log files
|
||||
setup-scripts/serverload.log
|
||||
setup-scripts/serverload.json
|
||||
# Oathkeeper JWKS signing token
|
||||
docker/kratos/oathkeeper/id_token.jwks.json
|
||||
/docker/kratos/config/kratos.yml
|
||||
|
|
81
CHANGELOG.md
81
CHANGELOG.md
|
@ -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
189
README.md
|
@ -1,18 +1,191 @@
|
|||
# Skynet Portal
|
||||
|
||||
## Latest Setup Documentation
|
||||
## Web application
|
||||
|
||||
Latest Skynet Webportal setup documentation and the setup process Skynet Labs
|
||||
supports is located at https://portal-docs.skynetlabs.com/.
|
||||
Use `yarn workspace webapp start` to start the development server.
|
||||
|
||||
Some scripts and setup documentation contained in this repository
|
||||
(`skynet-webportal`) may be outdated and generally should not be used.
|
||||
Use `yarn workspace webapp build` to compile the application to `/public` directory.
|
||||
|
||||
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
|
||||
|
||||
Skynet uses a custom [License](./LICENSE.md). The Skynet License is a source code license that allows you to use, modify
|
||||
and distribute the software, but you must preserve the payment mechanism in the software.
|
||||
Skynet uses a custom [License](./LICENSE.md). The Skynet License is a source
|
||||
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`
|
||||
|
||||
### 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).
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
.init
|
||||
.DS_Store
|
|
@ -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.
|
|
@ -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:
|
|
@ -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
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
- Fix `dashboard-v2` Dockerfile context in `docker-compose.accounts.yml` to
|
||||
avoid Ansible deploy (docker compose build) `permission denied` issues.
|
|
@ -1 +0,0 @@
|
|||
- Fix missing `logs` dir that is required for backup scripts (cron jobs).
|
|
@ -1 +0,0 @@
|
|||
- Add Pinner service to the portal stack. Activate it by selecting the 'p' module.
|
63
dc
63
dc
|
@ -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 $@
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
version: "3.8"
|
||||
version: "3.7"
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
|
@ -9,43 +9,40 @@ x-logging: &default-logging
|
|||
services:
|
||||
nginx:
|
||||
environment:
|
||||
- ACCOUNTS_ENABLED=true
|
||||
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only
|
||||
- ACCOUNTS_ENABLED=1
|
||||
volumes:
|
||||
- ./docker/accounts/nginx.account.conf:/etc/nginx/conf.extra.d/nginx.account.conf:ro
|
||||
depends_on:
|
||||
- accounts
|
||||
|
||||
health-check:
|
||||
environment:
|
||||
- ACCOUNTS_ENABLED=true
|
||||
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only
|
||||
- ACCOUNTS_ENABLED=1
|
||||
|
||||
accounts:
|
||||
# uncomment "build" and comment out "image" to build from sources
|
||||
# build: https://github.com/SkynetLabs/skynet-accounts.git#main
|
||||
image: skynetlabs/skynet-accounts:1.3.0
|
||||
build:
|
||||
context: ./docker/accounts
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
branch: main
|
||||
container_name: accounts
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- ACCOUNTS_EMAIL_URI=${ACCOUNTS_EMAIL_URI}
|
||||
- ACCOUNTS_JWKS_FILE=/conf/jwks.json
|
||||
- SKYNET_DB_HOST=${SKYNET_DB_HOST}
|
||||
- SKYNET_DB_PORT=${SKYNET_DB_PORT}
|
||||
- SKYNET_DB_USER=${SKYNET_DB_USER}
|
||||
- SKYNET_DB_PASS=${SKYNET_DB_PASS}
|
||||
- COOKIE_DOMAIN=${COOKIE_DOMAIN}
|
||||
- COOKIE_HASH_KEY=${COOKIE_HASH_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_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
||||
- SKYNET_ACCOUNTS_LOG_LEVEL=${SKYNET_ACCOUNTS_LOG_LEVEL:-info}
|
||||
volumes:
|
||||
- ./docker/data/accounts:/data
|
||||
- ./docker/accounts/conf:/conf
|
||||
- SKYNET_ACCOUNTS_LOG_LEVEL=${SKYNET_ACCOUNTS_LOG_LEVEL}
|
||||
- KRATOS_ADDR=${KRATOS_ADDR}
|
||||
- OATHKEEPER_ADDR=${OATHKEEPER_ADDR}
|
||||
expose:
|
||||
- 3000
|
||||
networks:
|
||||
|
@ -53,25 +50,119 @@ services:
|
|||
ipv4_address: 10.10.10.70
|
||||
depends_on:
|
||||
- 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:
|
||||
# uncomment "build" and comment out "image" to build from sources
|
||||
# build:
|
||||
# context: https://github.com/SkynetLabs/webportal-accounts-dashboard.git#main
|
||||
# dockerfile: Dockerfile
|
||||
image: skynetlabs/webportal-accounts-dashboard:2.1.1
|
||||
build:
|
||||
context: ./packages/dashboard
|
||||
dockerfile: Dockerfile
|
||||
container_name: dashboard
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./docker/data/dashboard/.cache:/usr/app/.cache
|
||||
- ./docker/data/dashboard/public:/usr/app/public
|
||||
environment:
|
||||
- NEXT_PUBLIC_SKYNET_PORTAL_API=${SKYNET_PORTAL_API}
|
||||
- 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:
|
||||
shared:
|
||||
ipv4_address: 10.10.10.85
|
||||
expose:
|
||||
- 9000
|
||||
- 3000
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -1,16 +1,10 @@
|
|||
version: "3.8"
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
sia:
|
||||
environment:
|
||||
- JAEGER_DISABLED=${JAEGER_DISABLED:-false} # Enable/Disable tracing
|
||||
- JAEGER_SERVICE_NAME=${SERVER_DOMAIN:-Skyd} # change to e.g. eu-ger-1
|
||||
- JAEGER_DISABLED=${JAEGER_DISABLED:-true} # Enable/Disable tracing
|
||||
- JAEGER_SERVICE_NAME=${PORTAL_NAME:-Skyd} # change to e.g. eu-ger-1
|
||||
# Configuration
|
||||
# See https://github.com/jaegertracing/jaeger-client-go#environment-variables
|
||||
# for all options.
|
||||
|
@ -21,15 +15,10 @@ services:
|
|||
- JAEGER_REPORTER_LOG_SPANS=false
|
||||
|
||||
jaeger-agent:
|
||||
image: jaegertracing/jaeger-agent:1.38.1
|
||||
command:
|
||||
[
|
||||
"--reporter.grpc.host-port=jaeger-collector:14250",
|
||||
"--reporter.grpc.retry.max=1000",
|
||||
]
|
||||
image: jaegertracing/jaeger-agent
|
||||
command: [ "--reporter.grpc.host-port=jaeger-collector:14250", "--reporter.grpc.retry.max=1000" ]
|
||||
container_name: jaeger-agent
|
||||
restart: on-failure
|
||||
logging: *default-logging
|
||||
expose:
|
||||
- 6831
|
||||
- 6832
|
||||
|
@ -43,11 +32,10 @@ services:
|
|||
- jaeger-collector
|
||||
|
||||
jaeger-collector:
|
||||
image: jaegertracing/jaeger-collector:1.38.1
|
||||
image: jaegertracing/jaeger-collector
|
||||
entrypoint: /wait_to_start.sh
|
||||
container_name: jaeger-collector
|
||||
restart: on-failure
|
||||
logging: *default-logging
|
||||
expose:
|
||||
- 14269
|
||||
- 14268
|
||||
|
@ -68,11 +56,10 @@ services:
|
|||
- elasticsearch
|
||||
|
||||
jaeger-query:
|
||||
image: jaegertracing/jaeger-query:1.38.1
|
||||
image: jaegertracing/jaeger-query
|
||||
entrypoint: /wait_to_start.sh
|
||||
container_name: jaeger-query
|
||||
restart: on-failure
|
||||
logging: *default-logging
|
||||
ports:
|
||||
- "127.0.0.1:16686:16686"
|
||||
expose:
|
||||
|
@ -93,13 +80,11 @@ services:
|
|||
- elasticsearch
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.6
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.15
|
||||
container_name: elasticsearch
|
||||
restart: on-failure
|
||||
logging: *default-logging
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||
volumes:
|
||||
# This dir needs to be chowned to 1000:1000
|
||||
- ./docker/data/elasticsearch/data:/usr/share/elasticsearch/data
|
||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
version: "3.8"
|
||||
version: "3.7"
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
|
@ -15,23 +15,16 @@ networks:
|
|||
|
||||
services:
|
||||
sia:
|
||||
# uncomment "build" and comment out "image" to build from sources
|
||||
# build:
|
||||
# context: https://github.com/SkynetLabs/docker-skyd.git#main
|
||||
# dockerfile: scratch/Dockerfile
|
||||
# args:
|
||||
# branch: master
|
||||
image: skynetlabs/skyd:1.6.9
|
||||
command: --disable-api-security --api-addr :9980 --modules gctwra
|
||||
build:
|
||||
context: ./docker/sia
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
branch: v1.5.5
|
||||
container_name: sia
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 5m
|
||||
logging: *default-logging
|
||||
environment:
|
||||
- SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-true}
|
||||
- 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
|
||||
- SIA_MODULES=gctwr
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
|
@ -42,79 +35,61 @@ services:
|
|||
expose:
|
||||
- 9980
|
||||
|
||||
certbot:
|
||||
# replace this image with the image supporting your dns provider from
|
||||
# https://hub.docker.com/r/certbot/certbot and adjust CERTBOT_ARGS env variable
|
||||
# note: you will need to authenticate your dns request so consult the plugin docs
|
||||
# configuration https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins
|
||||
#
|
||||
# =================================================================================
|
||||
# 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
|
||||
caddy:
|
||||
build:
|
||||
context: ./docker/caddy
|
||||
dockerfile: Dockerfile
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- CERTBOT_ARGS=--dns-route53
|
||||
volumes:
|
||||
- ./docker/certbot/entrypoint.sh:/entrypoint.sh
|
||||
- ./docker/data/certbot:/etc/letsencrypt
|
||||
- ./docker/data/caddy/data:/data
|
||||
- ./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:
|
||||
# uncomment "build" and comment out "image" to build from sources
|
||||
# build:
|
||||
# context: https://github.com/SkynetLabs/webportal-nginx.git#main
|
||||
# dockerfile: Dockerfile
|
||||
image: skynetlabs/webportal-nginx:1.0.0
|
||||
build:
|
||||
context: ./docker/nginx
|
||||
dockerfile: Dockerfile
|
||||
container_name: nginx
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
env_file:
|
||||
- .env
|
||||
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/blocker:/data/nginx/blocker
|
||||
- ./docker/data/nginx/logs:/usr/local/openresty/nginx/logs
|
||||
- ./docker/data/nginx/skynet:/data/nginx/skynet:ro
|
||||
- ./docker/data/sia/apipassword:/data/sia/apipassword:ro
|
||||
- ./docker/data/certbot:/etc/letsencrypt
|
||||
networks:
|
||||
shared:
|
||||
ipv4_address: 10.10.10.30
|
||||
ports:
|
||||
- "443:443"
|
||||
- "80:80"
|
||||
expose:
|
||||
- 80
|
||||
depends_on:
|
||||
- sia
|
||||
- handshake-api
|
||||
- dnslink-api
|
||||
- website
|
||||
|
||||
website:
|
||||
# uncomment "build" and comment out "image" to build from sources
|
||||
# build:
|
||||
# context: https://github.com/SkynetLabs/webportal-website.git#main
|
||||
# dockerfile: Dockerfile
|
||||
image: skynetlabs/webportal-website:0.2.3
|
||||
build:
|
||||
context: ./packages/website
|
||||
dockerfile: Dockerfile
|
||||
container_name: website
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
volumes:
|
||||
- ./docker/data/website/.cache:/usr/app/.cache
|
||||
- ./docker/data/website/.public:/usr/app/public
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
|
@ -124,11 +99,19 @@ services:
|
|||
- 9000
|
||||
|
||||
handshake:
|
||||
image: handshakeorg/hsd:4.0.2
|
||||
command: --chain-migrate=3 --no-wallet --no-auth --compact-tree-on-init --network=main --http-host=0.0.0.0
|
||||
build:
|
||||
context: ./docker/handshake
|
||||
dockerfile: Dockerfile
|
||||
container_name: handshake
|
||||
restart: unless-stopped
|
||||
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:
|
||||
- ./docker/data/handshake/.hsd:/root/.hsd
|
||||
networks:
|
||||
|
@ -138,11 +121,9 @@ services:
|
|||
- 12037
|
||||
|
||||
handshake-api:
|
||||
# uncomment "build" and comment out "image" to build from sources
|
||||
# build:
|
||||
# context: https://github.com/SkynetLabs/webportal-handshake-api.git#main
|
||||
# dockerfile: Dockerfile
|
||||
image: skynetlabs/webportal-handshake-api:0.1.3
|
||||
build:
|
||||
context: ./packages/handshake-api
|
||||
dockerfile: Dockerfile
|
||||
container_name: handshake-api
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
|
@ -161,27 +142,10 @@ services:
|
|||
depends_on:
|
||||
- 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:
|
||||
# uncomment "build" and comment out "image" to build from sources
|
||||
# build:
|
||||
# context: https://github.com/SkynetLabs/webportal-health-check.git#main
|
||||
# dockerfile: Dockerfile
|
||||
image: skynetlabs/webportal-health-check:1.0.0
|
||||
build:
|
||||
context: ./packages/health-check
|
||||
dockerfile: Dockerfile
|
||||
container_name: health-check
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
|
@ -197,3 +161,5 @@ services:
|
|||
- STATE_DIR=/usr/app/state
|
||||
expose:
|
||||
- 3100
|
||||
depends_on:
|
||||
- caddy
|
||||
|
|
|
@ -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"]
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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".
|
|
@ -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"]
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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).
|
|
@ -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
|
|
@ -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 }}
|
||||
}
|
|
@ -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;"]
|
|
@ -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
|
|
@ -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/*;
|
||||
}
|
|
@ -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;
|
|
@ -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';
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
# }
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
#}
|
||||
}
|
|
@ -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
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
.next
|
||||
package.json
|
||||
package-lock.json
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"printWidth": 120
|
||||
}
|
|
@ -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"]
|
|
@ -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.
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
future: {
|
||||
webpack5: true,
|
||||
},
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
));
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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]
|
||||
)) || <>—</>}
|
||||
</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>
|
||||
);
|
||||
}
|
|
@ -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" },
|
||||
},
|
||||
};
|
|
@ -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;
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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 } });
|
||||
}
|
||||
};
|
|
@ -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 } });
|
||||
}
|
||||
};
|
|
@ -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 } });
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import user from "./user.json";
|
||||
|
||||
export default (req, res) => {
|
||||
res.json(user);
|
||||
};
|
|
@ -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"
|
||||
}
|
|
@ -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 });
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,5 @@
|
|||
import stats from "./stats.json";
|
||||
|
||||
export default (req, res) => {
|
||||
res.json(stats);
|
||||
};
|
|
@ -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
|
||||
}
|
|
@ -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 });
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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: {} };
|
||||
};
|
||||
}
|
|
@ -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
Reference in New Issue