Accounts (#554)
* stripe env
* stripe env
* stripe env
* allow post
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* accounts/**
* favicon
* foo
* foo
* foo
* foo
* foo
* foo
* title
* fix dashboard timestamp
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* prices
* Revert "prices"
This reverts commit 7071ed4ef4
.
* Make sure we don't accidentally commit `kratos.yml`.
* Add Oathkeeper access rules for Stripe.
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* Add `max_breaches` to Kratos's sample config file.
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* payments
* cache .next folder
* Use own fork of Kratos's `master` in order to get the fix for the migrations issue.
* Don't retry running Kratos migrations.
* payments
* restart: no
* no
* no
* no
* no
* no
* no
* no
* no
* no
* payments
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* accounts
* limits
* limits
* nginx depends on accounts and kratos-migrate depends on cockroach.
* upload limit rate
* upload limit rate - 2
* upload limit rate - 3
* upload limit rate - 4
* upload limit rate - 5
* upload limit rate - 6
* upload limit rate - 7
* upload limit rate - 8
* upload limit rate - 9
* forgotten password link
* use header for skylink
* use header for skylink
* use header for skylink
* use header for skylink
* use header for skylink
* use header for skylink
* use header for skylink
* use header for skylink
* copy to clipboard
* fix ratelimit issue
* Allow access to the stripe webhook.
* enable allow_promotion_codes
* Allow POST on webhook.
* Add all env vars accounts need to docker-compose.
* Don't use custom port for accounts.
* print recovery
* recovery sign up link
* refactor cors header response
* refactor cors header response
* do not log unauthorized
* fix registration link
* settings logging
* update node and tailwindcss
* move webapp from volume
* host 0.0.0.0
* refactor dockerfile
* enable accounts
* cache public
* uncache public
* remove cache control
* no-cache
* no cache
* Do not use the person's name for registration.
* add verify route
* add verify route
* add verify route
* Go back to using the stock kratos image.
* add verify route
* fix settings link
* clean up verify flow
* refactor Dockerfile
* Remove first and last name from used traits.
* Remove account verification via email.
* Allow additional properties.
* Cookies and tokens last for 30 days now.
* Rename secure.siasky.net to account.siasky.net.
* redirect secure to account
Co-authored-by: Ivaylo Novakov <inovakov@gmail.com>
Co-authored-by: Ivaylo Novakov <ro-tex@users.noreply.github.com>
This commit is contained in:
parent
217dcbe36a
commit
fede204c6b
|
@ -53,6 +53,7 @@ typings/
|
|||
|
||||
# dotenv environment variable files
|
||||
.env*
|
||||
./docker/kratos/config/kratos.yml
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
@ -82,3 +83,13 @@ docker/nginx/conf.d/server-override/*
|
|||
__pycache__
|
||||
/.idea/
|
||||
/venv*
|
||||
|
||||
# CockroachDB certificates
|
||||
docker/cockroach/certs/*.crt
|
||||
docker/cockroach/certs/*.key
|
||||
docker/kratos/cr_certs/*.crt
|
||||
docker/kratos/cr_certs/*.key
|
||||
|
||||
# Oathkeeper JWKS signing token
|
||||
docker/kratos/oathkeeper/id_token.jwks.json
|
||||
/docker/kratos/config/kratos.yml
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
/package.json
|
||||
/package-lock.json
|
152
README.md
152
README.md
|
@ -26,12 +26,160 @@ 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 run an initialisation `docker run` with extra environment variables that will initialise the admin user with
|
||||
a password (example below).
|
||||
- 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).
|
||||
- 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_NODE - the name of your node
|
||||
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.
|
||||
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:
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
version: "3.7"
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
services:
|
||||
webapp:
|
||||
build:
|
||||
args:
|
||||
WITH_ACCOUNTS: 1 # enable accounts frontend
|
||||
|
||||
nginx:
|
||||
environment:
|
||||
- ACCOUNTS_ENABLED=1
|
||||
volumes:
|
||||
- ./docker/accounts/nginx.account.conf:/etc/nginx/conf.extra.d/nginx.account.conf:ro
|
||||
depends_on:
|
||||
- accounts
|
||||
|
||||
accounts:
|
||||
build:
|
||||
context: ./docker/accounts
|
||||
dockerfile: Dockerfile
|
||||
container_name: accounts
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- 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}
|
||||
- STRIPE_API_KEY=${STRIPE_API_KEY}
|
||||
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
||||
- SKYNET_ACCOUNTS_LOG_LEVEL=${SKYNET_ACCOUNTS_LOG_LEVEL}
|
||||
- KRATOS_ADDR=${KRATOS_ADDR}
|
||||
- OATHKEEPER_ADDR=${OATHKEEPER_ADDR}
|
||||
expose:
|
||||
- 3000
|
||||
networks:
|
||||
shared:
|
||||
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:
|
||||
build:
|
||||
context: ./packages/dashboard
|
||||
dockerfile: Dockerfile
|
||||
container_name: dashboard
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
env_file:
|
||||
- .env
|
||||
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=${SKYNET_DASHBOARD_URL}/.ory/kratos/public
|
||||
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
|
||||
volumes:
|
||||
- ./docker/data/dashboard/.next:/usr/app/.next
|
||||
networks:
|
||||
shared:
|
||||
ipv4_address: 10.10.10.85
|
||||
expose:
|
||||
- 3000
|
||||
|
||||
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
|
|
@ -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
|
|
@ -13,9 +13,6 @@ networks:
|
|||
config:
|
||||
- subnet: 10.10.10.0/24
|
||||
|
||||
volumes:
|
||||
webapp:
|
||||
|
||||
services:
|
||||
sia:
|
||||
build:
|
||||
|
@ -76,7 +73,6 @@ services:
|
|||
- ./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
|
||||
- webapp:/var/www/webportal:ro
|
||||
networks:
|
||||
shared:
|
||||
ipv4_address: 10.10.10.30
|
||||
|
@ -94,9 +90,13 @@ services:
|
|||
container_name: webapp
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
tty: true
|
||||
volumes:
|
||||
- webapp:/usr/app/public
|
||||
- ./docker/data/webapp/.cache:/usr/app/.cache
|
||||
networks:
|
||||
shared:
|
||||
ipv4_address: 10.10.10.35
|
||||
expose:
|
||||
- 9000
|
||||
|
||||
handshake:
|
||||
build:
|
||||
|
@ -163,18 +163,3 @@ services:
|
|||
depends_on:
|
||||
- handshake
|
||||
- handshake-api
|
||||
|
||||
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.70
|
||||
ports:
|
||||
- "27017:27017"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
FROM golang:1.15
|
||||
LABEL maintainer="NebulousLabs <devs@nebulous.tech>"
|
||||
|
||||
ENV GOOS linux
|
||||
ENV GOARCH amd64
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
RUN git clone --single-branch --branch main 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,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".
|
|
@ -1,8 +1,8 @@
|
|||
FROM node:15.8.0-alpine
|
||||
FROM node:15.12.0-alpine
|
||||
|
||||
WORKDIR /opt/hsd
|
||||
|
||||
RUN apk add --no-cache bash unbound-dev gmp-dev g++ gcc make python2 git
|
||||
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
|
||||
|
||||
|
|
|
@ -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}>/<{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 }}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
FROM openresty/openresty:1.19.3.1-2-bionic
|
||||
|
||||
# RUN apt-get update -qq && apt-get install cron logrotate -qq
|
||||
# RUN luarocks install luasocket
|
||||
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
|
|
@ -24,8 +24,8 @@ 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
|
||||
# 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;
|
||||
|
@ -57,6 +57,7 @@ server {
|
|||
rewrite ^/portals /skynet/portals permanent;
|
||||
rewrite ^/stats /skynet/stats permanent;
|
||||
rewrite ^/skynet/blacklist /skynet/blocklist permanent;
|
||||
rewrite ^/account/(.*) https://account.$domain.$tld/$1 permanent;
|
||||
|
||||
location / {
|
||||
# This is only safe workaround to reroute based on some conditions
|
||||
|
@ -77,7 +78,7 @@ server {
|
|||
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
root /var/www/webportal;
|
||||
proxy_pass http://webapp:9000;
|
||||
}
|
||||
|
||||
location /docs {
|
||||
|
@ -113,11 +114,11 @@ server {
|
|||
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')
|
||||
|
@ -153,11 +154,11 @@ server {
|
|||
# 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
|
||||
|
@ -222,9 +223,6 @@ server {
|
|||
end
|
||||
}
|
||||
|
||||
# overwrite the Cache-Control header to only cache for 60s in case the domain gets updated
|
||||
more_set_headers 'Cache-Control: public, max-age=60';
|
||||
|
||||
# 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;
|
||||
|
||||
|
@ -255,7 +253,7 @@ server {
|
|||
# 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
|
||||
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
|
||||
|
@ -276,6 +274,40 @@ server {
|
|||
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 headers = { Cookie = ngx.req.get_headers()["Cookie"] }
|
||||
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 = 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 {
|
||||
|
@ -306,8 +338,37 @@ server {
|
|||
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 headers = { Cookie = ngx.req.get_headers()["Cookie"] }
|
||||
local ok, statusCode, headers, statusText = http.request { url = "http://accounts:3000/track/upload/" .. skylink, method = "POST", headers = 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
|
||||
}
|
||||
}
|
||||
|
||||
location ~ "^/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
|
||||
|
@ -322,12 +383,44 @@ server {
|
|||
}
|
||||
|
||||
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
||||
add_header Cache-Control "public, max-age=86400"; # allow consumer to cache response
|
||||
|
||||
# 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 headers = { Cookie = ngx.req.get_headers()["Cookie"] }
|
||||
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 = 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)
|
||||
|
@ -365,6 +458,14 @@ server {
|
|||
proxy_pass http://127.0.0.1/$uri?attachment=true&$args;
|
||||
}
|
||||
|
||||
location /accounts {
|
||||
internal; # internal endpoint only
|
||||
access_log off; # do not log traffic
|
||||
|
||||
rewrite /accounts(.*) $1 break; # drop the /accounts prefix from uri
|
||||
proxy_pass http://accounts:3000;
|
||||
}
|
||||
|
||||
# include custom locations, specific to the server
|
||||
include /etc/nginx/conf.d/server-override/*;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ worker_processes 1;
|
|||
#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;
|
||||
|
@ -67,4 +68,5 @@ http {
|
|||
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;
|
||||
#}
|
||||
}
|
|
@ -5,6 +5,10 @@
|
|||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"sharp": "^0.27.2"
|
||||
"@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,16 @@
|
|||
FROM node:15.12.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 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,43 @@
|
|||
{
|
||||
"name": "dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/metropolis": "^4.1.0",
|
||||
"@ory/kratos-client": "^0.5.4-alpha.1",
|
||||
"@stripe/react-stripe-js": "^1.4.0",
|
||||
"@stripe/stripe-js": "^1.13.0",
|
||||
"@tailwindcss/forms": "^0.2.1",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"classnames": "^2.2.6",
|
||||
"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",
|
||||
"jwks-rsa": "^1.12.2",
|
||||
"ky": "0.25.1",
|
||||
"next": "^10.0.8",
|
||||
"postcss": "^8.2.8",
|
||||
"prettier": "^2.2.1",
|
||||
"pretty-bytes": "^5.5.0",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"skynet-js": "^3.0.0",
|
||||
"square": "^9.0.0",
|
||||
"stripe": "^8.137.0",
|
||||
"superagent": "^6.1.0",
|
||||
"swr": "^0.5.0",
|
||||
"tailwindcss": "^2.0.3",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.2.1"
|
||||
}
|
||||
}
|
|
@ -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,104 @@
|
|||
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
|
||||
.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,58 @@
|
|||
import { Client, Environment } from "square";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
const client = new Client({
|
||||
environment: Environment.Sandbox,
|
||||
accessToken: process.env.SQUARE_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
const api = {
|
||||
GET: async (req, res) => {
|
||||
const user = "R7R0NY1Z8WT11D43564EEFKTYR"; // req.headers["x-user"];
|
||||
|
||||
try {
|
||||
const { result: customerResult } = await client.customersApi.retrieveCustomer(user);
|
||||
const { customer } = customerResult;
|
||||
|
||||
res.json(customer.cards);
|
||||
} catch (error) {
|
||||
res.json([]);
|
||||
}
|
||||
},
|
||||
// POST: async (req, res) => {
|
||||
// const user = req.headers["x-user"];
|
||||
// const card = {
|
||||
// cardNonce: "YOUR_CARD_NONCE",
|
||||
// cardholderName: "Amelia Earhart",
|
||||
// billingAddress: {},
|
||||
// verificationToken: "verification_token0",
|
||||
// };
|
||||
|
||||
// card.bodyBillingAddress.addressLine1 = "500 Electric Ave";
|
||||
// card.bodyBillingAddress.addressLine2 = "Suite 600";
|
||||
// card.bodyBillingAddress.addressLine3 = "address_line_38";
|
||||
// card.bodyBillingAddress.locality = "New York";
|
||||
// card.bodyBillingAddress.sublocality = "sublocality2";
|
||||
// card.bodyBillingAddress.administrativeDistrictLevel1 = "NY";
|
||||
// card.bodyBillingAddress.postalCode = "10003";
|
||||
// card.bodyBillingAddress.country = "US";
|
||||
|
||||
// try {
|
||||
// const { result } = await client.customersApi.createCustomerCard(user, card);
|
||||
|
||||
// res.status(StatusCodes.NO_CONTENT);
|
||||
// } catch (error) {
|
||||
// console.log(Object.keys(error));
|
||||
|
||||
// res.status(StatusCodes.BAD_REQUEST);
|
||||
// }
|
||||
// },
|
||||
};
|
||||
|
||||
export default (req, res) => {
|
||||
if (req.method in api) {
|
||||
api[req.method](req, res);
|
||||
} else {
|
||||
res.status(StatusCodes.NOT_FOUND);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
import { Client, Environment } from "square";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
const client = new Client({
|
||||
environment: Environment.Sandbox,
|
||||
accessToken: process.env.SQUARE_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
const api = {
|
||||
GET: async (req, res) => {
|
||||
const user = "NBE7TRXZPGZXNBD64JB6DR5AGR"; // req.headers["x-user"];
|
||||
|
||||
try {
|
||||
// get locations for invoices search query
|
||||
const { result: locationsResponse } = await client.locationsApi.listLocations();
|
||||
const { locations } = locationsResponse;
|
||||
|
||||
// create invoices serach query
|
||||
const locationIds = locations.map(({ id }) => id);
|
||||
const customerIds = [user];
|
||||
const filter = { locationIds, customerIds };
|
||||
const sort = { field: "INVOICE_SORT_DATE", order: "DESC" };
|
||||
const query = { filter, sort };
|
||||
|
||||
// query invoices with given search criteria
|
||||
const { result: invoicesResponse } = await client.invoicesApi.searchInvoices({ query, limit: 10 });
|
||||
const { invoices } = invoicesResponse;
|
||||
|
||||
res.json(invoices);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log(error?.errors);
|
||||
|
||||
res.json([]); // todo: error handling
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default (req, res) => {
|
||||
if (req.method in api) {
|
||||
return api[req.method](req, res);
|
||||
}
|
||||
|
||||
return res.status(StatusCodes.NOT_FOUND);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
import { Client, Environment } from "square";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
const client = new Client({
|
||||
environment: Environment.Sandbox,
|
||||
accessToken: process.env.SQUARE_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
const api = {
|
||||
GET: async (req, res) => {
|
||||
try {
|
||||
const user = "NBE7TRXZPGZXNBD64JB6DR5AGR"; // req.headers["x-user"];
|
||||
|
||||
// create subscriptions search query
|
||||
const query = { filter: { customerIds: [user] } };
|
||||
|
||||
// query subscriptions with given search criteria
|
||||
const { result: subscriptionsResponse } = await client.subscriptionsApi.searchSubscriptions({ query });
|
||||
const { subscriptions } = subscriptionsResponse;
|
||||
|
||||
// get active subscription
|
||||
const subscription = subscriptions.find(({ status }) => status === "ACTIVE");
|
||||
|
||||
if (!subscription) {
|
||||
return res.status(StatusCodes.NO_CONTENT).end(); // no active subscription found
|
||||
}
|
||||
|
||||
console.log("....", subscription);
|
||||
|
||||
return res.json(subscription);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log(error?.errors);
|
||||
|
||||
return res.status(StatusCodes.BAD_REQUEST).end(); // todo: error handling
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default (req, res) => {
|
||||
if (req.method in api) {
|
||||
return api[req.method](req, res);
|
||||
}
|
||||
|
||||
return res.status(StatusCodes.NOT_FOUND).end();
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
import { Client, Environment } from "square";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
const client = new Client({
|
||||
environment: Environment.Sandbox,
|
||||
accessToken: process.env.SQUARE_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
const cancelSubscription = async (id) => {
|
||||
const { result: subscriptionsResponse } = await client.subscriptionsApi.cancelSubscription(id);
|
||||
const { subscription } = subscriptionsResponse;
|
||||
|
||||
return subscription;
|
||||
};
|
||||
|
||||
const getActiveSubscription = async (customerId) => {
|
||||
// create subscriptions search query
|
||||
const query = { filter: { customerIds: [customerId] } };
|
||||
|
||||
// query subscriptions with given search criteria
|
||||
const { result: subscriptionsResponse } = await client.subscriptionsApi.searchSubscriptions({ query });
|
||||
const { subscriptions } = subscriptionsResponse;
|
||||
|
||||
// get active subscription with a set cancellation date
|
||||
return subscriptions.find(({ status, canceledDate }) => status === "ACTIVE" && !canceledDate);
|
||||
};
|
||||
|
||||
const api = {
|
||||
POST: async (req, res) => {
|
||||
try {
|
||||
const user = "NBE7TRXZPGZXNBD64JB6DR5AGR"; // req.headers["x-user"];
|
||||
const subscription = await getActiveSubscription(user);
|
||||
|
||||
if (!subscription) {
|
||||
return res.status(StatusCodes.BAD_REQUEST).end(); // no active subscription found
|
||||
}
|
||||
|
||||
const canceledSubscription = await cancelSubscription(subscription.id);
|
||||
|
||||
return res.json(canceledSubscription);
|
||||
} catch (error) {
|
||||
console.log(error.errors);
|
||||
|
||||
return res.status(StatusCodes.BAD_REQUEST).end(); // todo: error handling
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default (req, res) => {
|
||||
if (req.method in api) {
|
||||
return api[req.method](req, res);
|
||||
}
|
||||
|
||||
return res.status(StatusCodes.NOT_FOUND).end();
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
import { Client, Environment } from "square";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
const client = new Client({
|
||||
environment: Environment.Sandbox,
|
||||
accessToken: process.env.SQUARE_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
const updateSubscription = async (id, body) => {
|
||||
const { result: subscriptionsResponse } = await client.subscriptionsApi.updateSubscription(id, body);
|
||||
const { subscription } = subscriptionsResponse;
|
||||
|
||||
return subscription;
|
||||
};
|
||||
|
||||
const getActiveCanceledSubscription = async (customerId) => {
|
||||
// create subscriptions search query
|
||||
const query = { filter: { customerIds: [customerId] } };
|
||||
|
||||
// query subscriptions with given search criteria
|
||||
const { result: subscriptionsResponse } = await client.subscriptionsApi.searchSubscriptions({ query });
|
||||
const { subscriptions } = subscriptionsResponse;
|
||||
|
||||
// get active subscription with a set cancellation date
|
||||
return subscriptions.find(({ status, canceledDate }) => status === "ACTIVE" && canceledDate);
|
||||
};
|
||||
|
||||
const api = {
|
||||
POST: async (req, res) => {
|
||||
try {
|
||||
const user = "NBE7TRXZPGZXNBD64JB6DR5AGR"; // req.headers["x-user"];
|
||||
const subscription = await getActiveCanceledSubscription(user);
|
||||
|
||||
if (!subscription) {
|
||||
return res.status(StatusCodes.BAD_REQUEST).end(); // no active subscription with cancel date found
|
||||
}
|
||||
|
||||
// update the subscription setting empty canceledDate
|
||||
const updatedSubscription = await updateSubscription(subscription.id, {
|
||||
subscription: { ...subscription, canceledDate: "" },
|
||||
});
|
||||
|
||||
return res.json(updatedSubscription);
|
||||
} catch (error) {
|
||||
console.log(error.errors);
|
||||
|
||||
return res.status(StatusCodes.BAD_REQUEST).end(); // todo: error handling
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default (req, res) => {
|
||||
if (req.method in api) {
|
||||
return api[req.method](req, res);
|
||||
}
|
||||
|
||||
return res.status(StatusCodes.NOT_FOUND).end();
|
||||
};
|
|
@ -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,59 @@
|
|||
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) {
|
||||
console.log(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,276 @@
|
|||
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?.storageUsed ?? 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,193 @@
|
|||
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.storageUsed)}</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,83 @@
|
|||
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 },
|
||||
});
|
||||
|
||||
console.log(flow, status, data);
|
||||
|
||||
if (status === 200) return { props: { flow: data } };
|
||||
|
||||
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
|
||||
} catch (error) {
|
||||
console.log(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;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export const isFreeTier = (tier) => tier === 1;
|
||||
export const isPaidTier = (tier) => tier > 1;
|
|
@ -0,0 +1,15 @@
|
|||
import useSWR from "swr";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
const fetcher = (url) =>
|
||||
fetch(url).then((res) => {
|
||||
if (res.status === StatusCodes.UNAUTHORIZED) {
|
||||
window.location.href = `/auth/login?return_to=${encodeURIComponent(window.location.href)}`;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
});
|
||||
|
||||
export default function useAccountsApi(key, config) {
|
||||
return useSWR(key, fetcher, config);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"id": "bda73c77-1e21-4bfd-b85a-322fce2e4576",
|
||||
"expires_at": "2020-01-28T13:48:04.690715Z",
|
||||
"issued_at": "2020-01-28T13:38:04.690732Z",
|
||||
"request_url": "http://127.0.0.1:4455/auth/browser/login",
|
||||
"methods": {
|
||||
"oidc": {
|
||||
"method": "oidc",
|
||||
"config": {
|
||||
"action": "http://127.0.0.1:4455/.ory/kratos/public/auth/browser/methods/oidc/auth/bda73c77-1e21-4bfd-b85a-322fce2e4576",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"value": "QJreyXtUD4oUSJfGNjA/+6ydsQyq0o/rfTL6QK86VadVFg6mwgX5x1QHVQ6uRqKxmwAcavQup3ILCSwl7ke97g=="
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"method": "password",
|
||||
"config": {
|
||||
"action": "http://127.0.0.1:4455/.ory/kratos/public/auth/browser/methods/password/login?request=bda73c77-1e21-4bfd-b85a-322fce2e4576",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"value": "M1gAKA8fIhw4JOpQ/5m9mKARBAvKhzWkyhbxjtZNLG8m1NBHtk7UUXhrKJhn7yDSl4ypbZR7HT28LSfrlzDEJg=="
|
||||
},
|
||||
{ "name": "identifier", "type": "text", "required": true, "value": "asfdasdffads" },
|
||||
{ "name": "password", "type": "password", "required": true }
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"message": "The provided credentials are invalid. Check for spelling mistakes in your password or username, email address, or phone number."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"id": "01e21a99-8e69-41f9-b3ba-4967f714c8eb",
|
||||
"type": "browser",
|
||||
"expires_at": "2021-03-18T15:46:30.634635Z",
|
||||
"issued_at": "2021-03-18T14:46:30.634635Z",
|
||||
"request_url": "http://oathkeeper:4455/self-service/recovery/browser",
|
||||
"messages": null,
|
||||
"methods": {
|
||||
"link": {
|
||||
"method": "link",
|
||||
"config": {
|
||||
"action": "http://127.0.0.1:4455/.ory/kratos/public/self-service/recovery/methods/link?flow=01e21a99-8e69-41f9-b3ba-4967f714c8eb",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"value": "XvPfLJzxmcTWnr2rmLzFcED9Ef1PIkqSBQgx5t0yu0WuIp2S6ll+B9cQ0ClIcLQ=="
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"type": "email",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": "choose_method"
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"id": "dbff7b96-8116-42c5-8624-f9fb28f1db15",
|
||||
"expires_at": "2020-01-28T13:49:01.2274112Z",
|
||||
"issued_at": "2020-01-28T13:39:01.2274261Z",
|
||||
"request_url": "http://127.0.0.1:4455/auth/browser/registration",
|
||||
"methods": {
|
||||
"oidc": {
|
||||
"method": "oidc",
|
||||
"config": {
|
||||
"action": "http://127.0.0.1:4455/.ory/kratos/public/auth/browser/methods/oidc/auth/dbff7b96-8116-42c5-8624-f9fb28f1db15",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"value": "xwb6A6iHdsguYwkAM6m3jj196E7TcmiWpAavIRxuAgXSiipsEdaAhW4sy8ir3yrECuBFKI2OQA/SPXlEXRPqTA=="
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"method": "password",
|
||||
"config": {
|
||||
"errors": [
|
||||
{
|
||||
"message": "The provided credentials are invalid. Check for spelling mistakes in your password or username, email address, or phone number."
|
||||
}
|
||||
],
|
||||
"action": "http://127.0.0.1:4455/.ory/kratos/public/auth/browser/methods/password/registration?request=dbff7b96-8116-42c5-8624-f9fb28f1db15",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"value": "xLg4B9WnuC0Ue+j9ay5EQvleaJpOl0H9xJJ7W3+Bwv7RNOhobPZOYFQ0KjXzWNkIzsPF/BBraWSyqa0+Pvwqtw=="
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"errors": [{ "message": "password: Is required" }]
|
||||
},
|
||||
{
|
||||
"name": "traits.email",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"errors": [
|
||||
{ "message": "traits.email: String length must be greater than or equal to 3" },
|
||||
{ "message": "traits.email: Does not match format 'email'" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"id": "e01f6bf9-845c-4c4b-a3ce-60691dd7a97c",
|
||||
"type": "browser",
|
||||
"expires_at": "2021-02-18T13:31:43.947193Z",
|
||||
"issued_at": "2021-02-18T12:31:43.947193Z",
|
||||
"request_url": "http://oathkeeper:4455/self-service/settings/browser",
|
||||
"messages": null,
|
||||
"methods": {
|
||||
"password": {
|
||||
"method": "password",
|
||||
"config": {
|
||||
"action": "https://account.siasky.xyz/.ory/kratos/public/self-service/settings/methods/password?flow=e01f6bf9-845c-4c4b-a3ce-60691dd7a97c",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{ "name": "password", "type": "password", "required": true },
|
||||
{
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"value": "i33iydUBN6jqwrkHlLPZ3ap7Ah4uC0UrTadn7zKT0jyouDpr+DF0/1Hbkshye9t7IKwyzOcLt7i6oR/OoEVg+g=="
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"method": "profile",
|
||||
"config": {
|
||||
"action": "https://account.siasky.xyz/.ory/kratos/public/self-service/settings/methods/profile?flow=e01f6bf9-845c-4c4b-a3ce-60691dd7a97c",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"required": true,
|
||||
"value": "i33iydUBN6jqwrkHlLPZ3ap7Ah4uC0UrTadn7zKT0jyouDpr+DF0/1Hbkshye9t7IKwyzOcLt7i6oR/OoEVg+g=="
|
||||
},
|
||||
{ "name": "traits.email", "type": "email", "value": "karol@nebulous.tech" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"id": "ab776d6d-f324-4fa7-a728-7587d5215481",
|
||||
"schema_id": "default",
|
||||
"schema_url": "",
|
||||
"traits": { "email": "karol@nebulous.tech" }
|
||||
},
|
||||
"state": "show_form"
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
.container {
|
||||
min-height: 100vh;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border-top: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title a:hover,
|
||||
.title a:focus,
|
||||
.title a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.5;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
max-width: 800px;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1rem;
|
||||
flex-basis: 45%;
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus,
|
||||
.card:active {
|
||||
color: #0070f3;
|
||||
border-color: #0070f3;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans,
|
||||
Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
module.exports = {
|
||||
purge: ["./src/**/*.js"],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["Metropolis", "Helvetica", "Arial", "Sans-Serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {
|
||||
backgroundColor: ["disabled"],
|
||||
textColor: ["disabled"],
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/forms")],
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:15.8.0-alpine
|
||||
FROM node:15.12.0-alpine
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:15.8.0-alpine
|
||||
FROM node:15.12.0-alpine
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
FROM node:15.8.0-alpine
|
||||
FROM node:15.12.0-alpine
|
||||
|
||||
RUN apk add --no-cache autoconf automake libtool gcc make g++ zlib-dev file nasm util-linux
|
||||
RUN apk update && apk add autoconf automake libtool gcc make g++ zlib-dev file nasm util-linux
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
COPY package.json .
|
||||
|
||||
ENV CYPRESS_INSTALL_BINARY 0
|
||||
|
||||
RUN yarn --no-lockfile
|
||||
|
||||
COPY src ./src
|
||||
COPY static ./static
|
||||
COPY gatsby-config.js .
|
||||
COPY package.json .
|
||||
|
||||
ENV CYPRESS_INSTALL_BINARY 0
|
||||
ARG WITH_ACCOUNTS=0
|
||||
|
||||
ENV GATSBY_TELEMETRY_DISABLED 1
|
||||
RUN yarn --no-lockfile
|
||||
RUN yarn build
|
||||
ENV GATSBY_WITH_ACCOUNTS $WITH_ACCOUNTS
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
CMD [ "sh", "-c", "yarn build && yarn serve --host 0.0.0.0" ]
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"dependencies": {
|
||||
"@fontsource/metropolis": "^4.2.1",
|
||||
"axios": "0.21.1",
|
||||
"boolean": "^3.0.2",
|
||||
"bytes": "3.1.0",
|
||||
"classnames": "2.2.6",
|
||||
"gatsby": "^3.0.4",
|
||||
|
@ -17,6 +18,7 @@
|
|||
"gatsby-plugin-sass": "^4.0.2",
|
||||
"gatsby-source-filesystem": "^3.0.0",
|
||||
"http-status-codes": "2.1.4",
|
||||
"js-cookie": "^2.2.1",
|
||||
"jsonp": "0.2.1",
|
||||
"node-sass": "5.0.0",
|
||||
"path-browserify": "1.0.1",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { boolean } from "boolean";
|
||||
import logo from "../../images/logo.svg";
|
||||
import "./HomeTop.scss";
|
||||
import { Skynet, Deco1, Deco2 } from "../../svg";
|
||||
|
@ -15,6 +16,18 @@ export default function HomeTop() {
|
|||
The decentralized CDN and file sharing platform for devs. Skynet is the storage foundation for a Free Internet!
|
||||
</p>
|
||||
|
||||
{boolean(process.env.GATSBY_WITH_ACCOUNTS) && (
|
||||
<p className="auth-links">
|
||||
<a href="/account/auth/registration" className="link">
|
||||
Sign up now!
|
||||
</a>{" "}
|
||||
Already have an account? Go to your{" "}
|
||||
<a href="/account/" className="link">
|
||||
dashboard
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Deco1 className="deco-1" />
|
||||
<Deco2 className="deco-2" />
|
||||
</div>
|
||||
|
|
|
@ -36,10 +36,22 @@
|
|||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
|
||||
&.auth-links {
|
||||
font-size: 18px;
|
||||
|
||||
.link {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $largebp) {
|
||||
font-size: 24px;
|
||||
max-width: 670px;
|
||||
}
|
||||
|
||||
& + p {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Get current working directory (pwd doesn't cut it)
|
||||
cwd=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
|
||||
# Set the environment:
|
||||
set -o allexport
|
||||
source $cwd/../.env
|
||||
set +o allexport
|
||||
# Check for AWS credentials:
|
||||
if [[ $AWS_ACCESS_KEY_ID == "" || $AWS_SECRET_ACCESS_KEY == "" ]]; then
|
||||
echo "Missing AWS credentials!"
|
||||
exit 1
|
||||
fi
|
||||
# Take the current datetime:
|
||||
DT=`date +%Y-%m-%d`
|
||||
# Create the backup:
|
||||
docker exec cockroach \
|
||||
cockroach sql \
|
||||
--host cockroach:26257 \
|
||||
--certs-dir=/certs \
|
||||
--execute="BACKUP TO 's3://skynet-crdb-backups/backups/cockroach/$DT?AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID&AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY';"
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/bash
|
||||
|
||||
BACKUP=$1
|
||||
if [[ $BACKUP == "" ]]; then
|
||||
echo "No backup name given. It should look like '2020-01-29'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current working directory (pwd doesn't cut it)
|
||||
cwd=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
|
||||
# Set the environment:
|
||||
set -o allexport
|
||||
source $cwd/../.env
|
||||
set +o allexport
|
||||
# Check for AWS credentials:
|
||||
if [[ $AWS_ACCESS_KEY_ID == "" || $AWS_SECRET_ACCESS_KEY == "" ]]; then
|
||||
echo "Missing AWS credentials!"
|
||||
exit 1
|
||||
fi
|
||||
# Restore the backup:
|
||||
docker exec cockroach \
|
||||
cockroach sql \
|
||||
--host cockroach:26257 \
|
||||
--certs-dir=/certs \
|
||||
--execute="RESTORE DATABASE defaultdb FROM 's3://skynet-crdb-backups/backups/cockroach/$DT?AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID&AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY';"
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Get current working directory (pwd doesn't cut it)
|
||||
cwd=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
|
||||
# Set the environment:
|
||||
set -o allexport
|
||||
source $cwd/../.env
|
||||
set +o allexport
|
||||
# Check for AWS credentials:
|
||||
if [[ $AWS_ACCESS_KEY_ID == "" || $AWS_SECRET_ACCESS_KEY == "" ]]; then
|
||||
echo "Missing AWS credentials!"
|
||||
exit 1
|
||||
fi
|
||||
# Take the current datetime:
|
||||
DT=`date +%Y-%m-%d`
|
||||
# Check if a backup already exists:
|
||||
totalFoundObjects=$(aws s3 ls s3://skynet-crdb-backups/backups/mongo/ --recursive --summarize | grep "$DT.tgz" | wc -l)
|
||||
if [ "$totalFoundObjects" -eq "1" ]; then
|
||||
echo "Backup already exists for today. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
# Create the backup:
|
||||
docker exec mongo \
|
||||
mongodump \
|
||||
-o /data/db/backups/$DT \
|
||||
mongodb://$SKYNET_DB_USER:$SKYNET_DB_PASS@$SKYNET_DB_HOST:$SKYNET_DB_PORT
|
||||
# Compress the backup:
|
||||
cd $cwd/../docker/data/mongo/db/backups/ && tar -czf $DT.tgz $DT && cd -
|
||||
# Upload the backup to S3:
|
||||
aws s3 cp $DT.tgz s3://skynet-crdb-backups/backups/mongo/
|
||||
# Clean up
|
||||
rm -rf $DT.tgz $cwd/../docker/data/mongo/db/backups/$DT
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
|
||||
BACKUP=$1
|
||||
if [[ $BACKUP == "" ]]; then
|
||||
echo "No backup name given. It should look like '2020-01-29'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current working directory (pwd doesn't cut it)
|
||||
cwd=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
|
||||
# Set the environment:
|
||||
set -o allexport
|
||||
source $cwd/../.env
|
||||
set +o allexport
|
||||
# Check for AWS credentials:
|
||||
if [[ $AWS_ACCESS_KEY_ID == "" || $AWS_SECRET_ACCESS_KEY == "" ]]; then
|
||||
echo "Missing AWS credentials!"
|
||||
exit 1
|
||||
fi
|
||||
# Check if the backup exists:
|
||||
totalFoundObjects=$(aws s3 ls s3://skynet-crdb-backups/backups/mongo/ --recursive --summarize | grep "$DT.tgz" | wc -l)
|
||||
if [ "$totalFoundObjects" -eq "0" ]; then
|
||||
echo "This backup doesn't exist!"
|
||||
exit 1
|
||||
fi
|
||||
# Get the backup from S3:
|
||||
aws s3 cp s3://skynet-crdb-backups/backups/mongo/$BACKUP.tgz $BACKUP.tgz
|
||||
# Prepare a clean `to_restore` dir:
|
||||
rm -rf $cwd/../docker/data/mongo/db/backups/to_restore
|
||||
mkdir -p $cwd/../docker/data/mongo/db/backups/to_restore
|
||||
# Decompress the backup:
|
||||
tar -xzf $BACKUP.tgz -C $cwd/../docker/data/mongo/db/backups/to_restore
|
||||
rm $BACKUP.tgz
|
||||
# Restore the backup:
|
||||
docker exec mongo \
|
||||
mongorestore \
|
||||
mongodb://$SKYNET_DB_USER:$SKYNET_DB_PASS@$SKYNET_DB_HOST:$SKYNET_DB_PORT \
|
||||
/data/db/backups/to_restore/$BACKUP
|
||||
# Clean up:
|
||||
rm -rf $cwd/../docker/data/mongo/db/backups/to_restore
|
|
@ -19,6 +19,8 @@ You may want to fork this repository and replace ssh keys in
|
|||
- [handshake](https://handshake.org) ([github](https://github.com/handshake-org/hsd)): full handshake node
|
||||
- [handshake-api](https://github.com/NebulousLabs/skynet-webportal/tree/master/packages/handshake-api): simple API talking to the handshake node - [read more](https://github.com/NebulousLabs/skynet-webportal/blob/master/packages/handshake-api/README.md)
|
||||
- [webapp](https://github.com/NebulousLabs/skynet-webportal/tree/master/packages/webapp): portal frontend application - [read more](https://github.com/NebulousLabs/skynet-webportal/blob/master/packages/webapp/README.md)
|
||||
- [kratos](https://www.ory.sh/kratos/): user account management system
|
||||
- [oathkeeper](https://www.ory.sh/oathkeeper/): identity and access proxy
|
||||
- discord integration
|
||||
- [funds-checker](funds-checker.py): script that checks wallet balance and sends status messages to discord periodically
|
||||
- [health-checker](health-checker.py): script that monitors health-check service for server health issues and reports them to discord periodically
|
||||
|
@ -83,10 +85,21 @@ At this point we have almost everything running, we just need to set up your wal
|
|||
- `CLOUDFLARE_AUTH_TOKEN` (optional) if using cloudflare as dns loadbalancer (need to change it in Caddyfile too)
|
||||
- `AWS_ACCESS_KEY_ID` (optional) if using route53 as a dns loadbalancer
|
||||
- `AWS_SECRET_ACCESS_KEY` (optional) if using route53 as a dns loadbalancer
|
||||
- `PORTAL_NAME` (optional) e.g. `siasky.xyz`
|
||||
- `DISCORD_BOT_TOKEN` (optional) if you're using Discord notifications for health checks and such
|
||||
- `SKYNET_DB_USER` (optional) if using `accounts` this is the MongoDB username
|
||||
- `SKYNET_DB_PASS` (optional) if using `accounts` this is the MongoDB password
|
||||
- `SKYNET_DB_HOST` (optional) if using `accounts` this is the MongoDB address or container name
|
||||
- `SKYNET_DB_PORT` (optional) if using `accounts` this is the MongoDB port
|
||||
- `COOKIE_DOMAIN` (optional) if using `accounts` this is the domain to which your cookies will be issued
|
||||
- `COOKIE_HASH_KEY` (optional) if using `accounts` hashing secret, at least 32 bytes
|
||||
- `COOKIE_ENC_KEY` (optional) if using `accounts` encryption key, at least 32 bytes
|
||||
|
||||
1. if you have a custom domain and you configured it in `DOMAIN_NAME`, edit `/home/user/skynet-webportal/docker/caddy/Caddyfile` and uncomment `import custom.domain`
|
||||
1. only for siasky.net domain instances: edit `/home/user/skynet-webportal/docker/caddy/Caddyfile`, uncomment `import siasky.net`
|
||||
1. `docker-compose up -d` to restart the services so they pick up new env variables
|
||||
1. `docker exec caddy caddy reload --config /etc/caddy/Caddyfile` to reload Caddyfile configuration
|
||||
1. add your custom Kratos configuration to `/home/user/skynet-webportal/docker/kratos/config/kratos.yml` (in particular, the credentials for your mail server should be here, rather than in your source control). For a starting point you can take `docker/kratos/config/kratos.yml.sample`.
|
||||
|
||||
## Subdomains
|
||||
|
||||
|
|
|
@ -23,17 +23,27 @@ docker-compose --version # sanity check
|
|||
# Create dummy .env file for docker-compose usage with variables
|
||||
# * DOMAIN_NAME - the domain name your server is using ie. example.com
|
||||
# * SKYNET_PORTAL_API - absolute url to the portal api ie. https://example.com
|
||||
# * SKYNET_DASHBOARD_URL - (optional) absolute url to the portal dashboard ie. https://account.example.com
|
||||
# * EMAIL_ADDRESS - this is the administrator contact email you need to supply for communication regarding SSL certification
|
||||
# * HSD_API_KEY - this is auto generated secure key for your handshake service integration
|
||||
# * CLOUDFLARE_AUTH_TOKEN` - (optional) if using cloudflare as dns loadbalancer (need to change it in Caddyfile too)
|
||||
# * CLOUDFLARE_AUTH_TOKEN - (optional) if using cloudflare as dns loadbalancer (need to change it in Caddyfile too)
|
||||
# * AWS_ACCESS_KEY_ID - (optional) if using route53 as a dns loadbalancer
|
||||
# * AWS_SECRET_ACCESS_KEY - (optional) if using route53 as a dns loadbalancer
|
||||
# * API_PORT - (optional) the port on which siad is listening, defaults to 9980
|
||||
# * PORTAL_NAME - the name of the portal, required by the discord bot
|
||||
# * DISCORD_BOT_TOKEN - required by the discord bot
|
||||
# * DISCORD_BOT_TOKEN - (optional) only required if you're using the discord notifications integration
|
||||
# * SKYNET_DB_USER - (optional) if using `accounts` this is the MongoDB username
|
||||
# * SKYNET_DB_PASS - (optional) if using `accounts` this is the MongoDB password
|
||||
# * SKYNET_DB_HOST - (optional) if using `accounts` this is the MongoDB address or container name
|
||||
# * SKYNET_DB_PORT - (optional) if using `accounts` this is the MongoDB port
|
||||
# * COOKIE_DOMAIN - (optional) if using `accounts` this is the domain to which your cookies will be issued
|
||||
# * COOKIE_HASH_KEY - (optional) if using `accounts` hashing secret, at least 32 bytes
|
||||
# * COOKIE_ENC_KEY - (optional) if using `accounts` encryption key, at least 32 bytes
|
||||
# * CR_IP - (optional) if using `accounts` the public IP/domain of your server, e.g. `helsinki.siasky.net`
|
||||
# * CR_CLUSTER_NODES - (optional) if using `accounts` the list of servers (with ports) which make up your CockroachDB cluster, e.g. `helsinki.siasky.net:26257,germany.siasky.net:26257,us-east.siasky.net:26257`
|
||||
if ! [ -f /home/user/skynet-webportal/.env ]; then
|
||||
HSD_API_KEY=$(openssl rand -base64 32) # generate safe random key for handshake
|
||||
printf "DOMAIN_NAME=example.com\nSKYNET_PORTAL_API=https://example.com\nEMAIL_ADDRESS=email@example.com\nSIA_WALLET_PASSWORD=\nHSD_API_KEY=${HSD_API_KEY}\nCLOUDFLARE_AUTH_TOKEN=\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nPORTAL_NAME=\nDISCORD_BOT_TOKEN=\n" > /home/user/skynet-webportal/.env
|
||||
printf "DOMAIN_NAME=example.com\nSKYNET_PORTAL_API=https://example.com\nSKYNET_DASHBOARD_URL=https://account.example.com\nEMAIL_ADDRESS=email@example.com\nSIA_WALLET_PASSWORD=\nHSD_API_KEY=${HSD_API_KEY}\nCLOUDFLARE_AUTH_TOKEN=\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nPORTAL_NAME=\nDISCORD_BOT_TOKEN=\n" > /home/user/skynet-webportal/.env
|
||||
fi
|
||||
|
||||
# Start docker container with nginx and client
|
||||
|
|
|
@ -14,7 +14,7 @@ mkdir -p /home/user/.ssh
|
|||
|
||||
# Install apt packages
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install ufw tmux ranger htop nload gcc g++ make git vim unzip curl
|
||||
sudo apt-get -y install ufw tmux ranger htop nload gcc g++ make git vim unzip curl awscli
|
||||
|
||||
# Setup GIT credentials (so commands like git stash would work)
|
||||
git config --global user.email "devs@nebulous.tech"
|
||||
|
|
Reference in New Issue