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
|
# dotenv environment variable files
|
||||||
.env*
|
.env*
|
||||||
|
./docker/kratos/config/kratos.yml
|
||||||
|
|
||||||
# Mac files
|
# Mac files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -82,3 +83,13 @@ docker/nginx/conf.d/server-override/*
|
||||||
__pycache__
|
__pycache__
|
||||||
/.idea/
|
/.idea/
|
||||||
/venv*
|
/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`
|
`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
|
## Contributing
|
||||||
|
|
||||||
### Testing Your Code
|
### Testing Your Code
|
||||||
|
|
||||||
Before pushing your code you should verify that it will pass our online test
|
Before pushing your code, you should verify that it will pass our online test suite.
|
||||||
suite.
|
|
||||||
|
|
||||||
**Cypress Tests**
|
**Cypress Tests**
|
||||||
Verify the Cypress test suite by doing the following:
|
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:
|
config:
|
||||||
- subnet: 10.10.10.0/24
|
- subnet: 10.10.10.0/24
|
||||||
|
|
||||||
volumes:
|
|
||||||
webapp:
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
sia:
|
sia:
|
||||||
build:
|
build:
|
||||||
|
@ -76,7 +73,6 @@ services:
|
||||||
- ./docker/data/nginx/logs:/usr/local/openresty/nginx/logs
|
- ./docker/data/nginx/logs:/usr/local/openresty/nginx/logs
|
||||||
- ./docker/data/nginx/skynet:/data/nginx/skynet:ro
|
- ./docker/data/nginx/skynet:/data/nginx/skynet:ro
|
||||||
- ./docker/data/sia/apipassword:/data/sia/apipassword:ro
|
- ./docker/data/sia/apipassword:/data/sia/apipassword:ro
|
||||||
- webapp:/var/www/webportal:ro
|
|
||||||
networks:
|
networks:
|
||||||
shared:
|
shared:
|
||||||
ipv4_address: 10.10.10.30
|
ipv4_address: 10.10.10.30
|
||||||
|
@ -94,9 +90,13 @@ services:
|
||||||
container_name: webapp
|
container_name: webapp
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
tty: true
|
|
||||||
volumes:
|
volumes:
|
||||||
- webapp:/usr/app/public
|
- ./docker/data/webapp/.cache:/usr/app/.cache
|
||||||
|
networks:
|
||||||
|
shared:
|
||||||
|
ipv4_address: 10.10.10.35
|
||||||
|
expose:
|
||||||
|
- 9000
|
||||||
|
|
||||||
handshake:
|
handshake:
|
||||||
build:
|
build:
|
||||||
|
@ -163,18 +163,3 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- handshake
|
- handshake
|
||||||
- handshake-api
|
- 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
|
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 git clone https://github.com/handshake-org/hsd.git /opt/hsd
|
||||||
RUN npm install --production
|
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
|
FROM openresty/openresty:1.19.3.1-2-bionic
|
||||||
|
|
||||||
# RUN apt-get update -qq && apt-get install cron logrotate -qq
|
# 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 ["sh", "-c", "service cron start;", "/usr/local/openresty/bin/openresty -g daemon off;"]
|
||||||
CMD ["/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
|
|
@ -57,6 +57,7 @@ server {
|
||||||
rewrite ^/portals /skynet/portals permanent;
|
rewrite ^/portals /skynet/portals permanent;
|
||||||
rewrite ^/stats /skynet/stats permanent;
|
rewrite ^/stats /skynet/stats permanent;
|
||||||
rewrite ^/skynet/blacklist /skynet/blocklist permanent;
|
rewrite ^/skynet/blacklist /skynet/blocklist permanent;
|
||||||
|
rewrite ^/account/(.*) https://account.$domain.$tld/$1 permanent;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# This is only safe workaround to reroute based on some conditions
|
# This is only safe workaround to reroute based on some conditions
|
||||||
|
@ -77,7 +78,7 @@ server {
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
include /etc/nginx/conf.d/include/cors;
|
||||||
|
|
||||||
root /var/www/webportal;
|
proxy_pass http://webapp:9000;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /docs {
|
location /docs {
|
||||||
|
@ -222,9 +223,6 @@ server {
|
||||||
end
|
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
|
# 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;
|
proxy_pass http://127.0.0.1/$skylink$rest;
|
||||||
|
|
||||||
|
@ -276,6 +274,40 @@ server {
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
proxy_set_header User-Agent: Sia-Agent;
|
||||||
proxy_read_timeout 600; # siad should timeout with 404 after 5 minutes
|
proxy_read_timeout 600; # siad should timeout with 404 after 5 minutes
|
||||||
proxy_pass http://siad/skynet/registry;
|
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 {
|
location /skynet/skyfile {
|
||||||
|
@ -306,8 +338,37 @@ server {
|
||||||
set $dir3 $3;
|
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 this call to siad endpoint (make sure the ip is correct)
|
||||||
proxy_pass http://siad/skynet/skyfile/$dir1/$dir2/$dir3$is_args$args;
|
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})(/.*)?)$" {
|
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
|
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
|
# 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
|
# decoded whitespaces and set will re-encode it for us before passing it to proxy_pass
|
||||||
set $skylink $1;
|
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_read_timeout 600;
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
proxy_set_header User-Agent: Sia-Agent;
|
||||||
# proxy this call to siad /skynet/skylink/ endpoint (make sure the ip is correct)
|
# 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;
|
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 custom locations, specific to the server
|
||||||
include /etc/nginx/conf.d/server-override/*;
|
include /etc/nginx/conf.d/server-override/*;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ worker_processes 1;
|
||||||
#pid logs/nginx.pid;
|
#pid logs/nginx.pid;
|
||||||
|
|
||||||
env SKYNET_PORTAL_API; # declare env variable to use it in config
|
env SKYNET_PORTAL_API; # declare env variable to use it in config
|
||||||
|
env ACCOUNTS_ENABLED; # declare env variable to use it in config
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
|
@ -67,4 +68,5 @@ http {
|
||||||
header_filter_by_lua 'ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API")';
|
header_filter_by_lua 'ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API")';
|
||||||
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
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/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"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
|
WORKDIR /usr/app
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:15.8.0-alpine
|
FROM node:15.12.0-alpine
|
||||||
|
|
||||||
WORKDIR /usr/app
|
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
|
WORKDIR /usr/app
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
|
||||||
|
ENV CYPRESS_INSTALL_BINARY 0
|
||||||
|
|
||||||
|
RUN yarn --no-lockfile
|
||||||
|
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY static ./static
|
COPY static ./static
|
||||||
COPY gatsby-config.js .
|
COPY gatsby-config.js .
|
||||||
COPY package.json .
|
|
||||||
|
|
||||||
ENV CYPRESS_INSTALL_BINARY 0
|
ARG WITH_ACCOUNTS=0
|
||||||
|
|
||||||
ENV GATSBY_TELEMETRY_DISABLED 1
|
ENV GATSBY_TELEMETRY_DISABLED 1
|
||||||
RUN yarn --no-lockfile
|
ENV GATSBY_WITH_ACCOUNTS $WITH_ACCOUNTS
|
||||||
RUN yarn build
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
CMD [ "sh", "-c", "yarn build && yarn serve --host 0.0.0.0" ]
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/metropolis": "^4.2.1",
|
"@fontsource/metropolis": "^4.2.1",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.1",
|
||||||
|
"boolean": "^3.0.2",
|
||||||
"bytes": "3.1.0",
|
"bytes": "3.1.0",
|
||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
"gatsby": "^3.0.4",
|
"gatsby": "^3.0.4",
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
"gatsby-plugin-sass": "^4.0.2",
|
"gatsby-plugin-sass": "^4.0.2",
|
||||||
"gatsby-source-filesystem": "^3.0.0",
|
"gatsby-source-filesystem": "^3.0.0",
|
||||||
"http-status-codes": "2.1.4",
|
"http-status-codes": "2.1.4",
|
||||||
|
"js-cookie": "^2.2.1",
|
||||||
"jsonp": "0.2.1",
|
"jsonp": "0.2.1",
|
||||||
"node-sass": "5.0.0",
|
"node-sass": "5.0.0",
|
||||||
"path-browserify": "1.0.1",
|
"path-browserify": "1.0.1",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { boolean } from "boolean";
|
||||||
import logo from "../../images/logo.svg";
|
import logo from "../../images/logo.svg";
|
||||||
import "./HomeTop.scss";
|
import "./HomeTop.scss";
|
||||||
import { Skynet, Deco1, Deco2 } from "../../svg";
|
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!
|
The decentralized CDN and file sharing platform for devs. Skynet is the storage foundation for a Free Internet!
|
||||||
</p>
|
</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" />
|
<Deco1 className="deco-1" />
|
||||||
<Deco2 className="deco-2" />
|
<Deco2 className="deco-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,10 +36,22 @@
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
&.auth-links {
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: $largebp) {
|
@media (min-width: $largebp) {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
max-width: 670px;
|
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](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)
|
- [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)
|
- [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
|
- discord integration
|
||||||
- [funds-checker](funds-checker.py): script that checks wallet balance and sends status messages to discord periodically
|
- [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
|
- [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)
|
- `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_ACCESS_KEY_ID` (optional) if using route53 as a dns loadbalancer
|
||||||
- `AWS_SECRET_ACCESS_KEY` (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. 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. 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-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. `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
|
## Subdomains
|
||||||
|
|
||||||
|
|
|
@ -23,17 +23,27 @@ docker-compose --version # sanity check
|
||||||
# Create dummy .env file for docker-compose usage with variables
|
# Create dummy .env file for docker-compose usage with variables
|
||||||
# * DOMAIN_NAME - the domain name your server is using ie. example.com
|
# * 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_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
|
# * 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
|
# * 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_ACCESS_KEY_ID - (optional) if using route53 as a dns loadbalancer
|
||||||
# * AWS_SECRET_ACCESS_KEY - (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
|
# * 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
|
# * 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
|
if ! [ -f /home/user/skynet-webportal/.env ]; then
|
||||||
HSD_API_KEY=$(openssl rand -base64 32) # generate safe random key for handshake
|
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
|
fi
|
||||||
|
|
||||||
# Start docker container with nginx and client
|
# Start docker container with nginx and client
|
||||||
|
|
|
@ -14,7 +14,7 @@ mkdir -p /home/user/.ssh
|
||||||
|
|
||||||
# Install apt packages
|
# Install apt packages
|
||||||
sudo apt-get update
|
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)
|
# Setup GIT credentials (so commands like git stash would work)
|
||||||
git config --global user.email "devs@nebulous.tech"
|
git config --global user.email "devs@nebulous.tech"
|
||||||
|
|
Reference in New Issue