upgrade accounts to new api
This commit is contained in:
parent
273f46ef30
commit
74084a8da8
|
@ -53,7 +53,6 @@ typings/
|
||||||
|
|
||||||
# dotenv environment variable files
|
# dotenv environment variable files
|
||||||
.env*
|
.env*
|
||||||
./docker/kratos/config/kratos.yml
|
|
||||||
|
|
||||||
# Mac files
|
# Mac files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -86,13 +85,3 @@ 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
|
|
||||||
|
|
|
@ -29,18 +29,22 @@ services:
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- SKYNET_DB_HOST=${SKYNET_DB_HOST}
|
- ACCOUNTS_EMAIL_URI=${ACCOUNTS_EMAIL_URI}
|
||||||
- SKYNET_DB_PORT=${SKYNET_DB_PORT}
|
- ACCOUNTS_JWKS_FILE=/data/jwks.json
|
||||||
- SKYNET_DB_USER=${SKYNET_DB_USER}
|
|
||||||
- SKYNET_DB_PASS=${SKYNET_DB_PASS}
|
|
||||||
- COOKIE_DOMAIN=${COOKIE_DOMAIN}
|
- COOKIE_DOMAIN=${COOKIE_DOMAIN}
|
||||||
- COOKIE_HASH_KEY=${COOKIE_HASH_KEY}
|
- COOKIE_HASH_KEY=${COOKIE_HASH_KEY}
|
||||||
- COOKIE_ENC_KEY=${COOKIE_ENC_KEY}
|
- COOKIE_ENC_KEY=${COOKIE_ENC_KEY}
|
||||||
|
- PORTAL_DOMAIN=${PORTAL_DOMAIN}
|
||||||
|
- SERVER_DOMAIN=${SERVER_DOMAIN}
|
||||||
|
- SKYNET_DB_HOST=${SKYNET_DB_HOST:-mongo}
|
||||||
|
- SKYNET_DB_PORT=${SKYNET_DB_PORT:-27017}
|
||||||
|
- SKYNET_DB_USER=${SKYNET_DB_USER}
|
||||||
|
- SKYNET_DB_PASS=${SKYNET_DB_PASS}
|
||||||
- STRIPE_API_KEY=${STRIPE_API_KEY}
|
- STRIPE_API_KEY=${STRIPE_API_KEY}
|
||||||
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
||||||
- SKYNET_ACCOUNTS_LOG_LEVEL=${SKYNET_ACCOUNTS_LOG_LEVEL}
|
- SKYNET_ACCOUNTS_LOG_LEVEL=${SKYNET_ACCOUNTS_LOG_LEVEL:-info}
|
||||||
- KRATOS_ADDR=${KRATOS_ADDR}
|
volumes:
|
||||||
- OATHKEEPER_ADDR=${OATHKEEPER_ADDR}
|
- ./docker/data/accounts:/data
|
||||||
expose:
|
expose:
|
||||||
- 3000
|
- 3000
|
||||||
networks:
|
networks:
|
||||||
|
@ -48,50 +52,6 @@ services:
|
||||||
ipv4_address: 10.10.10.70
|
ipv4_address: 10.10.10.70
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongo
|
- mongo
|
||||||
- oathkeeper
|
|
||||||
|
|
||||||
kratos-migrate:
|
|
||||||
image: oryd/kratos:v0.5.5-alpha.1
|
|
||||||
container_name: kratos-migrate
|
|
||||||
restart: "no"
|
|
||||||
logging: *default-logging
|
|
||||||
environment:
|
|
||||||
- DSN=cockroach://root@cockroach:26257/defaultdb?max_conns=20&max_idle_conns=4&sslmode=verify-full&sslcert=/certs/node.crt&sslkey=/certs/node.key&sslrootcert=/certs/ca.crt
|
|
||||||
- SQA_OPT_OUT=true
|
|
||||||
volumes:
|
|
||||||
- ./docker/kratos/config:/etc/config/kratos
|
|
||||||
- ./docker/data/cockroach/sqlite:/var/lib/sqlite
|
|
||||||
- ./docker/kratos/cr_certs:/certs
|
|
||||||
command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes
|
|
||||||
networks:
|
|
||||||
shared:
|
|
||||||
ipv4_address: 10.10.10.80
|
|
||||||
depends_on:
|
|
||||||
- cockroach
|
|
||||||
|
|
||||||
kratos:
|
|
||||||
image: oryd/kratos:v0.5.5-alpha.1
|
|
||||||
container_name: kratos
|
|
||||||
restart: unless-stopped
|
|
||||||
logging: *default-logging
|
|
||||||
expose:
|
|
||||||
- 4433 # public
|
|
||||||
- 4434 # admin
|
|
||||||
environment:
|
|
||||||
- DSN=cockroach://root@cockroach:26257/defaultdb?max_conns=20&max_idle_conns=4&sslmode=verify-full&sslcert=/certs/node.crt&sslkey=/certs/node.key&sslrootcert=/certs/ca.crt
|
|
||||||
- LOG_LEVEL=trace
|
|
||||||
- SERVE_PUBLIC_BASE_URL=${SKYNET_DASHBOARD_URL}/.ory/kratos/public/
|
|
||||||
- SQA_OPT_OUT=true
|
|
||||||
command: serve -c /etc/config/kratos/kratos.yml
|
|
||||||
volumes:
|
|
||||||
- ./docker/kratos/config:/etc/config/kratos
|
|
||||||
- ./docker/data/cockroach/sqlite:/var/lib/sqlite
|
|
||||||
- ./docker/kratos/cr_certs:/certs
|
|
||||||
networks:
|
|
||||||
shared:
|
|
||||||
ipv4_address: 10.10.10.81
|
|
||||||
depends_on:
|
|
||||||
- kratos-migrate
|
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
build:
|
build:
|
||||||
|
@ -105,50 +65,13 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- NEXT_PUBLIC_SKYNET_PORTAL_API=${SKYNET_PORTAL_API}
|
- NEXT_PUBLIC_SKYNET_PORTAL_API=${SKYNET_PORTAL_API}
|
||||||
- NEXT_PUBLIC_SKYNET_DASHBOARD_URL=${SKYNET_DASHBOARD_URL}
|
- NEXT_PUBLIC_SKYNET_DASHBOARD_URL=${SKYNET_DASHBOARD_URL}
|
||||||
- NEXT_PUBLIC_KRATOS_BROWSER_URL=${SKYNET_DASHBOARD_URL}/.ory/kratos/public
|
|
||||||
- NEXT_PUBLIC_KRATOS_PUBLIC_URL=http://oathkeeper:4455/.ory/kratos/public
|
|
||||||
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
|
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
|
||||||
|
volumes:
|
||||||
|
- ./docker/data/dashboard/.next:/usr/app/.next
|
||||||
networks:
|
networks:
|
||||||
shared:
|
shared:
|
||||||
ipv4_address: 10.10.10.85
|
ipv4_address: 10.10.10.85
|
||||||
expose:
|
expose:
|
||||||
- 3000
|
- 3000
|
||||||
depends_on:
|
depends_on:
|
||||||
- oathkeeper
|
- mongo
|
||||||
|
|
||||||
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
|
|
||||||
logging: *default-logging
|
|
||||||
networks:
|
|
||||||
shared:
|
|
||||||
ipv4_address: 10.10.10.83
|
|
||||||
depends_on:
|
|
||||||
- kratos
|
|
||||||
|
|
||||||
cockroach:
|
|
||||||
image: cockroachdb/cockroach:v20.2.3
|
|
||||||
container_name: cockroach
|
|
||||||
restart: unless-stopped
|
|
||||||
logging: *default-logging
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
command: start --advertise-addr=${CR_IP} --join=${CR_CLUSTER_NODES} --certs-dir=/certs --listen-addr=0.0.0.0:26257 --http-addr=0.0.0.0:8080
|
|
||||||
volumes:
|
|
||||||
- ./docker/data/cockroach/sqlite:/cockroach/cockroach-data
|
|
||||||
- ./docker/cockroach/certs:/certs
|
|
||||||
ports:
|
|
||||||
- "4080:8080"
|
|
||||||
- "26257:26257"
|
|
||||||
networks:
|
|
||||||
shared:
|
|
||||||
ipv4_address: 10.10.10.84
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
FROM golang:1.16.7
|
FROM golang:1.16.7
|
||||||
LABEL maintainer="NebulousLabs <devs@nebulous.tech>"
|
LABEL maintainer="NebulousLabs <devs@siasky.net>"
|
||||||
|
|
||||||
ENV GOOS linux
|
ENV GOOS linux
|
||||||
ENV GOARCH amd64
|
ENV GOARCH amd64
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
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,31 +0,0 @@
|
||||||
{
|
|
||||||
"$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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
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
|
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
"$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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
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).
|
|
|
@ -1,116 +0,0 @@
|
||||||
- id: "ory:kratos:public"
|
|
||||||
upstream:
|
|
||||||
preserve_host: true
|
|
||||||
url: "http://kratos:4433"
|
|
||||||
strip_path: /.ory/kratos/public
|
|
||||||
match:
|
|
||||||
url: "http://oathkeeper:4455/.ory/kratos/public/<**>"
|
|
||||||
methods:
|
|
||||||
- GET
|
|
||||||
- POST
|
|
||||||
- PUT
|
|
||||||
- DELETE
|
|
||||||
- PATCH
|
|
||||||
authenticators:
|
|
||||||
- handler: noop
|
|
||||||
authorizer:
|
|
||||||
handler: allow
|
|
||||||
mutators:
|
|
||||||
- handler: noop
|
|
||||||
|
|
||||||
- id: "dashboard:anonymous"
|
|
||||||
upstream:
|
|
||||||
preserve_host: true
|
|
||||||
url: "http://dashboard:3000"
|
|
||||||
match:
|
|
||||||
url: "http://oathkeeper:4455/<{_next/**,auth/**,recovery,verify,error,favicon.ico}{/,}>"
|
|
||||||
methods:
|
|
||||||
- GET
|
|
||||||
authenticators:
|
|
||||||
- handler: anonymous
|
|
||||||
authorizer:
|
|
||||||
handler: allow
|
|
||||||
mutators:
|
|
||||||
- handler: noop
|
|
||||||
|
|
||||||
- id: "dashboard:protected"
|
|
||||||
upstream:
|
|
||||||
preserve_host: true
|
|
||||||
url: "http://dashboard:3000"
|
|
||||||
match:
|
|
||||||
url: "http://oathkeeper:4455/<{,api/**,settings,uploads,downloads,payments}>"
|
|
||||||
methods:
|
|
||||||
- GET
|
|
||||||
- POST
|
|
||||||
- PUT
|
|
||||||
- DELETE
|
|
||||||
- PATCH
|
|
||||||
authenticators:
|
|
||||||
- handler: cookie_session
|
|
||||||
authorizer:
|
|
||||||
handler: allow
|
|
||||||
mutators:
|
|
||||||
- handler: id_token
|
|
||||||
- handler: header
|
|
||||||
config:
|
|
||||||
headers:
|
|
||||||
X-User: "{{ print .Subject }}"
|
|
||||||
errors:
|
|
||||||
- handler: redirect
|
|
||||||
config:
|
|
||||||
to: http://127.0.0.1/auth/login
|
|
||||||
|
|
||||||
- id: "accounts:anonymous"
|
|
||||||
upstream:
|
|
||||||
preserve_host: true
|
|
||||||
url: "http://accounts:3000"
|
|
||||||
match:
|
|
||||||
url: "http://oathkeeper<{,:4455}>/<{health,stripe/prices,stripe/webhook}>"
|
|
||||||
methods:
|
|
||||||
- GET
|
|
||||||
- POST
|
|
||||||
authenticators:
|
|
||||||
- handler: anonymous
|
|
||||||
authorizer:
|
|
||||||
handler: allow
|
|
||||||
mutators:
|
|
||||||
- handler: noop
|
|
||||||
|
|
||||||
- id: "accounts:public"
|
|
||||||
upstream:
|
|
||||||
preserve_host: true
|
|
||||||
url: "http://accounts:3000"
|
|
||||||
match:
|
|
||||||
url: "http://oathkeeper<{,:4455}>/<{user/limits}>"
|
|
||||||
methods:
|
|
||||||
- GET
|
|
||||||
authenticators:
|
|
||||||
- handler: cookie_session
|
|
||||||
- handler: noop
|
|
||||||
authorizer:
|
|
||||||
handler: allow
|
|
||||||
mutators:
|
|
||||||
- handler: id_token
|
|
||||||
|
|
||||||
- id: "accounts:protected"
|
|
||||||
upstream:
|
|
||||||
preserve_host: true
|
|
||||||
url: "http://accounts:3000"
|
|
||||||
match:
|
|
||||||
url: "http://oathkeeper<{,:4455}>/<{login,logout,user,user/uploads,user/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
|
|
|
@ -1,94 +0,0 @@
|
||||||
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 }}
|
|
||||||
}
|
|
|
@ -5,6 +5,18 @@ include /etc/nginx/conf.d/include/ssl-settings;
|
||||||
include /etc/nginx/conf.d/include/init-optional-variables;
|
include /etc/nginx/conf.d/include/init-optional-variables;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_redirect http://127.0.0.1/ https://$host/;
|
proxy_pass http://dashboard:3000;
|
||||||
proxy_pass http://oathkeeper:4455;
|
}
|
||||||
|
|
||||||
|
location /api/stripe/billing {
|
||||||
|
proxy_pass http://dashboard:3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/stripe/checkout {
|
||||||
|
proxy_pass http://dashboard:3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
rewrite /api/(.*) /$1 break;
|
||||||
|
proxy_pass http://accounts:3000;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
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,6 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals",
|
||||||
|
"rules": {
|
||||||
|
"@next/next/no-html-link-for-pages": "off"
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,6 @@ RUN yarn --frozen-lockfile
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY styles ./styles
|
COPY styles ./styles
|
||||||
COPY postcss.config.js tailwind.config.js ./
|
COPY .eslintrc.json postcss.config.js tailwind.config.js ./
|
||||||
|
|
||||||
CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|KRATOS|STRIPE' > .env.local && yarn build && yarn start"]
|
CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|STRIPE' > .env.local && yarn build && yarn start"]
|
||||||
|
|
|
@ -8,11 +8,10 @@
|
||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/metropolis": "4.5.0",
|
"@fontsource/sora": "4.5.0",
|
||||||
"@ory/kratos-client": "0.5.4-alpha.1",
|
"@fontsource/source-sans-pro": "4.5.0",
|
||||||
"@stripe/react-stripe-js": "1.6.0",
|
"@stripe/react-stripe-js": "1.6.0",
|
||||||
"@stripe/stripe-js": "1.21.1",
|
"@stripe/stripe-js": "1.21.1",
|
||||||
"@tailwindcss/forms": "0.3.4",
|
|
||||||
"autoprefixer": "10.4.0",
|
"autoprefixer": "10.4.0",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"copy-text-to-clipboard": "^3.0.1",
|
"copy-text-to-clipboard": "^3.0.1",
|
||||||
|
@ -21,7 +20,7 @@
|
||||||
"fast-levenshtein": "3.0.0",
|
"fast-levenshtein": "3.0.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"http-status-codes": "2.1.4",
|
"http-status-codes": "2.1.4",
|
||||||
"ky": "0.25.1",
|
"ky": "0.28.7",
|
||||||
"next": "12.0.3",
|
"next": "12.0.3",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"postcss": "8.3.11",
|
"postcss": "8.3.11",
|
||||||
|
@ -31,13 +30,16 @@
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-toastify": "8.1.0",
|
"react-toastify": "8.1.0",
|
||||||
"skynet-js": "3.0.2",
|
"skynet-js": "3.0.2",
|
||||||
"stripe": "8.186.1",
|
"stripe": "8.188.0",
|
||||||
"superagent": "6.1.0",
|
"superagent": "6.1.0",
|
||||||
"swr": "1.0.1",
|
"swr": "1.0.1",
|
||||||
"tailwindcss": "2.2.19",
|
|
||||||
"yup": "0.32.11"
|
"yup": "0.32.11"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"devDependencies": {
|
||||||
"axios": "0.21.4"
|
"@tailwindcss/forms": "0.3.4",
|
||||||
|
"@tailwindcss/typography": "0.4.1",
|
||||||
|
"eslint": "<8.0.0",
|
||||||
|
"eslint-config-next": "12.0.3",
|
||||||
|
"tailwindcss": "2.2.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,44 @@
|
||||||
|
import * as React from "react";
|
||||||
import { useFormik, getIn, setIn } from "formik";
|
import { useFormik, getIn, setIn } from "formik";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import SelfServiceMessages from "./SelfServiceMessages";
|
import SelfServiceMessages from "./SelfServiceMessages";
|
||||||
|
|
||||||
export default function SelfServiceForm({ flow, config, fieldsConfig, title, button = "Submit" }) {
|
export default function SelfServiceForm({ fieldsConfig, onSubmit, title, validationSchema = null, button = "Submit" }) {
|
||||||
const fields = config.fields
|
const [messages, setMessages] = React.useState([]);
|
||||||
.filter((field) => !field.name.startsWith("traits.name")) // drop name fields
|
const fields = fieldsConfig.sort((a, b) => (a.position < b.position ? -1 : 1));
|
||||||
.map((field) => ({ ...field, ...fieldsConfig[field.name] }))
|
|
||||||
.sort((a, b) => (a.position < b.position ? -1 : 1));
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: fields.reduce((acc, field) => setIn(acc, field.name, field.value ?? ""), {}),
|
initialValues: fields.reduce((acc, field) => setIn(acc, field.name, field.value ?? ""), {}),
|
||||||
|
validationSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
if (!formik.isValid) return;
|
||||||
|
|
||||||
|
setMessages([]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onSubmit(values);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
const data = await error.response.json();
|
||||||
|
|
||||||
|
if (data.message) {
|
||||||
|
setMessages((messages) => [...messages, { type: "error", text: data.message }]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setMessages((messages) => [...messages, { type: "error", text: error.toString() }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
<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>}
|
{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">
|
<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}>
|
<form className="space-y-6" onSubmit={formik.handleSubmit}>
|
||||||
{fields.map((field) => (
|
{fields.map((field) => (
|
||||||
<div key={field.name} className={classnames({ hidden: field.type === "hidden" })}>
|
<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">
|
<label htmlFor={field.name} className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
{fieldsConfig[field.name]?.label ?? field.name}
|
{field.label ?? field.name}
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
|
@ -27,61 +46,23 @@ export default function SelfServiceForm({ flow, config, fieldsConfig, title, but
|
||||||
name={field.name}
|
name={field.name}
|
||||||
type={field.type}
|
type={field.type}
|
||||||
autoComplete={field.autoComplete}
|
autoComplete={field.autoComplete}
|
||||||
required={field.required}
|
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
value={getIn(formik.values, field.name) ?? ""}
|
value={getIn(formik.values, field.name) ?? ""}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"appearance-none block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none sm:text-sm",
|
"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(
|
"border-gray-300 placeholder-gray-400 focus:ring-green-500 focus:border-green-500": !(
|
||||||
field?.messages?.length
|
formik.errors[field.name] && formik.touched[field.name]
|
||||||
),
|
),
|
||||||
"border-red-300 text-red-900 placeholder-red-300 focus:ring-red-500 focus:border-red-500":
|
"border-red-300 text-red-900 placeholder-red-300 focus:ring-red-500 focus:border-red-500":
|
||||||
Boolean(field?.messages?.length),
|
formik.errors[field.name] && formik.touched[field.name],
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelfServiceMessages messages={field.messages} />
|
{formik.errors[field.name] && formik.touched[field.name] && (
|
||||||
|
<p className="mt-2 text-xs text-red-600">{formik.errors[field.name]}</p>
|
||||||
{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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,9 +75,7 @@ export default function SelfServiceForm({ flow, config, fieldsConfig, title, but
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<SelfServiceMessages messages={config.messages} />
|
{messages.length > 0 && <SelfServiceMessages messages={messages} />}
|
||||||
|
|
||||||
{flow && <SelfServiceMessages messages={flow.messages} />}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,8 +32,9 @@ import classnames from "classnames";
|
||||||
export default function SelfServiceMessages({ messages = [] }) {
|
export default function SelfServiceMessages({ messages = [] }) {
|
||||||
if (!messages) return null; // make sure we don't throw on invalid data
|
if (!messages) return null; // make sure we don't throw on invalid data
|
||||||
|
|
||||||
return messages.map(({ text, type }) => (
|
return messages.map(({ text, type }, index) => (
|
||||||
<p
|
<p
|
||||||
|
key={index}
|
||||||
className={classnames("mt-2 text-sm", {
|
className={classnames("mt-2 text-sm", {
|
||||||
"text-red-600": type === "error",
|
"text-red-600": type === "error",
|
||||||
"text-blue-600": type === "info",
|
"text-blue-600": type === "info",
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import ky from "ky/umd";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import config from "../../src/config";
|
import accountsApi from "../services/accountsApi";
|
||||||
|
|
||||||
export default function Layout({ title, children }) {
|
export default function Layout({ title, children }) {
|
||||||
const [menuOpen, openMenu] = useState(false);
|
const [menuOpen, openMenu] = useState(false);
|
||||||
|
@ -13,9 +12,8 @@ export default function Layout({ title, children }) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ky.post("/logout");
|
await accountsApi.post("logout");
|
||||||
|
router.push("/auth/login");
|
||||||
window.location = `${config.kratos.browser}/self-service/browser/flows/logout`;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error); // todo: handle errors with a message
|
console.log(error); // todo: handle errors with a message
|
||||||
}
|
}
|
||||||
|
@ -35,23 +33,15 @@ export default function Layout({ title, children }) {
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="flex-shrink-0">
|
<a className="flex-shrink-0">
|
||||||
<svg
|
<svg
|
||||||
viewBox="19.88800048828125 37.1175193787 132.07760620117188 132.07760620117188"
|
role="img"
|
||||||
width={33}
|
width="33"
|
||||||
height={33}
|
height="33"
|
||||||
|
fill="#00C65E"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<title>Skynet</title>
|
||||||
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"
|
<path d="m-.0004 6.4602 21.3893 11.297c.561.2935.6633 1.0532.1999 1.4846h-.011a10.0399 10.0399 0 0 1-2.2335 1.5307c-6.912 3.4734-14.9917-1.838-14.5438-9.5605l2.8601 1.9752c.856 4.508 5.6187 7.1094 9.8742 5.3932zm8.6477 3.1509 14.3661 5.6785a.8704.8704 0 0 1 .5197 1.0466v.0182c-.1537.5377-.7668.7938-1.2575.5252zm5.2896-7.4375c2.7093-.2325 6.0946.7869 8.1116 3.3871 1.699 2.1951 2.0497 4.8772 1.9298 7.6465v-.007c-.0478.5874-.6494.9616-1.1975.745l-9.7652-3.8596 9.0656 2.4313a7.296 7.296 0 0 0-1.0677-4.5631c-2.9683-4.7678-9.9847-4.5344-12.6297.4201a7.5048 7.5048 0 0 0-.398.8831L5.5546 7.9614c.069-.1017.1417-.198.2144-.2962.1163-.2416.2417-.487.3798-.7268 1.6118-2.7911 4.3102-4.4338 7.1558-4.6973.2108-.0182.4215-.049.6323-.0672z" />
|
||||||
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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -152,7 +142,7 @@ export default function Layout({ title, children }) {
|
||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
aria-labelledby="user-menu"
|
aria-labelledby="user-menu"
|
||||||
>
|
>
|
||||||
<Link href="/settings">
|
<Link href="/user/settings">
|
||||||
<a className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
|
<a className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">
|
||||||
Settings
|
Settings
|
||||||
</a>
|
</a>
|
||||||
|
@ -281,7 +271,7 @@ export default function Layout({ title, children }) {
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className="mt-3 px-2 space-y-1">
|
<div className="mt-3 px-2 space-y-1">
|
||||||
<Link href="/settings">
|
<Link href="/user/settings">
|
||||||
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">
|
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700">
|
||||||
Settings
|
Settings
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default function Table({ items, count, headers, mutate, actions, offset,
|
||||||
if (offset < 0) setOffset(0);
|
if (offset < 0) setOffset(0);
|
||||||
else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize);
|
else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize);
|
||||||
else if (offset % pageSize) setOffset(offset - (offset % pageSize));
|
else if (offset % pageSize) setOffset(offset - (offset % pageSize));
|
||||||
}, [offset, pageSize, setOffset]);
|
}, [count, offset, pageSize, setOffset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
@ -104,7 +104,7 @@ export default function Table({ items, count, headers, mutate, actions, offset,
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr className="bg-white">
|
<tr className="bg-white">
|
||||||
<td colspan={headers.length + actions.length} className="text-center py-6 text-sm text-gray-500">
|
<td colSpan={headers.length + actions.length} className="text-center py-6 text-sm text-gray-500">
|
||||||
no entries
|
no entries
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,13 +1,4 @@
|
||||||
export default {
|
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: {
|
tiers: {
|
||||||
starter: { id: "starter", tier: 1, name: "Free", description: "Pin up to 100GB" },
|
starter: { id: "starter", tier: 1, name: "Free", description: "Pin up to 100GB" },
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,8 +4,13 @@ import { ToastContainer } from "react-toastify";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import "normalize.css";
|
import "normalize.css";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import "tailwindcss/tailwind.css";
|
import "../../styles/globals.css";
|
||||||
import "@fontsource/metropolis/all.css";
|
import "@fontsource/sora/300.css"; // light
|
||||||
|
import "@fontsource/sora/400.css"; // normal
|
||||||
|
import "@fontsource/sora/500.css"; // medium
|
||||||
|
import "@fontsource/sora/600.css"; // semibold
|
||||||
|
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||||
|
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||||
|
|
||||||
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
||||||
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
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) {
|
|
||||||
console.log(`Cookie is present but authentication failed: ${error.message}`);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,4 +1,4 @@
|
||||||
import ky from "ky/umd";
|
import ky from "ky";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
|
||||||
|
@ -11,10 +11,10 @@ const getStripeCustomer = (stripeCustomerId = null) => {
|
||||||
return stripe.customers.create();
|
return stripe.customers.create();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async function billingApi(req, res) {
|
||||||
try {
|
try {
|
||||||
const authorization = req.headers.authorization; // authorization header from request
|
const cookie = req.headers.cookie; // cookie header from request
|
||||||
const { stripeCustomerId } = await ky("http://accounts:3000/user", { headers: { authorization } }).json();
|
const { stripeCustomerId } = await ky("http://accounts:3000/user", { headers: { cookie } }).json();
|
||||||
const customer = await getStripeCustomer(stripeCustomerId);
|
const customer = await getStripeCustomer(stripeCustomerId);
|
||||||
const session = await stripe.billingPortal.sessions.create({
|
const session = await stripe.billingPortal.sessions.create({
|
||||||
customer: customer.id,
|
customer: customer.id,
|
||||||
|
@ -25,4 +25,4 @@ export default async (req, res) => {
|
||||||
} catch ({ message }) {
|
} catch ({ message }) {
|
||||||
res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
|
res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import ky from "ky/umd";
|
import ky from "ky";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
import { isPaidTier } from "../../../services/tiers";
|
import { isPaidTier } from "../../../services/tiers";
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
||||||
|
|
||||||
const getStripeCustomer = async (user, authorization) => {
|
const getStripeCustomer = async (user, cookie) => {
|
||||||
if (user.stripeCustomerId) {
|
if (user.stripeCustomerId) {
|
||||||
return stripe.customers.retrieve(user.stripeCustomerId);
|
return stripe.customers.retrieve(user.stripeCustomerId);
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,12 @@ const getStripeCustomer = async (user, authorization) => {
|
||||||
const customer = await stripe.customers.create();
|
const customer = await stripe.customers.create();
|
||||||
|
|
||||||
// update user instance and include the customer id once created
|
// update user instance and include the customer id once created
|
||||||
await ky.put(`http://accounts:3000/user`, { headers: { authorization }, json: { stripeCustomerId: customer.id } });
|
await ky.put("http://accounts:3000/user", { headers: { cookie }, json: { stripeCustomerId: customer.id } });
|
||||||
|
|
||||||
return customer;
|
return customer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async function checkoutApi(req, res) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
return res.status(StatusCodes.NOT_FOUND).end();
|
return res.status(StatusCodes.NOT_FOUND).end();
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,8 @@ export default async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authorization = req.headers.authorization; // authorization header from request
|
const cookie = req.headers.cookie; // cookie header from request
|
||||||
const user = await ky("http://accounts:3000/user", { headers: { authorization } }).json();
|
const user = await ky("http://accounts:3000/user", { headers: { cookie } }).json();
|
||||||
|
|
||||||
if (isPaidTier(user.tier)) {
|
if (isPaidTier(user.tier)) {
|
||||||
const message = `Customer can have only one active subscription at a time, use Stripe Customer Portal to manage active subscription`;
|
const message = `Customer can have only one active subscription at a time, use Stripe Customer Portal to manage active subscription`;
|
||||||
|
@ -39,7 +39,7 @@ export default async (req, res) => {
|
||||||
return res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
|
return res.status(StatusCodes.BAD_REQUEST).json({ error: { message } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const customer = await getStripeCustomer(user, authorization);
|
const customer = await getStripeCustomer(user, cookie);
|
||||||
const session = await stripe.checkout.sessions.create({
|
const session = await stripe.checkout.sessions.create({
|
||||||
mode: "subscription",
|
mode: "subscription",
|
||||||
payment_method_types: ["card"],
|
payment_method_types: ["card"],
|
||||||
|
@ -55,4 +55,4 @@ export default async (req, res) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(StatusCodes.BAD_REQUEST).json({ error: { message: error.message } });
|
res.status(StatusCodes.BAD_REQUEST).json({ error: { message: error.message } });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
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 } });
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,37 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
import user from "./user.json";
|
|
||||||
|
|
||||||
export default (req, res) => {
|
|
||||||
res.json(user);
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
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 });
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,5 +0,0 @@
|
||||||
import stats from "./stats.json";
|
|
||||||
|
|
||||||
export default (req, res) => {
|
|
||||||
res.json(stats);
|
|
||||||
};
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"storageUsed": 809500672,
|
|
||||||
"numRegReads": 0,
|
|
||||||
"numRegWrites": 0,
|
|
||||||
"numUploads": 13,
|
|
||||||
"numDownloads": 78,
|
|
||||||
"totalUploadsSize": 618649028,
|
|
||||||
"totalDownloadsSize": 32307956843,
|
|
||||||
"bwUploads": 2810183680,
|
|
||||||
"bwDownloads": 32323934976,
|
|
||||||
"bwRegReads": 0,
|
|
||||||
"bwRegWrites": 0
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
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 });
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,65 +1,48 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Configuration, PublicApi } from "@ory/kratos-client";
|
import { useRouter } from "next/router";
|
||||||
import config from "../../config";
|
import * as Yup from "yup";
|
||||||
|
import accountsApi from "../../services/accountsApi";
|
||||||
|
import useAnonRoute from "../../services/useAnonRoute";
|
||||||
import SelfServiceForm from "../../components/Form/SelfServiceForm";
|
import SelfServiceForm from "../../components/Form/SelfServiceForm";
|
||||||
|
|
||||||
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
|
const fieldsConfig = [
|
||||||
|
{
|
||||||
export async function getServerSideProps(context) {
|
name: "email",
|
||||||
const flow = context.query.flow;
|
type: "text",
|
||||||
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) {
|
|
||||||
console.log(`Unexpected error retrieving login flow: ${error.message}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: `${config.kratos.browser}/self-service/login/browser?return_to=${redirect}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldsConfig = {
|
|
||||||
identifier: {
|
|
||||||
label: "Email address",
|
label: "Email address",
|
||||||
autoComplete: "email",
|
autoComplete: "email",
|
||||||
position: 0,
|
position: 0,
|
||||||
},
|
},
|
||||||
password: {
|
{
|
||||||
|
name: "password",
|
||||||
|
type: "password",
|
||||||
label: "Password",
|
label: "Password",
|
||||||
autoComplete: "current-password",
|
autoComplete: "current-password",
|
||||||
position: 1,
|
position: 1,
|
||||||
},
|
},
|
||||||
csrf_token: {
|
];
|
||||||
position: 99,
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
email: Yup.string().required("Email is required").email("This email is invalid"),
|
||||||
|
password: Yup.string().required("Password is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
useAnonRoute(); // ensure user is not logged in
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onSubmit = async (values) => {
|
||||||
|
await accountsApi.post("login", {
|
||||||
|
json: {
|
||||||
|
email: values.email,
|
||||||
|
password: values.password,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Login({ flow }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<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">
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
@ -82,14 +65,19 @@ export default function Login({ flow }) {
|
||||||
<Link href="/auth/registration">
|
<Link href="/auth/registration">
|
||||||
<a className="font-medium text-green-600 hover:text-green-500">sign up</a>
|
<a className="font-medium text-green-600 hover:text-green-500">sign up</a>
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
if you don't have one yet
|
if you don't have one yet
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelfServiceForm flow={flow} config={flow.methods.password.config} fieldsConfig={fieldsConfig} button="Sign in" />
|
<SelfServiceForm
|
||||||
|
fieldsConfig={fieldsConfig}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
button="Sign in"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-md text-center mt-2">
|
<div className="sm:mx-auto sm:w-full sm:max-w-md text-center mt-2">
|
||||||
<Link href="/recovery">
|
<Link href="/auth/recovery">
|
||||||
<a className="text-sm font-medium text-green-600 hover:text-green-500">Forgot your password?</a>
|
<a className="text-sm font-medium text-green-600 hover:text-green-500">Forgot your password?</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,57 +1,39 @@
|
||||||
|
import * as React from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Configuration, PublicApi } from "@ory/kratos-client";
|
import * as Yup from "yup";
|
||||||
import config from "../config";
|
import accountsApi from "../../services/accountsApi";
|
||||||
import SelfServiceForm from "../components/Form/SelfServiceForm";
|
import useAnonRoute from "../../services/useAnonRoute";
|
||||||
|
import SelfServiceForm from "../../components/Form/SelfServiceForm";
|
||||||
|
|
||||||
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
|
const fieldsConfig = [
|
||||||
|
{
|
||||||
export async function getServerSideProps(context) {
|
name: "email",
|
||||||
const flow = context.query.flow;
|
type: "text",
|
||||||
|
label: "Email address",
|
||||||
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",
|
autoComplete: "email",
|
||||||
position: 0,
|
position: 0,
|
||||||
},
|
},
|
||||||
csrf_token: {
|
];
|
||||||
position: 99,
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
email: Yup.string().required("Email is required").email("This email is invalid"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Recovery() {
|
||||||
|
useAnonRoute(); // ensure user is not logged in
|
||||||
|
|
||||||
|
const [success, setSuccess] = React.useState(false);
|
||||||
|
|
||||||
|
const onSubmit = async (values) => {
|
||||||
|
await accountsApi.post("user/recover/request", {
|
||||||
|
json: {
|
||||||
|
email: values.email,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setSuccess(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Recovery({ flow }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<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">
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
@ -84,12 +66,20 @@ export default function Recovery({ flow }) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!success && (
|
||||||
<SelfServiceForm
|
<SelfServiceForm
|
||||||
flow={flow}
|
|
||||||
config={flow.methods.link.config}
|
|
||||||
fieldsConfig={fieldsConfig}
|
fieldsConfig={fieldsConfig}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={onSubmit}
|
||||||
button="Send recovery link"
|
button="Send recovery link"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<p className="mt-4 text-center text-primary">
|
||||||
|
Account recovery requested, please follow instructions sent in email.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,88 +1,66 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Configuration, PublicApi } from "@ory/kratos-client";
|
import { useRouter } from "next/router";
|
||||||
import { getIn } from "formik";
|
|
||||||
import config from "../../config";
|
|
||||||
import levenshtein from "fast-levenshtein";
|
import levenshtein from "fast-levenshtein";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import accountsApi from "../../services/accountsApi";
|
||||||
|
import useAnonRoute from "../../services/useAnonRoute";
|
||||||
import lcs from "../../services/longestCommonSequence";
|
import lcs from "../../services/longestCommonSequence";
|
||||||
import SelfServiceForm from "../../components/Form/SelfServiceForm";
|
import SelfServiceForm from "../../components/Form/SelfServiceForm";
|
||||||
|
|
||||||
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
|
const fieldsConfig = [
|
||||||
|
{
|
||||||
export async function getServerSideProps(context) {
|
name: "email",
|
||||||
const flow = context.query.flow;
|
type: "text",
|
||||||
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) {
|
|
||||||
console.log(`Unexpected error retrieving registration flow: ${error.message}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: `${config.kratos.browser}/self-service/registration/browser?return_to=${redirect}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldsConfig = {
|
|
||||||
"traits.email": {
|
|
||||||
label: "Email address",
|
label: "Email address",
|
||||||
autoComplete: "email",
|
autoComplete: "email",
|
||||||
position: 0,
|
position: 0,
|
||||||
},
|
},
|
||||||
password: {
|
{
|
||||||
|
name: "password",
|
||||||
|
type: "password",
|
||||||
label: "Password",
|
label: "Password",
|
||||||
autoComplete: "new-password",
|
autoComplete: "new-password",
|
||||||
position: 1,
|
position: 1,
|
||||||
checks: [
|
},
|
||||||
{
|
{
|
||||||
label: "At least 6 characters long",
|
name: "confirmPassword",
|
||||||
validate: (values, field) => {
|
type: "password",
|
||||||
const value = getIn(values, field);
|
label: "Password repeated",
|
||||||
|
autoComplete: "new-password",
|
||||||
|
position: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return value && value.length > 5;
|
const validationSchema = Yup.object().shape({
|
||||||
},
|
email: Yup.string().required("Email is required").email("This email is invalid"),
|
||||||
},
|
password: Yup.string()
|
||||||
{
|
.required("Password is required")
|
||||||
label: "Significantly different from the email",
|
.min(6, "Password has to be at least 6 characters long")
|
||||||
validate: (values, field) => {
|
.test("levenshtein", "This password is too similar to your email", function (value) {
|
||||||
const value = getIn(values, field);
|
const email = this.parent.email;
|
||||||
const email = getIn(values, "traits.email");
|
|
||||||
|
|
||||||
// levenshtein distance higher than 5 and longest common sequence shorter than half of the password
|
// 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;
|
return value && email && levenshtein.get(value, email) > 5 && lcs(value, email).length / value.length <= 0.5;
|
||||||
|
}),
|
||||||
|
confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Registration() {
|
||||||
|
useAnonRoute(); // ensure user is not logged in
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onSubmit = async (values) => {
|
||||||
|
await accountsApi.post("user", {
|
||||||
|
json: {
|
||||||
|
email: values.email,
|
||||||
|
password: values.password,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
],
|
|
||||||
},
|
router.push("/");
|
||||||
csrf_token: {
|
|
||||||
position: 99,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Registration({ flow }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<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">
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
@ -109,7 +87,12 @@ export default function Registration({ flow }) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelfServiceForm flow={flow} config={flow.methods.password.config} fieldsConfig={fieldsConfig} button="Sign up" />
|
<SelfServiceForm
|
||||||
|
fieldsConfig={fieldsConfig}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
button="Sign up"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,10 @@ import prettyBytes from "pretty-bytes";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Layout from "../components/Layout";
|
import Layout from "../components/Layout";
|
||||||
import Table from "../components/Table";
|
import Table from "../components/Table";
|
||||||
import authServerSideProps from "../services/authServerSideProps";
|
|
||||||
import { SkynetClient } from "skynet-js";
|
import { SkynetClient } from "skynet-js";
|
||||||
import useAccountsApi from "../services/useAccountsApi";
|
import useAccountsApi from "../services/useAccountsApi";
|
||||||
|
|
||||||
const skynetClient = new SkynetClient(process.env.NEXT_PUBLIC_SKYNET_PORTAL_API);
|
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 getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
|
||||||
const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss");
|
const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const headers = [
|
const headers = [
|
||||||
|
@ -36,22 +34,15 @@ const headers = [
|
||||||
];
|
];
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
export const getServerSideProps = authServerSideProps(async (context, api) => {
|
export default function Downloads() {
|
||||||
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 [offset, setOffset] = useState(0);
|
||||||
const { data } = useAccountsApi(`${apiPrefix}/user/downloads?pageSize=10&offset=${offset}`, {
|
const { data } = useAccountsApi(`user/downloads?pageSize=10&offset=${offset}`, {
|
||||||
initialData: offset === 0 ? initialData : undefined,
|
|
||||||
revalidateOnMount: true,
|
revalidateOnMount: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// preload next page if it exists (based on the response from the current page query)
|
// 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;
|
const nextPageOffset = data && data.offset + data.pageSize < data.count ? data.offset + data.pageSize : offset;
|
||||||
useAccountsApi(`${apiPrefix}/user/downloads?pageSize=10&offset=${nextPageOffset}`);
|
useAccountsApi(`user/downloads?pageSize=10&offset=${nextPageOffset}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout title="Your downloads">
|
<Layout title="Your downloads">
|
||||||
|
|
|
@ -4,23 +4,14 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Layout from "../components/Layout";
|
import Layout from "../components/Layout";
|
||||||
import authServerSideProps from "../services/authServerSideProps";
|
|
||||||
import { SkynetClient } from "skynet-js";
|
import { SkynetClient } from "skynet-js";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import useAccountsApi from "../services/useAccountsApi";
|
import useAccountsApi from "../services/useAccountsApi";
|
||||||
import { isFreeTier } from "../services/tiers";
|
import React from "react";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const skynetClient = new SkynetClient(process.env.NEXT_PUBLIC_SKYNET_PORTAL_API);
|
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 }) {
|
function SkylinkList({ items = [], timestamp }) {
|
||||||
return (
|
return (
|
||||||
|
@ -99,13 +90,19 @@ function SkylinkList({ items = [], timestamp }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ plans }) {
|
export default function Home() {
|
||||||
const { data: user } = useAccountsApi(`${apiPrefix}/user`);
|
const { data: prices } = useAccountsApi("stripe/prices");
|
||||||
const { data: stats } = useAccountsApi(`${apiPrefix}/user/stats`);
|
const { data: user } = useAccountsApi("user");
|
||||||
const { data: downloads } = useAccountsApi(`${apiPrefix}/user/downloads?pageSize=3&offset=0`);
|
const { data: stats } = useAccountsApi("user/stats");
|
||||||
const { data: uploads } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=3&offset=0`);
|
const { data: downloads } = useAccountsApi("user/downloads?pageSize=3&offset=0");
|
||||||
|
const { data: uploads } = useAccountsApi("user/uploads?pageSize=3&offset=0");
|
||||||
|
const [plans, setPlans] = React.useState([config.tiers.starter]);
|
||||||
|
|
||||||
const activePlan = plans.find(({ tier }) => (user ? user.tier === tier : isFreeTier(tier)));
|
React.useEffect(() => {
|
||||||
|
if (prices) setPlans((plans) => [...plans, ...prices].sort((a, b) => a.tier - b.tier));
|
||||||
|
}, [setPlans, prices]);
|
||||||
|
|
||||||
|
const activePlan = plans.find(({ tier }) => user && user.tier === tier);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout title="Dashboard">
|
<Layout title="Dashboard">
|
||||||
|
@ -135,7 +132,7 @@ export default function Home({ plans }) {
|
||||||
<div className="ml-5 w-0 flex-1">
|
<div className="ml-5 w-0 flex-1">
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Current plan</dt>
|
<dt className="text-sm font-medium text-gray-500 truncate">Current plan</dt>
|
||||||
<dd className="flex items-baseline">
|
<dd className="flex items-baseline">
|
||||||
<div className="text-2xl font-semibold text-gray-900">{activePlan.name}</div>
|
<div className="text-2xl font-semibold text-gray-900">{activePlan?.name}</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import Layout from "../components/Layout";
|
import Layout from "../components/Layout";
|
||||||
import ky from "ky/umd";
|
import ky from "ky";
|
||||||
import { useEffect, useState } from "react";
|
import * as React from "react";
|
||||||
import authServerSideProps from "../services/authServerSideProps";
|
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import useAccountsApi from "../services/useAccountsApi";
|
import useAccountsApi from "../services/useAccountsApi";
|
||||||
import { isFreeTier, isPaidTier } from "../services/tiers";
|
import { isFreeTier, isPaidTier } from "../services/tiers";
|
||||||
|
|
||||||
const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
|
|
||||||
|
|
||||||
const ActiveBadge = () => {
|
const ActiveBadge = () => {
|
||||||
return (
|
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">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs bg-green-200 text-green-800 ml-3">
|
||||||
|
@ -19,22 +16,23 @@ const ActiveBadge = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps = authServerSideProps(async (context, api) => {
|
export default function Payments() {
|
||||||
const [user, stats, stripe] = await Promise.all([
|
const { data: user } = useAccountsApi("user");
|
||||||
api.get("user").json(),
|
const { data: stats } = useAccountsApi("user/stats");
|
||||||
api.get("user/stats").json(),
|
const { data: prices } = useAccountsApi("stripe/prices");
|
||||||
api.get("stripe/prices").json(),
|
const [plans, setPlans] = React.useState([config.tiers.starter]);
|
||||||
]);
|
const [selectedPlan, setSelectedPlan] = React.useState(null);
|
||||||
const plans = [config.tiers.starter, ...stripe].sort((a, b) => a.tier - b.tier);
|
|
||||||
|
|
||||||
return { props: { plans, user, stats } };
|
React.useEffect(() => {
|
||||||
});
|
if (prices) setPlans((plans) => [...plans, ...prices].sort((a, b) => a.tier - b.tier));
|
||||||
|
}, [setPlans, prices]);
|
||||||
|
|
||||||
|
const activePlan = plans.find(({ tier }) => user && user.tier === tier);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setSelectedPlan(activePlan);
|
||||||
|
}, [activePlan, setSelectedPlan]);
|
||||||
|
|
||||||
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 () => {
|
const handleSubscribe = async () => {
|
||||||
try {
|
try {
|
||||||
const price = selectedPlan.stripe;
|
const price = selectedPlan.stripe;
|
||||||
|
@ -46,12 +44,6 @@ export default function Payments({ plans, user: initialUserData, stats: initialS
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activePlan && isPaidTier(activePlan.tier)) {
|
|
||||||
setSelectedPlan(activePlan);
|
|
||||||
}
|
|
||||||
}, [activePlan, selectedPlan, setSelectedPlan]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout title="Payments">
|
<Layout title="Payments">
|
||||||
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
|
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default function Payments() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-6 pb-8 px-6">
|
<div className="pt-6 pb-8 px-6">
|
||||||
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
|
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
|
||||||
<ul className="mt-6 space-y-4">
|
<ul className="mt-6 space-y-4">
|
||||||
<li className="flex space-x-3">
|
<li className="flex space-x-3">
|
||||||
{/* Heroicon name: solid/check */}
|
{/* Heroicon name: solid/check */}
|
||||||
|
@ -96,7 +96,7 @@ export default function Payments() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-6 pb-8 px-6">
|
<div className="pt-6 pb-8 px-6">
|
||||||
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
|
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
|
||||||
<ul className="mt-6 space-y-4">
|
<ul className="mt-6 space-y-4">
|
||||||
<li className="flex space-x-3">
|
<li className="flex space-x-3">
|
||||||
{/* Heroicon name: solid/check */}
|
{/* Heroicon name: solid/check */}
|
||||||
|
@ -168,7 +168,7 @@ export default function Payments() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-6 pb-8 px-6">
|
<div className="pt-6 pb-8 px-6">
|
||||||
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
|
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
|
||||||
<ul className="mt-6 space-y-4">
|
<ul className="mt-6 space-y-4">
|
||||||
<li className="flex space-x-3">
|
<li className="flex space-x-3">
|
||||||
{/* Heroicon name: solid/check */}
|
{/* Heroicon name: solid/check */}
|
||||||
|
@ -257,7 +257,7 @@ export default function Payments() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-6 pb-8 px-6">
|
<div className="pt-6 pb-8 px-6">
|
||||||
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
|
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What's included</h3>
|
||||||
<ul className="mt-6 space-y-4">
|
<ul className="mt-6 space-y-4">
|
||||||
<li className="flex space-x-3">
|
<li className="flex space-x-3">
|
||||||
{/* Heroicon name: solid/check */}
|
{/* Heroicon name: solid/check */}
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
import { Configuration, PublicApi } from "@ory/kratos-client";
|
|
||||||
import Layout from "../components/Layout";
|
|
||||||
import config from "../config";
|
|
||||||
import SelfServiceForm from "../components/Form/SelfServiceForm";
|
|
||||||
import authServerSideProps from "../services/authServerSideProps";
|
|
||||||
|
|
||||||
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
|
|
||||||
|
|
||||||
export const getServerSideProps = authServerSideProps(async (context) => {
|
|
||||||
const flow = context.query.flow;
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
return { props: { flow: require("../../stubs/settings.json") } };
|
|
||||||
}
|
|
||||||
|
|
||||||
// The flow is used to identify the login and registration flow and
|
|
||||||
// return data like the csrf_token and so on.
|
|
||||||
if (!flow || typeof flow !== "string") {
|
|
||||||
// No flow ID found in URL, initializing settings flow.
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: `${config.kratos.browser}/self-service/settings/browser`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { status, data } = await kratos.getSelfServiceSettingsFlow(flow, {
|
|
||||||
headers: { cookie: context.req.headers.cookie },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (status === 200) return { props: { flow: data } };
|
|
||||||
|
|
||||||
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: `${config.kratos.browser}/self-service/settings/browser`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const fieldsConfig = {
|
|
||||||
"traits.email": {
|
|
||||||
label: "Email address",
|
|
||||||
autoComplete: "email",
|
|
||||||
position: 0,
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
label: "Password",
|
|
||||||
autoComplete: "new-password",
|
|
||||||
position: 1,
|
|
||||||
},
|
|
||||||
csrf_token: {
|
|
||||||
position: 99,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Settings({ flow }) {
|
|
||||||
const profileConfig = flow.methods.profile.config;
|
|
||||||
const passwordConfig = flow.methods.password.config;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout title="Settings">
|
|
||||||
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6 grid grid-cols-1 gap-5 sm:grid-cols-2">
|
|
||||||
<SelfServiceForm config={profileConfig} fieldsConfig={fieldsConfig} title="Account settings" button="Update" />
|
|
||||||
<SelfServiceForm
|
|
||||||
config={passwordConfig}
|
|
||||||
fieldsConfig={fieldsConfig}
|
|
||||||
title="Authentication settings"
|
|
||||||
button="Update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,16 +1,14 @@
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ky from "ky/umd";
|
import ky from "ky";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import Layout from "../components/Layout";
|
import Layout from "../components/Layout";
|
||||||
import Table from "../components/Table";
|
import Table from "../components/Table";
|
||||||
import authServerSideProps from "../services/authServerSideProps";
|
|
||||||
import { SkynetClient } from "skynet-js";
|
import { SkynetClient } from "skynet-js";
|
||||||
import useAccountsApi from "../services/useAccountsApi";
|
import useAccountsApi from "../services/useAccountsApi";
|
||||||
|
|
||||||
const skynetClient = new SkynetClient(process.env.NEXT_PUBLIC_SKYNET_PORTAL_API);
|
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 getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
|
||||||
const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss");
|
const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const headers = [
|
const headers = [
|
||||||
|
@ -40,7 +38,7 @@ const actions = [
|
||||||
{
|
{
|
||||||
name: "Unpin Skylink",
|
name: "Unpin Skylink",
|
||||||
action: async ({ skylink }, mutate) => {
|
action: async ({ skylink }, mutate) => {
|
||||||
await toast.promise(ky.delete(`/user/uploads/${skylink}`), {
|
await toast.promise(ky.delete(`/services/user/uploads/${skylink}`), {
|
||||||
pending: "Unpinning Skylink",
|
pending: "Unpinning Skylink",
|
||||||
success: "Skylink unpinned",
|
success: "Skylink unpinned",
|
||||||
error: (error) => error.message,
|
error: (error) => error.message,
|
||||||
|
@ -51,22 +49,15 @@ const actions = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getServerSideProps = authServerSideProps(async (context, api) => {
|
export default function Uploads() {
|
||||||
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 [offset, setOffset] = useState(0);
|
||||||
const { data, mutate } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${offset}`, {
|
const { data, mutate } = useAccountsApi(`user/uploads?pageSize=10&offset=${offset}`, {
|
||||||
initialData: offset === 0 ? initialData : undefined,
|
|
||||||
revalidateOnMount: true,
|
revalidateOnMount: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// preload next page if it exists (based on the response from the current page query)
|
// 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;
|
const nextPageOffset = data && data.offset + data.pageSize < data.count ? data.offset + data.pageSize : offset;
|
||||||
useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${nextPageOffset}`);
|
useAccountsApi(`user/uploads?pageSize=10&offset=${nextPageOffset}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout title="Your uploads">
|
<Layout title="Your uploads">
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import accountsApi from "../../services/accountsApi";
|
||||||
|
import useAnonRoute from "../../services/useAnonRoute";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Recover() {
|
||||||
|
useAnonRoute(); // ensure user is not logged in
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const [error, setError] = React.useState("");
|
||||||
|
const [confirmed, setConfirmed] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
async function confirm() {
|
||||||
|
try {
|
||||||
|
await accountsApi.get("user/confirm", { searchParams: { token: router.query.token } });
|
||||||
|
|
||||||
|
setConfirmed(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/");
|
||||||
|
}, 5000);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
const data = await error.response.json();
|
||||||
|
|
||||||
|
setError(data.message ?? error.toString());
|
||||||
|
} else {
|
||||||
|
setError(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (router.query.token) confirm();
|
||||||
|
}, [router, setConfirmed]);
|
||||||
|
|
||||||
|
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">
|
||||||
|
{!confirmed && !error && (
|
||||||
|
<h2 className="mt-6 text-center text-xl text-gray-900">Confirming your email, please wait.</h2>
|
||||||
|
)}
|
||||||
|
{confirmed && (
|
||||||
|
<h2 className="mt-6 text-center text-xl">
|
||||||
|
Email confirmed! You will be redirected to the{" "}
|
||||||
|
<Link href="/">
|
||||||
|
<a className="text-primary hover:text-primary-light">dashboard</a>
|
||||||
|
</Link>{" "}
|
||||||
|
in 3 seconds.
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{error && <h2 className="mt-6 text-center text-xl text-red-900">{error}</h2>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,31 +1,52 @@
|
||||||
import Link from "next/link";
|
import * as Yup from "yup";
|
||||||
import { Configuration, PublicApi } from "@ory/kratos-client";
|
import { useRouter } from "next/router";
|
||||||
import config from "../config";
|
import accountsApi from "../../services/accountsApi";
|
||||||
|
import useAnonRoute from "../../services/useAnonRoute";
|
||||||
|
import SelfServiceForm from "../../components/Form/SelfServiceForm";
|
||||||
|
|
||||||
const kratos = new PublicApi(new Configuration({ basePath: config.kratos.public }));
|
const fieldsConfig = [
|
||||||
|
{
|
||||||
|
name: "password",
|
||||||
|
type: "password",
|
||||||
|
label: "Password",
|
||||||
|
autoComplete: "new-password",
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "confirmPassword",
|
||||||
|
type: "password",
|
||||||
|
label: "Password repeated",
|
||||||
|
autoComplete: "new-password",
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token",
|
||||||
|
type: "hidden",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
const validationSchema = Yup.object().shape({
|
||||||
const error = context.query.error;
|
password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"),
|
||||||
|
confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"),
|
||||||
|
});
|
||||||
|
|
||||||
// No error was send, redirecting back to home.
|
export default function Recover() {
|
||||||
if (!error || typeof error !== "string") {
|
useAnonRoute(); // ensure user is not logged in
|
||||||
console.log("No error ID found in URL, redirecting to homepage.");
|
|
||||||
|
|
||||||
return { redirect: { permanent: false, destination: "/" } };
|
const router = useRouter();
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const onSubmit = async (values) => {
|
||||||
const { status, data } = await kratos.getSelfServiceError(error);
|
await accountsApi.post("user/recover", {
|
||||||
|
json: {
|
||||||
|
token: router.query.token,
|
||||||
|
password: values.password,
|
||||||
|
confirmPassword: values.confirmPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if ("errors" in data) return { props: { errors: data.errors } };
|
router.push("/");
|
||||||
|
};
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<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">
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
@ -42,27 +63,15 @@ export default function Error({ errors }) {
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">An error occurred</h2>
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Set new password</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>
|
||||||
|
|
||||||
|
<SelfServiceForm
|
||||||
|
fieldsConfig={fieldsConfig}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
button="Confirm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import Layout from "../../components/Layout";
|
||||||
|
import SelfServiceForm from "../../components/Form/SelfServiceForm";
|
||||||
|
import accountsApi from "../../services/accountsApi";
|
||||||
|
|
||||||
|
const profileConfig = [
|
||||||
|
{
|
||||||
|
name: "email",
|
||||||
|
type: "text",
|
||||||
|
label: "New email address",
|
||||||
|
autoComplete: "email",
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "confirmEmail",
|
||||||
|
type: "text",
|
||||||
|
label: "New email address repeated",
|
||||||
|
autoComplete: "email",
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const passwordConfig = [
|
||||||
|
{
|
||||||
|
name: "password",
|
||||||
|
type: "password",
|
||||||
|
label: "New password",
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "confirmPassword",
|
||||||
|
type: "password",
|
||||||
|
label: "New password repeated",
|
||||||
|
position: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const emailValidationSchema = Yup.object().shape({
|
||||||
|
email: Yup.string().required("Email is required").email("This email is invalid"),
|
||||||
|
confirmEmail: Yup.string().oneOf([Yup.ref("email"), null], "Emails must match"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordValidationSchema = Yup.object().shape({
|
||||||
|
password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"),
|
||||||
|
confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
const onEmailSubmit = async (values) => {
|
||||||
|
await accountsApi.put("user", {
|
||||||
|
json: {
|
||||||
|
email: values.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPasswordSubmit = async (values) => {
|
||||||
|
await accountsApi.put("user", {
|
||||||
|
json: {
|
||||||
|
password: values.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
fieldsConfig={profileConfig}
|
||||||
|
validationSchema={emailValidationSchema}
|
||||||
|
onSubmit={onEmailSubmit}
|
||||||
|
title="Account settings"
|
||||||
|
button="Update"
|
||||||
|
/>
|
||||||
|
<SelfServiceForm
|
||||||
|
fieldsConfig={passwordConfig}
|
||||||
|
validationSchema={passwordValidationSchema}
|
||||||
|
onSubmit={onPasswordSubmit}
|
||||||
|
title="Authentication settings"
|
||||||
|
button="Update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,103 +0,0 @@
|
||||||
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,5 @@
|
||||||
|
import ky from "ky";
|
||||||
|
|
||||||
|
const prefix = process.env.NEXT_PUBLIC_SKYNET_DASHBOARD_URL ?? "";
|
||||||
|
|
||||||
|
export default ky.create({ prefixUrl: `${prefix}/api` });
|
|
@ -1,37 +0,0 @@
|
||||||
import ky from "ky/umd";
|
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
|
||||||
|
|
||||||
export default function authServerSideProps(getServerSideProps) {
|
|
||||||
return function authenticate(context) {
|
|
||||||
const authCookies = ["ory_kratos_session", "skynet-jwt"];
|
|
||||||
|
|
||||||
if (isProduction && !authCookies.every((cookie) => context.req.cookies[cookie])) {
|
|
||||||
// it is higly unusual that some of the cookies would be set but other would not
|
|
||||||
if (authCookies.some((cookie) => context.req.cookies[cookie])) {
|
|
||||||
console.log(
|
|
||||||
"Unexpected auth cookies state!",
|
|
||||||
authCookies.map((cookie) => `[${cookie}] is ${context.req.cookies[cookie] ? "set" : "not set"}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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: {} };
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,15 +1,21 @@
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
|
||||||
const fetcher = (url) =>
|
const prefix = process.env.NEXT_PUBLIC_SKYNET_DASHBOARD_URL ?? "";
|
||||||
fetch(url).then((res) => {
|
|
||||||
|
const fetcher = (url, router) => {
|
||||||
|
return fetch(url).then((res) => {
|
||||||
if (res.status === StatusCodes.UNAUTHORIZED) {
|
if (res.status === StatusCodes.UNAUTHORIZED) {
|
||||||
window.location.href = `/auth/login?return_to=${encodeURIComponent(window.location.href)}`;
|
router.push(`/auth/login?return_to=${encodeURIComponent(window.location.href)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function useAccountsApi(key, config) {
|
export default function useAccountsApi(key, config) {
|
||||||
return useSWR(key, fetcher, config);
|
const router = useRouter();
|
||||||
|
|
||||||
|
return useSWR(`${prefix}/api/${key}`, (url) => fetcher(url, router), config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
|
||||||
|
const prefix = process.env.NEXT_PUBLIC_SKYNET_DASHBOARD_URL ?? "";
|
||||||
|
const fetcher = (url, router) => {
|
||||||
|
return fetch(url).then((res) => {
|
||||||
|
if (res.status === StatusCodes.OK) router.push("/");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useAnonRoute() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return useSWR(`${prefix}/api/user`, (url) => fetcher(url, router));
|
||||||
|
}
|
|
@ -1,45 +0,0 @@
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
{
|
|
||||||
"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'" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,3 @@
|
||||||
html,
|
@tailwind base;
|
||||||
body {
|
@tailwind components;
|
||||||
padding: 0;
|
@tailwind utilities;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,18 +1,65 @@
|
||||||
|
const defaultTheme = require("tailwindcss/defaultTheme");
|
||||||
|
const plugin = require("tailwindcss/plugin");
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
primary: { light: "#33D17E", DEFAULT: "#00c65e" },
|
||||||
|
warning: "#ffd567",
|
||||||
|
error: "#ED5454",
|
||||||
|
palette: {
|
||||||
|
100: "#f5f7f7",
|
||||||
|
200: "#d4dddb",
|
||||||
|
300: "#9e9e9e",
|
||||||
|
400: "#555555",
|
||||||
|
500: "#242424",
|
||||||
|
600: "#0d0d0d",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
purge: ["./src/**/*.js"],
|
purge: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
darkMode: false, // or 'media' or 'class'
|
darkMode: false, // or 'media' or 'class'
|
||||||
theme: {
|
theme: {
|
||||||
|
screens: {
|
||||||
|
sm: "640px",
|
||||||
|
tablet: "640px",
|
||||||
|
md: "768px",
|
||||||
|
lg: "1024px",
|
||||||
|
desktop: "1024px",
|
||||||
|
xl: "1280px",
|
||||||
|
hires: "1408px",
|
||||||
|
"2xl": "1536px",
|
||||||
|
},
|
||||||
|
backgroundColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||||
|
borderColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||||
|
textColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||||
|
placeholderColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Metropolis", "Helvetica", "Arial", "Sans-Serif"],
|
sans: ["Sora", ...defaultTheme.fontFamily.sans],
|
||||||
|
content: ["Source\\ Sans\\ Pro", ...defaultTheme.fontFamily.sans],
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
extend: {
|
|
||||||
backgroundColor: ["disabled"],
|
backgroundColor: ["disabled"],
|
||||||
textColor: ["disabled"],
|
textColor: ["disabled"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("@tailwindcss/forms")],
|
variants: {
|
||||||
|
extend: {
|
||||||
|
animation: ["hover"],
|
||||||
|
rotate: ["hover"],
|
||||||
|
backgroundColor: ["disabled"],
|
||||||
|
textColor: ["disabled"],
|
||||||
|
margin: ["first"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require("@tailwindcss/typography"),
|
||||||
|
require("@tailwindcss/forms"),
|
||||||
|
plugin(function ({ addBase, theme }) {
|
||||||
|
addBase({
|
||||||
|
body: {
|
||||||
|
color: theme("textColor.palette.600"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue