Merge branch 'master' into matt/sky-571-update-webportal-docker-compose
This commit is contained in:
commit
831565f4a2
|
@ -12,14 +12,6 @@ updates:
|
||||||
directory: "/packages/health-check"
|
directory: "/packages/health-check"
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: "/packages/website"
|
|
||||||
schedule:
|
|
||||||
interval: monthly
|
|
||||||
- package-ecosystem: docker
|
|
||||||
directory: "/docker/nginx"
|
|
||||||
schedule:
|
|
||||||
interval: monthly
|
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/docker/sia"
|
directory: "/docker/sia"
|
||||||
schedule:
|
schedule:
|
||||||
|
@ -36,7 +28,3 @@ updates:
|
||||||
directory: "/packages/health-check"
|
directory: "/packages/health-check"
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
- package-ecosystem: docker
|
|
||||||
directory: "/packages/website"
|
|
||||||
schedule:
|
|
||||||
interval: monthly
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
name: Build Storybook - packages/dashboard-v2
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- "packages/dashboard-v2/**"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "packages/dashboard-v2/**"
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: packages/dashboard-v2
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
- run: yarn install
|
|
||||||
- run: yarn build-storybook
|
|
||||||
- name: "Deploy to Skynet"
|
|
||||||
uses: skynetlabs/deploy-to-skynet-action@v2
|
|
||||||
with:
|
|
||||||
upload-dir: packages/dashboard-v2/storybook-build
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
|
@ -1,54 +0,0 @@
|
||||||
name: Deploy website to Skynet
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- "packages/website/**"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "packages/website/**"
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: packages/website
|
|
||||||
|
|
||||||
env:
|
|
||||||
PORTAL_DOMAIN: siasky.net
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
cache: yarn
|
|
||||||
cache-dependency-path: packages/website/yarn.lock
|
|
||||||
|
|
||||||
- run: yarn
|
|
||||||
- run: yarn build
|
|
||||||
|
|
||||||
- name: "Integration tests"
|
|
||||||
uses: cypress-io/github-action@v3
|
|
||||||
env:
|
|
||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
working-directory: packages/website
|
|
||||||
install: false
|
|
||||||
record: true
|
|
||||||
start: yarn develop
|
|
||||||
wait-on: http://localhost:8000
|
|
||||||
wait-on-timeout: 120
|
|
||||||
config: baseUrl=http://localhost:8000
|
|
||||||
|
|
||||||
- name: "Deploy to Skynet"
|
|
||||||
uses: skynetlabs/deploy-to-skynet-action@v2
|
|
||||||
with:
|
|
||||||
upload-dir: packages/website/public
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
registry-seed: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && secrets.WEBSITE_REGISTRY_SEED || '' }}
|
|
|
@ -13,14 +13,11 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
dockerfile:
|
dockerfile:
|
||||||
- docker/nginx/Dockerfile
|
|
||||||
- docker/nginx/testing/Dockerfile
|
|
||||||
- docker/sia/Dockerfile
|
- docker/sia/Dockerfile
|
||||||
- packages/dashboard/Dockerfile
|
- packages/dashboard/Dockerfile
|
||||||
- packages/dashboard-v2/Dockerfile
|
- packages/dashboard-v2/Dockerfile
|
||||||
- packages/handshake-api/Dockerfile
|
- packages/handshake-api/Dockerfile
|
||||||
- packages/health-check/Dockerfile
|
- packages/health-check/Dockerfile
|
||||||
- packages/website/Dockerfile
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: hadolint/hadolint-action@v2.0.0
|
- uses: hadolint/hadolint-action@v2.0.0
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
name: Lint - packages/dashboard-v2
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- packages/dashboard-v2/**
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: packages/dashboard-v2
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
|
|
||||||
- run: yarn
|
|
||||||
- run: yarn prettier --check
|
|
||||||
- run: yarn lint
|
|
|
@ -1,23 +0,0 @@
|
||||||
name: Lint - packages/handshake-api
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- packages/handshake-api/**
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: packages/handshake-api
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
|
|
||||||
- run: yarn
|
|
||||||
- run: yarn prettier --check .
|
|
|
@ -1,23 +0,0 @@
|
||||||
name: Lint - packages/website
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- packages/website/**
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: packages/website
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
|
|
||||||
- run: yarn
|
|
||||||
- run: yarn prettier --check .
|
|
|
@ -1,46 +0,0 @@
|
||||||
# Install and run unit tests with busted
|
|
||||||
# Docs: http://olivinelabs.com/busted/
|
|
||||||
|
|
||||||
name: Nginx Lua Unit Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- docker/nginx/libs/**
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- docker/nginx/libs/**
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: openresty/openresty:1.19.9.1-focal
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
luarocks install lua-resty-http
|
|
||||||
luarocks install hasher
|
|
||||||
luarocks install busted
|
|
||||||
luarocks install luacov
|
|
||||||
luarocks install luacheck
|
|
||||||
|
|
||||||
- name: Lint Code With Luacheck
|
|
||||||
run: luacheck docker/nginx/libs --std ngx_lua+busted
|
|
||||||
|
|
||||||
- name: Run Tests With Busted
|
|
||||||
# ran from root repo directory; produces luacov.stats.out file
|
|
||||||
run: docker/nginx/testing/rbusted --lpath='docker/nginx/libs/?.lua;docker/nginx/libs/?/?.lua' --verbose --coverage --pattern=spec docker/nginx/libs
|
|
||||||
|
|
||||||
- name: Generate Code Coverage Report With Luacov
|
|
||||||
# requires config file in cwd; produces luacov.report.out file
|
|
||||||
run: cp docker/nginx/testing/.luacov . && luacov && rm .luacov
|
|
||||||
|
|
||||||
- uses: codecov/codecov-action@v3
|
|
||||||
with:
|
|
||||||
root_dir: ${GITHUB_WORKSPACE}
|
|
||||||
files: ./luacov.report.out
|
|
||||||
flags: nginx-lua
|
|
|
@ -67,10 +67,6 @@ yarn-error.log
|
||||||
# Yarn Integrity file
|
# Yarn Integrity file
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
# Cypress
|
|
||||||
packages/website/cypress/screenshots
|
|
||||||
packages/website/cypress/videos
|
|
||||||
|
|
||||||
# Docker data
|
# Docker data
|
||||||
docker/data
|
docker/data
|
||||||
|
|
||||||
|
|
|
@ -82,8 +82,9 @@ services:
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# dashboard-v2:
|
# dashboard-v2:
|
||||||
# build:
|
# build:
|
||||||
# context: ./packages/dashboard-v2
|
# context: https://github.com/SkynetLabs/webportal-accounts-dashboard.git#main
|
||||||
# dockerfile: Dockerfile
|
# dockerfile: Dockerfile
|
||||||
|
# image: skynetlabs/webportal-accounts-dashboard@1.0.1
|
||||||
# container_name: dashboard-v2
|
# container_name: dashboard-v2
|
||||||
# restart: unless-stopped
|
# restart: unless-stopped
|
||||||
# logging: *default-logging
|
# logging: *default-logging
|
||||||
|
|
|
@ -57,9 +57,9 @@ services:
|
||||||
nginx:
|
nginx:
|
||||||
# uncomment "build" and comment out "image" to build from sources
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
# build:
|
# build:
|
||||||
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
# context: https://github.com/SkynetLabs/webportal-nginx.git#main
|
||||||
# dockerfile: ./docker/nginx/Dockerfile
|
# dockerfile: Dockerfile
|
||||||
image: skynetlabs/nginx
|
image: skynetlabs/webportal-nginx:0.1.1
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
@ -72,10 +72,6 @@ services:
|
||||||
- ./docker/data/nginx/skynet:/data/nginx/skynet:ro
|
- ./docker/data/nginx/skynet:/data/nginx/skynet:ro
|
||||||
- ./docker/data/sia/apipassword:/data/sia/apipassword:ro
|
- ./docker/data/sia/apipassword:/data/sia/apipassword:ro
|
||||||
- ./docker/data/certbot:/etc/letsencrypt
|
- ./docker/data/certbot:/etc/letsencrypt
|
||||||
- ./docker/nginx/libs:/etc/nginx/libs
|
|
||||||
- ./docker/nginx/conf.d:/etc/nginx/conf.d
|
|
||||||
- ./docker/nginx/conf.d.templates:/etc/nginx/templates
|
|
||||||
- ./docker/nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
|
|
||||||
networks:
|
networks:
|
||||||
shared:
|
shared:
|
||||||
ipv4_address: 10.10.10.30
|
ipv4_address: 10.10.10.30
|
||||||
|
@ -91,9 +87,9 @@ services:
|
||||||
website:
|
website:
|
||||||
# uncomment "build" and comment out "image" to build from sources
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
# build:
|
# build:
|
||||||
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
# context: https://github.com/SkynetLabs/webportal-website.git#main
|
||||||
# dockerfile: ./packages/website/Dockerfile
|
# dockerfile: Dockerfile
|
||||||
image: skynetlabs/website
|
image: skynetlabs/webportal-website:0.1.0
|
||||||
container_name: website
|
container_name: website
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
@ -132,9 +128,9 @@ services:
|
||||||
handshake-api:
|
handshake-api:
|
||||||
# uncomment "build" and comment out "image" to build from sources
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
# build:
|
# build:
|
||||||
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
# context: https://github.com/SkynetLabs/webportal-handshake-api.git#main
|
||||||
# dockerfile: ./packages/handshake-api/Dockerfile
|
# dockerfile: Dockerfile
|
||||||
image: skynetlabs/handshake-api
|
image: skynetlabs/webportal-handshake-api:0.1.1
|
||||||
container_name: handshake-api
|
container_name: handshake-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
FROM openresty/openresty:1.19.9.1-focal
|
|
||||||
|
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get --no-install-recommends -y install bc=1.07.1-2build1 && \
|
|
||||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
|
||||||
luarocks install lua-resty-http && \
|
|
||||||
luarocks install hasher
|
|
||||||
|
|
||||||
# reload nginx every 6 hours (for reloading certificates)
|
|
||||||
ENV NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS 6
|
|
||||||
|
|
||||||
# copy entrypoint and entrypoint scripts
|
|
||||||
COPY docker/nginx/docker-entrypoint.sh /
|
|
||||||
COPY docker/nginx/docker-entrypoint.d /docker-entrypoint.d
|
|
||||||
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
|
||||||
|
|
||||||
STOPSIGNAL SIGQUIT
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
|
@ -1,44 +0,0 @@
|
||||||
server {
|
|
||||||
server_name account.${PORTAL_DOMAIN}; # example: account.siasky.net
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.http;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name account.${PORTAL_DOMAIN}; # example: account.siasky.net
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
|
|
||||||
set_by_lua_block $skynet_server_domain {
|
|
||||||
-- fall back to portal domain if server domain is not defined
|
|
||||||
if "${SERVER_DOMAIN}" == "" then
|
|
||||||
return "${PORTAL_DOMAIN}"
|
|
||||||
end
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.account;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name account.${SERVER_DOMAIN}; # example: account.eu-ger-1.siasky.net
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.http;
|
|
||||||
|
|
||||||
set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name account.${SERVER_DOMAIN}; # example: account.eu-ger-1.siasky.net
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain {
|
|
||||||
-- when accessing portal directly through server domain, portal domain should be set to server domain
|
|
||||||
-- motivation: skynet-js uses Skynet-Portal-Api header (that is set to $skynet_portal_domain) to detect current
|
|
||||||
-- portal address and it should be server domain when accessing specific server by its domain address
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
set_by_lua_block $skynet_server_domain { return "${SERVER_DOMAIN}" }
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.account;
|
|
||||||
|
|
||||||
set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
server {
|
|
||||||
server_name ${PORTAL_DOMAIN}; # example: siasky.net
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.http;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name ${PORTAL_DOMAIN}; # example: siasky.net
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
|
|
||||||
set_by_lua_block $skynet_server_domain {
|
|
||||||
-- fall back to portal domain if server domain is not defined
|
|
||||||
if "${SERVER_DOMAIN}" == "" then
|
|
||||||
return "${PORTAL_DOMAIN}"
|
|
||||||
end
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.api;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name ${SERVER_DOMAIN}; # example: eu-ger-1.siasky.net
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.http;
|
|
||||||
|
|
||||||
set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name ${SERVER_DOMAIN}; # example: eu-ger-1.siasky.net
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain {
|
|
||||||
-- when accessing portal directly through server domain, portal domain should be set to server domain
|
|
||||||
-- motivation: skynet-js uses Skynet-Portal-Api header (that is set to $skynet_portal_domain) to detect current
|
|
||||||
-- portal address and it should be server domain when accessing specific server by its domain address
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
set_by_lua_block $skynet_server_domain { return "${SERVER_DOMAIN}" }
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.api;
|
|
||||||
|
|
||||||
set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
lua_shared_dict dnslink 10m;
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.dnslink;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 default_server;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/local-certificate.crt;
|
|
||||||
ssl_certificate_key /etc/ssl/local-certificate.key;
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
|
|
||||||
set_by_lua_block $skynet_server_domain {
|
|
||||||
-- fall back to portal domain if server domain is not defined
|
|
||||||
if "${SERVER_DOMAIN}" == "" then
|
|
||||||
return "${PORTAL_DOMAIN}"
|
|
||||||
end
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.dnslink;
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
server {
|
|
||||||
server_name *.hns.${PORTAL_DOMAIN}; # example: *.hns.siasky.net
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.http;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name *.hns.${PORTAL_DOMAIN}; # example: *.hns.siasky.net
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
|
|
||||||
set_by_lua_block $skynet_server_domain {
|
|
||||||
-- fall back to portal domain if server domain is not defined
|
|
||||||
if "${SERVER_DOMAIN}" == "" then
|
|
||||||
return "${PORTAL_DOMAIN}"
|
|
||||||
end
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_set_header Host ${PORTAL_DOMAIN};
|
|
||||||
include /etc/nginx/conf.d/server/server.hns;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name *.hns.${SERVER_DOMAIN}; # example: *.hns.eu-ger-1.siasky.net
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.http;
|
|
||||||
|
|
||||||
set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name *.hns.${SERVER_DOMAIN}; # example: *.hns.eu-ger-1.siasky.net
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain {
|
|
||||||
-- when accessing portal directly through server domain, portal domain should be set to server domain
|
|
||||||
-- motivation: skynet-js uses Skynet-Portal-Api header (that is set to $skynet_portal_domain) to detect current
|
|
||||||
-- portal address and it should be server domain when accessing specific server by its domain address
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
set_by_lua_block $skynet_server_domain { return "${SERVER_DOMAIN}" }
|
|
||||||
|
|
||||||
proxy_set_header Host ${SERVER_DOMAIN};
|
|
||||||
include /etc/nginx/conf.d/server/server.hns;
|
|
||||||
|
|
||||||
set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
server {
|
|
||||||
server_name *.${PORTAL_DOMAIN}; # example: *.siasky.net
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.http;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name *.${PORTAL_DOMAIN}; # example: *.siasky.net
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
|
|
||||||
set_by_lua_block $skynet_server_domain {
|
|
||||||
-- fall back to portal domain if server domain is not defined
|
|
||||||
if "${SERVER_DOMAIN}" == "" then
|
|
||||||
return "${PORTAL_DOMAIN}"
|
|
||||||
end
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.skylink;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name *.${SERVER_DOMAIN}; # example: *.eu-ger-1.siasky.net
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.http;
|
|
||||||
|
|
||||||
set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name *.${SERVER_DOMAIN}; # example: *.eu-ger-1.siasky.net
|
|
||||||
|
|
||||||
set_by_lua_block $skynet_portal_domain {
|
|
||||||
-- when accessing portal directly through server domain, portal domain should be set to server domain
|
|
||||||
-- motivation: skynet-js uses Skynet-Portal-Api header (that is set to $skynet_portal_domain) to detect current
|
|
||||||
-- portal address and it should be server domain when accessing specific server by its domain address
|
|
||||||
return "${SERVER_DOMAIN}"
|
|
||||||
}
|
|
||||||
set_by_lua_block $skynet_server_domain { return "${SERVER_DOMAIN}" }
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server/server.skylink;
|
|
||||||
|
|
||||||
set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
-----BEGIN DH PARAMETERS-----
|
|
||||||
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
|
|
||||||
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
|
|
||||||
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
|
|
||||||
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
|
|
||||||
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
|
|
||||||
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
|
|
||||||
-----END DH PARAMETERS-----
|
|
|
@ -1,18 +0,0 @@
|
||||||
# enables gzip compression
|
|
||||||
gzip on;
|
|
||||||
|
|
||||||
# set the gzip compression level (1-9)
|
|
||||||
gzip_comp_level 6;
|
|
||||||
|
|
||||||
# tells proxies to cache both gzipped and regular versions of a resource
|
|
||||||
gzip_vary on;
|
|
||||||
|
|
||||||
# informs NGINX to not compress anything smaller than the defined size
|
|
||||||
gzip_min_length 256;
|
|
||||||
|
|
||||||
# compress data even for clients that are connecting via proxies if a response header includes
|
|
||||||
# the "expired", "no-cache", "no-store", "private", and "Authorization" parameters
|
|
||||||
gzip_proxied expired no-cache no-store private auth;
|
|
||||||
|
|
||||||
# enables the types of files that can be compressed
|
|
||||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;
|
|
|
@ -1,9 +0,0 @@
|
||||||
if ($request_method = 'OPTIONS') {
|
|
||||||
include /etc/nginx/conf.d/include/cors-headers;
|
|
||||||
more_set_headers 'Access-Control-Max-Age: 1728000';
|
|
||||||
more_set_headers 'Content-Type: text/plain; charset=utf-8';
|
|
||||||
more_set_headers 'Content-Length: 0';
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/cors-headers;
|
|
|
@ -1,5 +0,0 @@
|
||||||
more_set_headers 'Access-Control-Allow-Origin: $http_origin';
|
|
||||||
more_set_headers 'Access-Control-Allow-Credentials: true';
|
|
||||||
more_set_headers 'Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE';
|
|
||||||
more_set_headers 'Access-Control-Allow-Headers: DNT,User-Agent,X-Requested-With,If-Modified-Since,If-None-Match,Cache-Control,Content-Type,Range,X-HTTP-Method-Override,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location,Skynet-API-Key';
|
|
||||||
more_set_headers 'Access-Control-Expose-Headers: Content-Length,Content-Range,ETag,Accept-Ranges,Skynet-File-Metadata,Skynet-Skylink,Skynet-Proof,Skynet-Portal-Api,Skynet-Server-Api,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location';
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Extract 2 sets of 2 characters from $request_id and assign to $dir1, $dir2
|
|
||||||
# respectfully. The rest of the $request_id is going to be assigned to $dir3.
|
|
||||||
# We use those variables to automatically generate a unique path for the uploaded file.
|
|
||||||
# This ensures that not all uploaded files end up in the same directory, which is something
|
|
||||||
# that causes performance issues in the renter.
|
|
||||||
# Example path result: /af/24/9bc5ec894920ccc45634dc9a8065
|
|
||||||
if ($request_id ~* "(\w{2})(\w{2})(\w+)") {
|
|
||||||
set $dir1 $1;
|
|
||||||
set $dir2 $2;
|
|
||||||
set $dir3 $3;
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
# optional variables initialisation - those variables are used in log_format
|
|
||||||
# but are not set on every route so we need to initialise them with empty value
|
|
||||||
# because otherwise logger with throw error
|
|
||||||
|
|
||||||
# set only on hns routes
|
|
||||||
set $hns_domain "";
|
|
||||||
|
|
||||||
# set only if server has been access through SERVER_DOMAIN
|
|
||||||
set $server_alias "";
|
|
||||||
|
|
||||||
# expose skylink variable so we can use it in access log
|
|
||||||
set $skylink "";
|
|
||||||
|
|
||||||
# cached account limits (json string) - applies only if accounts are enabled
|
|
||||||
set $account_limits "";
|
|
|
@ -1,3 +0,0 @@
|
||||||
allow 127.0.0.1/32; # localhost
|
|
||||||
allow 10.10.10.0/24; # docker network
|
|
||||||
deny all;
|
|
|
@ -1,94 +0,0 @@
|
||||||
include /etc/nginx/conf.d/include/proxy-pass-internal;
|
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
|
||||||
|
|
||||||
# variable definititions - we need to define a variable to be able to access it in lua by ngx.var.something
|
|
||||||
set $skylink ''; # placeholder for the raw 46 bit skylink
|
|
||||||
|
|
||||||
# resolve handshake domain by requesting to /hnsres endpoint and assign correct values to $skylink and $rest
|
|
||||||
rewrite_by_lua_block {
|
|
||||||
local json = require('cjson')
|
|
||||||
local httpc = require("resty.http").new()
|
|
||||||
|
|
||||||
-- make a get request to /hnsres endpoint with the domain name from request_uri
|
|
||||||
-- 10.10.10.50 points to handshake-api service (alias not available when using resty-http)
|
|
||||||
local hnsres_res, hnsres_err = httpc:request_uri("http://10.10.10.50:3100/hnsres/" .. ngx.var.hns_domain)
|
|
||||||
|
|
||||||
-- print error and exit with 500 or exit with response if status is not 200
|
|
||||||
if hnsres_err or (hnsres_res and hnsres_res.status ~= ngx.HTTP_OK) then
|
|
||||||
ngx.status = (hnsres_err and ngx.HTTP_INTERNAL_SERVER_ERROR) or hnsres_res.status
|
|
||||||
ngx.header["content-type"] = "text/plain"
|
|
||||||
ngx.say(hnsres_err or hnsres_res.body)
|
|
||||||
return ngx.exit(ngx.status)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- since /hnsres endpoint response is a json, we need to decode it before we access it
|
|
||||||
-- example response: '{"skylink":"sia://XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"}'
|
|
||||||
local hnsres_json = json.decode(hnsres_res.body)
|
|
||||||
|
|
||||||
-- define local variable containing rest of the skylink if provided
|
|
||||||
local skylink_rest
|
|
||||||
|
|
||||||
if hnsres_json.skylink then
|
|
||||||
-- try to match the skylink with sia:// prefix
|
|
||||||
skylink, skylink_rest = string.match(hnsres_json.skylink, "sia://([^/?]+)(.*)")
|
|
||||||
|
|
||||||
-- in case the skylink did not match, assume that there is no sia:// prefix and try to match again
|
|
||||||
if skylink == nil then
|
|
||||||
skylink, skylink_rest = string.match(hnsres_json.skylink, "/?([^/?]+)(.*)")
|
|
||||||
end
|
|
||||||
elseif hnsres_json.registry then
|
|
||||||
local publickey = hnsres_json.registry.publickey
|
|
||||||
local datakey = hnsres_json.registry.datakey
|
|
||||||
|
|
||||||
-- make a get request to /skynet/registry endpoint with the credentials from text record
|
|
||||||
-- 10.10.10.10 points to sia service (alias not available when using resty-http)
|
|
||||||
local registry_res, registry_err = httpc:request_uri("http://10.10.10.10:9980/skynet/registry?publickey=" .. publickey .. "&datakey=" .. datakey, {
|
|
||||||
headers = { ["User-Agent"] = "Sia-Agent" }
|
|
||||||
})
|
|
||||||
|
|
||||||
-- print error and exit with 500 or exit with response if status is not 200
|
|
||||||
if registry_err or (registry_res and registry_res.status ~= ngx.HTTP_OK) then
|
|
||||||
ngx.status = (registry_err and ngx.HTTP_INTERNAL_SERVER_ERROR) or registry_res.status
|
|
||||||
ngx.header["content-type"] = "text/plain"
|
|
||||||
ngx.say(registry_err or registry_res.body)
|
|
||||||
return ngx.exit(ngx.status)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- since /skynet/registry endpoint response is a json, we need to decode it before we access it
|
|
||||||
local registry_json = json.decode(registry_res.body)
|
|
||||||
-- response will contain a hex encoded skylink, we need to decode it
|
|
||||||
local data = (registry_json.data:gsub('..', function (cc)
|
|
||||||
return string.char(tonumber(cc, 16))
|
|
||||||
end))
|
|
||||||
|
|
||||||
skylink = data
|
|
||||||
end
|
|
||||||
|
|
||||||
-- fail with a generic 404 if skylink has not been extracted from a valid /hnsres response for some reason
|
|
||||||
if not skylink then
|
|
||||||
return ngx.exit(ngx.HTTP_NOT_FOUND)
|
|
||||||
end
|
|
||||||
|
|
||||||
ngx.var.skylink = skylink
|
|
||||||
if ngx.var.path == "/" and skylink_rest ~= nil and skylink_rest ~= "" and skylink_rest ~= "/" then
|
|
||||||
ngx.var.path = skylink_rest
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
# we proxy to another nginx location rather than directly to siad because we do not want to deal with caching here
|
|
||||||
proxy_pass https://127.0.0.1/$skylink$path$is_args$args;
|
|
||||||
|
|
||||||
# in case siad returns location header, we need to replace the skylink with the domain name
|
|
||||||
header_filter_by_lua_block {
|
|
||||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
|
||||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
|
||||||
|
|
||||||
if ngx.header.location then
|
|
||||||
-- match location redirect part after the skylink
|
|
||||||
local path = string.match(ngx.header.location, "[^/?]+(.*)");
|
|
||||||
|
|
||||||
-- because siad will set the location header to ie. XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg/index.html
|
|
||||||
-- we need to replace the skylink with the domain_name so we are not redirected to skylink
|
|
||||||
ngx.header.location = ngx.var.hns_domain .. path
|
|
||||||
end
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
|
||||||
|
|
||||||
# ensure that skylink that we pass around is base64 encoded (transform base32 encoded ones)
|
|
||||||
# this is important because we want only one format in cache keys and logs
|
|
||||||
set_by_lua_block $skylink { return require("skynet.skylink").parse(ngx.var.skylink) }
|
|
||||||
|
|
||||||
# default download rate to unlimited
|
|
||||||
set $limit_rate 0;
|
|
||||||
|
|
||||||
access_by_lua_block {
|
|
||||||
if require("skynet.account").accounts_enabled() then
|
|
||||||
-- check if portal is in authenticated only mode
|
|
||||||
if require("skynet.account").is_access_unauthorized() then
|
|
||||||
return require("skynet.account").exit_access_unauthorized()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check if portal is in subscription only mode
|
|
||||||
if require("skynet.account").is_access_forbidden() then
|
|
||||||
return require("skynet.account").exit_access_forbidden()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get account limits of currently authenticated user
|
|
||||||
local limits = require("skynet.account").get_account_limits()
|
|
||||||
|
|
||||||
-- apply download speed limit
|
|
||||||
ngx.var.limit_rate = limits.download
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
limit_rate_after 512k;
|
|
||||||
limit_rate $limit_rate;
|
|
||||||
|
|
||||||
proxy_read_timeout 600;
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
|
|
||||||
proxy_pass http://sia:9980/skynet/skylink/$skylink$path$is_args$args;
|
|
||||||
|
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
local skynet_modules = require("skynet.modules")
|
|
||||||
local skynet_scanner = require("skynet.scanner")
|
|
||||||
local skynet_tracker = require("skynet.tracker")
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("a") then
|
|
||||||
skynet_tracker.track_download(ngx.header["Skynet-Skylink"], ngx.status, skynet_account.get_auth_headers(), ngx.var.body_bytes_sent)
|
|
||||||
end
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("s") then
|
|
||||||
skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
|
|
||||||
end
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
include /etc/nginx/conf.d/include/sia-auth;
|
|
||||||
|
|
||||||
limit_req zone=registry_access_by_ip burst=600 nodelay;
|
|
||||||
limit_req zone=registry_access_by_ip_throttled burst=200 nodelay;
|
|
||||||
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_read_timeout 600; # siad should timeout with 404 after 5 minutes
|
|
||||||
proxy_pass http://sia:9980/skynet/registry;
|
|
||||||
|
|
||||||
access_by_lua_block {
|
|
||||||
if require("skynet.account").accounts_enabled() then
|
|
||||||
-- check if portal is in authenticated only mode
|
|
||||||
if require("skynet.account").is_access_unauthorized() then
|
|
||||||
return require("skynet.account").exit_access_unauthorized()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check if portal is in subscription only mode
|
|
||||||
if require("skynet.account").is_access_forbidden() then
|
|
||||||
return require("skynet.account").exit_access_forbidden()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get account limits of currently authenticated user
|
|
||||||
local limits = require("skynet.account").get_account_limits()
|
|
||||||
|
|
||||||
-- apply registry rate limits (forced delay)
|
|
||||||
if limits.registry > 0 then
|
|
||||||
ngx.sleep(limits.registry / 1000)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
local skynet_tracker = require("skynet.tracker")
|
|
||||||
|
|
||||||
skynet_tracker.track_registry(ngx.status, skynet_account.get_auth_headers(), ngx.req.get_method())
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
access_by_lua_block {
|
|
||||||
-- check portal access rules and exit if access is restricted
|
|
||||||
if require("skynet.account").is_access_unauthorized() then
|
|
||||||
return require("skynet.account").exit_access_unauthorized()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check if portal is in subscription only mode
|
|
||||||
if require("skynet.account").is_access_forbidden() then
|
|
||||||
return require("skynet.account").exit_access_forbidden()
|
|
||||||
end
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
# ----------------------------------------------------------------
|
|
||||||
# this file should be included on all locations that proxy_pass to
|
|
||||||
# another nginx location - internal nginx traffic
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
|
|
||||||
# increase the timeout on internal nginx proxy_pass locations to a
|
|
||||||
# value that is significantly higher than expected and let the end
|
|
||||||
# location handle correct timeout
|
|
||||||
proxy_read_timeout 30m;
|
|
||||||
proxy_send_timeout 30m;
|
|
|
@ -1,6 +0,0 @@
|
||||||
# Add a list of IPs here that should be severely rate limited on upload.
|
|
||||||
# Note that it is possible to add IP ranges as well as the full IP address.
|
|
||||||
#
|
|
||||||
# Examples:
|
|
||||||
# 192.168.0.0/24 1;
|
|
||||||
# 79.85.222.247 1;
|
|
|
@ -1,4 +0,0 @@
|
||||||
rewrite_by_lua_block {
|
|
||||||
-- set basic authorization header with base64 encoded apipassword
|
|
||||||
ngx.req.set_header("Authorization", require("skynet.utils").authorization_header())
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
# https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&hsts=false&ocsp=false&guideline=5.6
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/skynet-portal/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/skynet-portal/privkey.pem;
|
|
||||||
|
|
||||||
ssl_session_timeout 1d;
|
|
||||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
|
||||||
ssl_session_tickets off;
|
|
||||||
|
|
||||||
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
|
|
||||||
ssl_dhparam /etc/nginx/conf.d/dhparam.pem;
|
|
||||||
|
|
||||||
# intermediate configuration
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
|
|
||||||
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
|
|
||||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
|
||||||
|
|
||||||
# OCSP stapling
|
|
||||||
ssl_stapling on;
|
|
||||||
ssl_stapling_verify on;
|
|
||||||
|
|
||||||
# verify chain of trust of OCSP response using Root CA and Intermediate certs
|
|
||||||
ssl_trusted_certificate /etc/letsencrypt/live/skynet-portal/chain.pem;
|
|
|
@ -1,7 +0,0 @@
|
||||||
# Every file from within this directory will be included in the server block
|
|
||||||
# of the nginx configuration, at the very end. See client.conf.
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# location /blog {
|
|
||||||
# root /var/www/blog;
|
|
||||||
# }
|
|
|
@ -1,62 +0,0 @@
|
||||||
listen 443 ssl http2;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/ssl-settings;
|
|
||||||
include /etc/nginx/conf.d/include/init-optional-variables;
|
|
||||||
|
|
||||||
# Uncomment to launch new Dashboard under /v2 path
|
|
||||||
# location /v2 {
|
|
||||||
# proxy_pass http://dashboard-v2:9000;
|
|
||||||
# }
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://dashboard:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /health {
|
|
||||||
proxy_pass http://accounts:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /stripe/webhook {
|
|
||||||
proxy_pass http://accounts:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/register {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
rewrite /api/(.*) /$1 break;
|
|
||||||
proxy_pass http://accounts:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/user/pubkey/register {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
rewrite /api/(.*) /$1 break;
|
|
||||||
proxy_pass http://accounts:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/login {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
rewrite /api/(.*) /$1 break;
|
|
||||||
proxy_pass http://accounts:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/logout {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
rewrite /api/(.*) /$1 break;
|
|
||||||
proxy_pass http://accounts:3000;
|
|
||||||
}
|
|
|
@ -1,495 +0,0 @@
|
||||||
listen 443 ssl http2;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/ssl-settings;
|
|
||||||
include /etc/nginx/conf.d/include/init-optional-variables;
|
|
||||||
|
|
||||||
# ddos protection: closing slow connections
|
|
||||||
client_body_timeout 1h;
|
|
||||||
client_header_timeout 1h;
|
|
||||||
send_timeout 1h;
|
|
||||||
|
|
||||||
proxy_connect_timeout 1h;
|
|
||||||
proxy_read_timeout 1h;
|
|
||||||
proxy_send_timeout 1h;
|
|
||||||
|
|
||||||
# Increase the body buffer size, to ensure the internal POSTs can always
|
|
||||||
# parse the full POST contents into memory.
|
|
||||||
client_body_buffer_size 128k;
|
|
||||||
client_max_body_size 128k;
|
|
||||||
|
|
||||||
# legacy endpoint rewrite
|
|
||||||
rewrite ^/portals /skynet/portals permanent;
|
|
||||||
rewrite ^/stats /skynet/stats permanent;
|
|
||||||
rewrite ^/skynet/blacklist /skynet/blocklist permanent;
|
|
||||||
rewrite ^/docs(?:/(.*))?$ https://sdk.skynetlabs.com/$1 permanent;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
proxy_pass http://website:9000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/blocklist {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
add_header X-Proxy-Cache $upstream_cache_status;
|
|
||||||
|
|
||||||
proxy_cache skynet;
|
|
||||||
proxy_cache_valid any 1m; # cache blocklist for 1 minute
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_pass http://sia:9980/skynet/blocklist;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/portal/blocklist {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
add_header X-Proxy-Cache $upstream_cache_status;
|
|
||||||
|
|
||||||
proxy_cache skynet;
|
|
||||||
proxy_cache_valid 200 204 15m; # cache portal blocklist for 15 minutes
|
|
||||||
|
|
||||||
# 10.10.10.110 points to blocker service
|
|
||||||
proxy_pass http://10.10.10.110:4000/blocklist;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/portals {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
add_header X-Proxy-Cache $upstream_cache_status;
|
|
||||||
|
|
||||||
proxy_cache skynet;
|
|
||||||
proxy_cache_valid any 1m; # cache portals for 1 minute
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_pass http://sia:9980/skynet/portals;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/stats {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
add_header X-Proxy-Cache $upstream_cache_status;
|
|
||||||
|
|
||||||
proxy_cache skynet;
|
|
||||||
proxy_cache_valid any 1m; # cache stats for 1 minute
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_read_timeout 5m; # extend the read timeout
|
|
||||||
proxy_pass http://sia:9980/skynet/stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define path for server load endpoint
|
|
||||||
location /serverload {
|
|
||||||
# Define root directory in the nginx container to load file from
|
|
||||||
root /usr/local/share;
|
|
||||||
|
|
||||||
# including this because of peer pressure from the other routes
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
# tell nginx to expect json
|
|
||||||
default_type 'application/json';
|
|
||||||
|
|
||||||
# Allow for /serverload to load /serverload.json file
|
|
||||||
try_files $uri $uri.json =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/health {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
add_header X-Proxy-Cache $upstream_cache_status;
|
|
||||||
|
|
||||||
proxy_cache skynet;
|
|
||||||
proxy_cache_key $request_uri; # use whole request uri (uri + args) as cache key
|
|
||||||
proxy_cache_valid any 1m; # cache responses for 1 minute
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_read_timeout 5m; # extend the read timeout
|
|
||||||
proxy_pass http://sia:9980;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /health-check {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
access_log off; # do not log traffic to health-check endpoint
|
|
||||||
|
|
||||||
proxy_pass http://10.10.10.60:3100; # hardcoded ip because health-check waits for nginx
|
|
||||||
}
|
|
||||||
|
|
||||||
location /abuse {
|
|
||||||
return 308 /0404guluqu38oaqapku91ed11kbhkge55smh9lhjukmlrj37lfpm8no/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /abuse/report {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
# 10.10.10.110 points to blocker service
|
|
||||||
proxy_pass http://10.10.10.110:4000/powblock;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /hns {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
# match the request_uri and extract the hns domain and anything that is passed in the uri after it
|
|
||||||
# example: /hns/something/foo/bar matches:
|
|
||||||
# > hns_domain: something
|
|
||||||
# > path: /foo/bar/
|
|
||||||
set_by_lua_block $hns_domain { return string.match(ngx.var.uri, "/hns/([^/?]+)") }
|
|
||||||
set_by_lua_block $path { return string.match(ngx.var.uri, "/hns/[^/?]+(.*)") }
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
include /etc/nginx/conf.d/include/location-hns;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /hnsres {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
|
||||||
|
|
||||||
proxy_pass http://handshake-api:3100;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/registry {
|
|
||||||
include /etc/nginx/conf.d/include/location-skynet-registry;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/restore {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
include /etc/nginx/conf.d/include/sia-auth;
|
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
|
||||||
|
|
||||||
client_max_body_size 5M;
|
|
||||||
|
|
||||||
# increase request timeouts
|
|
||||||
proxy_read_timeout 600;
|
|
||||||
proxy_send_timeout 600;
|
|
||||||
|
|
||||||
proxy_request_buffering off; # stream uploaded files through the proxy as it comes in
|
|
||||||
proxy_set_header Expect $http_expect;
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
|
|
||||||
# proxy this call to siad endpoint (make sure the ip is correct)
|
|
||||||
proxy_pass http://sia:9980;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/registry/subscription {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
# default to unlimited bandwidth and no delay
|
|
||||||
set $bandwidthlimit "0";
|
|
||||||
set $notificationdelay "0";
|
|
||||||
|
|
||||||
rewrite_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
|
|
||||||
if skynet_account.accounts_enabled() then
|
|
||||||
-- check if portal is in authenticated only mode
|
|
||||||
if skynet_account.is_access_unauthorized() then
|
|
||||||
return skynet_account.exit_access_unauthorized()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check if portal is in subscription only mode
|
|
||||||
if skynet_account.is_access_forbidden() then
|
|
||||||
return skynet_account.exit_access_forbidden()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get account limits of currently authenticated user
|
|
||||||
local limits = skynet_account.get_account_limits()
|
|
||||||
|
|
||||||
-- apply bandwidth limit and notification delay
|
|
||||||
ngx.var.bandwidthlimit = limits.download
|
|
||||||
ngx.var.notificationdelay = limits.registry
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "Upgrade";
|
|
||||||
|
|
||||||
proxy_pass http://sia:9980/skynet/registry/subscription?bandwidthlimit=$bandwidthlimit¬ificationdelay=$notificationdelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/skyfile {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
include /etc/nginx/conf.d/include/sia-auth;
|
|
||||||
include /etc/nginx/conf.d/include/generate-siapath;
|
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
|
||||||
|
|
||||||
limit_req zone=uploads_by_ip burst=10 nodelay;
|
|
||||||
limit_req zone=uploads_by_ip_throttled;
|
|
||||||
|
|
||||||
limit_conn upload_conn 5;
|
|
||||||
limit_conn upload_conn_rl 1;
|
|
||||||
|
|
||||||
client_max_body_size 5000M; # make sure to limit the size of upload to a sane value
|
|
||||||
|
|
||||||
# increase request timeouts
|
|
||||||
proxy_read_timeout 600;
|
|
||||||
proxy_send_timeout 600;
|
|
||||||
|
|
||||||
proxy_request_buffering off; # stream uploaded files through the proxy as it comes in
|
|
||||||
proxy_set_header Expect $http_expect;
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
|
|
||||||
# proxy this call to siad endpoint (make sure the ip is correct)
|
|
||||||
proxy_pass http://sia:9980/skynet/skyfile/$dir1/$dir2/$dir3$is_args$args;
|
|
||||||
|
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
local skynet_modules = require("skynet.modules")
|
|
||||||
local skynet_scanner = require("skynet.scanner")
|
|
||||||
local skynet_tracker = require("skynet.tracker")
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("a") then
|
|
||||||
skynet_tracker.track_upload(
|
|
||||||
ngx.header["Skynet-Skylink"],
|
|
||||||
ngx.status,
|
|
||||||
skynet_account.get_auth_headers(),
|
|
||||||
ngx.var.remote_addr
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("s") then
|
|
||||||
skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
|
|
||||||
end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# endpoint implementing resumable file uploads open protocol https://tus.io
|
|
||||||
location /skynet/tus {
|
|
||||||
include /etc/nginx/conf.d/include/cors-headers; # include cors headers but do not overwrite OPTIONS response
|
|
||||||
|
|
||||||
limit_req zone=uploads_by_ip burst=10 nodelay;
|
|
||||||
limit_req zone=uploads_by_ip_throttled;
|
|
||||||
|
|
||||||
limit_conn upload_conn 5;
|
|
||||||
limit_conn upload_conn_rl 1;
|
|
||||||
|
|
||||||
# Do not limit body size in nginx, skyd will reject early on too large upload
|
|
||||||
client_max_body_size 0;
|
|
||||||
|
|
||||||
# Those timeouts need to be elevated since skyd can stall reading
|
|
||||||
# data for a while when overloaded which would terminate connection
|
|
||||||
client_body_timeout 1h;
|
|
||||||
proxy_send_timeout 1h;
|
|
||||||
|
|
||||||
# Add X-Forwarded-* headers
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# rewrite proxy request to use correct host uri from env variable (required to return correct location header)
|
|
||||||
proxy_redirect $scheme://$host $scheme://$skynet_server_domain;
|
|
||||||
|
|
||||||
# proxy /skynet/tus requests to siad endpoint with all arguments
|
|
||||||
proxy_pass http://sia:9980;
|
|
||||||
|
|
||||||
access_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
|
|
||||||
if skynet_account.accounts_enabled() then
|
|
||||||
-- check if portal is in authenticated only mode
|
|
||||||
if skynet_account.is_access_unauthorized() then
|
|
||||||
return skynet_account.exit_access_unauthorized()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check if portal is in subscription only mode
|
|
||||||
if skynet_account.is_access_forbidden() then
|
|
||||||
return skynet_account.exit_access_forbidden()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get account limits of currently authenticated user
|
|
||||||
local limits = skynet_account.get_account_limits()
|
|
||||||
|
|
||||||
-- apply upload size limits
|
|
||||||
ngx.req.set_header("SkynetMaxUploadSize", limits.maxUploadSize)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
# extract skylink from base64 encoded upload metadata and assign to a proper header
|
|
||||||
header_filter_by_lua_block {
|
|
||||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
|
||||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
|
||||||
|
|
||||||
if ngx.header["Upload-Metadata"] then
|
|
||||||
local encodedSkylink = string.match(ngx.header["Upload-Metadata"], "Skylink ([^,?]+)")
|
|
||||||
|
|
||||||
if encodedSkylink then
|
|
||||||
ngx.header["Skynet-Skylink"] = ngx.decode_base64(encodedSkylink)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
local skynet_modules = require("skynet.modules")
|
|
||||||
local skynet_scanner = require("skynet.scanner")
|
|
||||||
local skynet_tracker = require("skynet.tracker")
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("a") then
|
|
||||||
skynet_tracker.track_upload(
|
|
||||||
ngx.header["Skynet-Skylink"],
|
|
||||||
ngx.status,
|
|
||||||
skynet_account.get_auth_headers(),
|
|
||||||
ngx.var.remote_addr
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("s") then
|
|
||||||
skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
|
|
||||||
end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/pin {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
include /etc/nginx/conf.d/include/sia-auth;
|
|
||||||
include /etc/nginx/conf.d/include/generate-siapath;
|
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
|
||||||
|
|
||||||
limit_req zone=uploads_by_ip burst=10 nodelay;
|
|
||||||
limit_req zone=uploads_by_ip_throttled;
|
|
||||||
|
|
||||||
limit_conn upload_conn 5;
|
|
||||||
limit_conn upload_conn_rl 1;
|
|
||||||
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_pass http://sia:9980$uri?siapath=$dir1/$dir2/$dir3&$args;
|
|
||||||
|
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
local skynet_modules = require("skynet.modules")
|
|
||||||
local skynet_scanner = require("skynet.scanner")
|
|
||||||
local skynet_tracker = require("skynet.tracker")
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("a") then
|
|
||||||
skynet_tracker.track_upload(
|
|
||||||
ngx.header["Skynet-Skylink"],
|
|
||||||
ngx.status,
|
|
||||||
skynet_account.get_auth_headers(),
|
|
||||||
ngx.var.remote_addr
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("s") then
|
|
||||||
skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
|
|
||||||
end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/metadata {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
|
||||||
|
|
||||||
header_filter_by_lua_block {
|
|
||||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
|
||||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_pass http://sia:9980;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/resolve {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
|
||||||
|
|
||||||
header_filter_by_lua_block {
|
|
||||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
|
||||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_pass http://sia:9980;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ "^/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
|
|
||||||
set $skylink $2;
|
|
||||||
set $path $3;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/location-skylink;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ "^/file/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
|
|
||||||
set $skylink $2;
|
|
||||||
set $path $3;
|
|
||||||
set $args attachment=true&$args;
|
|
||||||
#set $is_args ?;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/location-skylink;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /skynet/trustless/basesector {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
|
||||||
|
|
||||||
# default download rate to unlimited
|
|
||||||
set $limit_rate 0;
|
|
||||||
|
|
||||||
access_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
|
|
||||||
if skynet_account.accounts_enabled() then
|
|
||||||
-- check if portal is in authenticated only mode
|
|
||||||
if skynet_account.is_access_unauthorized() then
|
|
||||||
return skynet_account.exit_access_unauthorized()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check if portal is in subscription only mode
|
|
||||||
if skynet_account.is_access_forbidden() then
|
|
||||||
return skynet_account.exit_access_forbidden()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get account limits of currently authenticated user
|
|
||||||
local limits = skynet_account.get_account_limits()
|
|
||||||
|
|
||||||
-- apply download speed limit
|
|
||||||
ngx.var.limit_rate = limits.download
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
limit_rate_after 512k;
|
|
||||||
limit_rate $limit_rate;
|
|
||||||
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
|
||||||
proxy_pass http://sia:9980;
|
|
||||||
|
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
local skynet_modules = require("skynet.modules")
|
|
||||||
local skynet_scanner = require("skynet.scanner")
|
|
||||||
local skynet_tracker = require("skynet.tracker")
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("a") then
|
|
||||||
skynet_tracker.track_download(ngx.header["Skynet-Skylink"], ngx.status, skynet_account.get_auth_headers(), ngx.var.body_bytes_sent)
|
|
||||||
end
|
|
||||||
|
|
||||||
if skynet_modules.is_enabled("s") then
|
|
||||||
skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
|
|
||||||
end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
location /__internal/do/not/use/accounts {
|
|
||||||
include /etc/nginx/conf.d/include/cors;
|
|
||||||
|
|
||||||
charset utf-8;
|
|
||||||
charset_types application/json;
|
|
||||||
default_type application/json;
|
|
||||||
|
|
||||||
content_by_lua_block {
|
|
||||||
local json = require('cjson')
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
|
|
||||||
local accounts_enabled = skynet_account.accounts_enabled()
|
|
||||||
local is_auth_required = skynet_account.is_auth_required()
|
|
||||||
local is_subscription_required = skynet_account.is_subscription_required()
|
|
||||||
local is_authenticated = skynet_account.is_authenticated()
|
|
||||||
local has_subscription = skynet_account.has_subscription()
|
|
||||||
|
|
||||||
ngx.say(json.encode{
|
|
||||||
enabled = accounts_enabled,
|
|
||||||
auth_required = is_auth_required,
|
|
||||||
subscription_required = is_subscription_required,
|
|
||||||
authenticated = is_authenticated,
|
|
||||||
subscription = has_subscription,
|
|
||||||
})
|
|
||||||
return ngx.exit(ngx.HTTP_OK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/server-override/*;
|
|
|
@ -1,55 +0,0 @@
|
||||||
include /etc/nginx/conf.d/include/init-optional-variables;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
set $skylink "";
|
|
||||||
set $path $uri;
|
|
||||||
|
|
||||||
rewrite_by_lua_block {
|
|
||||||
local cjson = require("cjson")
|
|
||||||
local cache = ngx.shared.dnslink
|
|
||||||
local cache_value = cache:get(ngx.var.host)
|
|
||||||
|
|
||||||
if cache_value == nil then
|
|
||||||
local httpc = require("resty.http").new()
|
|
||||||
|
|
||||||
-- 10.10.10.55 points to dnslink-api service (alias not available when using resty-http)
|
|
||||||
local res, err = httpc:request_uri("http://10.10.10.55:3100/dnslink/" .. ngx.var.host)
|
|
||||||
|
|
||||||
if err or (res and res.status ~= ngx.HTTP_OK) then
|
|
||||||
-- check whether we can fallback to regular skylink request
|
|
||||||
local match_skylink = ngx.re.match(ngx.var.uri, "^/([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?")
|
|
||||||
|
|
||||||
if match_skylink then
|
|
||||||
ngx.var.skylink = match_skylink[1]
|
|
||||||
ngx.var.path = match_skylink[2] or "/"
|
|
||||||
else
|
|
||||||
ngx.status = (err and ngx.HTTP_INTERNAL_SERVER_ERROR) or res.status
|
|
||||||
ngx.header["content-type"] = "text/plain"
|
|
||||||
ngx.say(err or res.body)
|
|
||||||
ngx.exit(ngx.status)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local resolved = cjson.decode(res.body)
|
|
||||||
|
|
||||||
ngx.var.skylink = resolved.skylink
|
|
||||||
if resolved.sponsor then
|
|
||||||
ngx.req.set_header("Skynet-Api-Key", resolved.sponsor)
|
|
||||||
end
|
|
||||||
|
|
||||||
local cache_ttl = 300 -- 5 minutes cache expire time
|
|
||||||
cache:set(ngx.var.host, res.body, cache_ttl)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local resolved = cjson.decode(cache_value)
|
|
||||||
|
|
||||||
ngx.var.skylink = resolved.skylink
|
|
||||||
if resolved.sponsor then
|
|
||||||
ngx.req.set_header("Skynet-Api-Key", resolved.sponsor)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ngx.var.skylink = require("skynet.skylink").parse(ngx.var.skylink)
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/location-skylink;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
listen 443 ssl http2;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/ssl-settings;
|
|
||||||
include /etc/nginx/conf.d/include/init-optional-variables;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
set_by_lua_block $hns_domain { return string.match(ngx.var.host, "[^%.]+") }
|
|
||||||
set $path $uri;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/location-hns;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
listen 80;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/init-optional-variables;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
listen 443 ssl http2;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/ssl-settings;
|
|
||||||
include /etc/nginx/conf.d/include/init-optional-variables;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
set_by_lua_block $skylink { return string.match(ngx.var.host, "%w+") }
|
|
||||||
set_by_lua_block $path {
|
|
||||||
-- strip ngx.var.request_uri from query params - this is basically the same as ngx.var.uri but
|
|
||||||
-- do not use ngx.var.uri because it will already be unescaped and we need to use escaped path
|
|
||||||
-- examples: escaped uri "/b%20r56+7" and unescaped uri "/b r56 7"
|
|
||||||
return string.gsub(ngx.var.request_uri, "?.*", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/include/location-skylink;
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# https://github.com/nginxinc/docker-nginx/blob/master/entrypoint/20-envsubst-on-templates.sh
|
|
||||||
# https://github.com/nginxinc/docker-nginx/blob/master/LICENSE
|
|
||||||
|
|
||||||
# Copyright (C) 2011-2016 Nginx, Inc.
|
|
||||||
# All rights reserved.
|
|
||||||
|
|
||||||
# Redistribution and use in source and binary forms, with or without
|
|
||||||
# modification, are permitted provided that the following conditions
|
|
||||||
# are met:
|
|
||||||
# 1. Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# 2. Redistributions in binary form must reproduce the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer in the
|
|
||||||
# documentation and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
||||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
||||||
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
||||||
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
||||||
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
||||||
# SUCH DAMAGE.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
ME=$(basename $0)
|
|
||||||
|
|
||||||
auto_envsubst() {
|
|
||||||
local template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}"
|
|
||||||
local suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}"
|
|
||||||
local output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}"
|
|
||||||
|
|
||||||
local template defined_envs relative_path output_path subdir
|
|
||||||
defined_envs=$(printf '${%s} ' $(env | cut -d= -f1))
|
|
||||||
[ -d "$template_dir" ] || return 0
|
|
||||||
if [ ! -w "$output_dir" ]; then
|
|
||||||
echo >&3 "$ME: ERROR: $template_dir exists, but $output_dir is not writable"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do
|
|
||||||
relative_path="${template#$template_dir/}"
|
|
||||||
output_path="$output_dir/${relative_path%$suffix}"
|
|
||||||
subdir=$(dirname "$relative_path")
|
|
||||||
# create a subdirectory where the template file exists
|
|
||||||
mkdir -p "$output_dir/$subdir"
|
|
||||||
echo >&3 "$ME: Running envsubst on $template to $output_path"
|
|
||||||
envsubst "$defined_envs" < "$template" > "$output_path"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
auto_envsubst
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,20 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# source: https://github.com/nginxinc/docker-nginx/pull/509
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
ME=$(basename $0)
|
|
||||||
|
|
||||||
[ "${NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS:-}" ] || exit 0
|
|
||||||
if [ $(echo "$NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS > 0" | bc) = 0 ]; then
|
|
||||||
echo >&3 "$ME: Error. Provide integer or floating point number greater that 0. See 'man sleep'."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
start_background_reload() {
|
|
||||||
echo >&3 "$ME: Reloading Nginx every $NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS hour(s)"
|
|
||||||
while :; do sleep ${NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS}h; echo >&3 "$ME: Reloading Nginx ..." && nginx -s reload; done &
|
|
||||||
}
|
|
||||||
|
|
||||||
start_background_reload
|
|
|
@ -1,18 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Generate locally signed ssl certificate to be used on routes
|
|
||||||
# that do not require certificate issued by trusted CA
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
ME=$(basename $0)
|
|
||||||
|
|
||||||
generate_local_certificate() {
|
|
||||||
echo >&3 "$ME: Generating locally signed ssl certificate"
|
|
||||||
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
|
|
||||||
-subj '/CN=local-certificate' \
|
|
||||||
-keyout /etc/ssl/local-certificate.key \
|
|
||||||
-out /etc/ssl/local-certificate.crt
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_local_certificate
|
|
|
@ -1,78 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# vim:sw=4:ts=4:et
|
|
||||||
|
|
||||||
# https://github.com/nginxinc/docker-nginx/blob/master/entrypoint/docker-entrypoint.sh
|
|
||||||
# https://github.com/nginxinc/docker-nginx/blob/master/LICENSE
|
|
||||||
|
|
||||||
# Copyright (C) 2011-2016 Nginx, Inc.
|
|
||||||
# All rights reserved.
|
|
||||||
|
|
||||||
# Redistribution and use in source and binary forms, with or without
|
|
||||||
# modification, are permitted provided that the following conditions
|
|
||||||
# are met:
|
|
||||||
# 1. Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# 2. Redistributions in binary form must reproduce the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer in the
|
|
||||||
# documentation and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
||||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
||||||
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
||||||
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
||||||
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
||||||
# SUCH DAMAGE.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
|
|
||||||
exec 3>&1
|
|
||||||
else
|
|
||||||
exec 3>/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Really dirty backwards compatible workaround for single server portals:
|
|
||||||
# =======================================================================
|
|
||||||
# in the past we used to require single server portals not to include
|
|
||||||
# server domain env variable because it messed up with caddy; we switched
|
|
||||||
# to certbot and that is not the case any more but we also switched to
|
|
||||||
# using built in envsubst instead of custom mustache script for nginx
|
|
||||||
# templating and now when server domain is not defined, it messes up whole
|
|
||||||
# nginx config and nginx will not start; this workaround assigns portal
|
|
||||||
# domain as a server domain if server domain is not defined
|
|
||||||
if [ -z "${SERVER_DOMAIN}" ]; then
|
|
||||||
export SERVER_DOMAIN=${PORTAL_DOMAIN}
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$1" = "nginx" -o "$1" = "nginx-debug" ]; then
|
|
||||||
if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then
|
|
||||||
echo >&3 "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration"
|
|
||||||
|
|
||||||
echo >&3 "$0: Looking for shell scripts in /docker-entrypoint.d/"
|
|
||||||
find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do
|
|
||||||
case "$f" in
|
|
||||||
*.sh)
|
|
||||||
if [ -x "$f" ]; then
|
|
||||||
echo >&3 "$0: Launching $f";
|
|
||||||
"$f"
|
|
||||||
else
|
|
||||||
# warn on shell scripts without exec bit
|
|
||||||
echo >&3 "$0: Ignoring $f, not executable";
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*) echo >&3 "$0: Ignoring $f";;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
echo >&3 "$0: Configuration complete; ready for start up"
|
|
||||||
else
|
|
||||||
echo >&3 "$0: No files found in /docker-entrypoint.d/, skipping configuration"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
|
|
@ -1,301 +0,0 @@
|
||||||
-- source: https://github.com/aiq/basexx
|
|
||||||
-- license: MIT
|
|
||||||
-- modified: exposed from_basexx and to_basexx generic functions
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- util functions
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local function divide_string( str, max )
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
local start = 1
|
|
||||||
for i = 1, #str do
|
|
||||||
if i % max == 0 then
|
|
||||||
table.insert( result, str:sub( start, i ) )
|
|
||||||
start = i + 1
|
|
||||||
elseif i == #str then
|
|
||||||
table.insert( result, str:sub( start, i ) )
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
local function number_to_bit( num, length )
|
|
||||||
local bits = {}
|
|
||||||
|
|
||||||
while num > 0 do
|
|
||||||
local rest = math.floor( math.fmod( num, 2 ) )
|
|
||||||
table.insert( bits, rest )
|
|
||||||
num = ( num - rest ) / 2
|
|
||||||
end
|
|
||||||
|
|
||||||
while #bits < length do
|
|
||||||
table.insert( bits, "0" )
|
|
||||||
end
|
|
||||||
|
|
||||||
return string.reverse( table.concat( bits ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ignore_set( str, set )
|
|
||||||
if set then
|
|
||||||
str = str:gsub( "["..set.."]", "" )
|
|
||||||
end
|
|
||||||
return str
|
|
||||||
end
|
|
||||||
|
|
||||||
local function pure_from_bit( str )
|
|
||||||
return ( str:gsub( '........', function ( cc )
|
|
||||||
return string.char( tonumber( cc, 2 ) )
|
|
||||||
end ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
local function unexpected_char_error( str, pos )
|
|
||||||
local c = string.sub( str, pos, pos )
|
|
||||||
return string.format( "unexpected character at position %d: '%s'", pos, c )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local basexx = {}
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- base2(bitfield) decode and encode function
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local bitMap = { o = "0", i = "1", l = "1" }
|
|
||||||
|
|
||||||
function basexx.from_bit( str, ignore )
|
|
||||||
str = ignore_set( str, ignore )
|
|
||||||
str = string.lower( str )
|
|
||||||
str = str:gsub( '[ilo]', function( c ) return bitMap[ c ] end )
|
|
||||||
local pos = string.find( str, "[^01]" )
|
|
||||||
if pos then return nil, unexpected_char_error( str, pos ) end
|
|
||||||
|
|
||||||
return pure_from_bit( str )
|
|
||||||
end
|
|
||||||
|
|
||||||
function basexx.to_bit( str )
|
|
||||||
return ( str:gsub( '.', function ( c )
|
|
||||||
local byte = string.byte( c )
|
|
||||||
local bits = {}
|
|
||||||
for _ = 1,8 do
|
|
||||||
table.insert( bits, byte % 2 )
|
|
||||||
byte = math.floor( byte / 2 )
|
|
||||||
end
|
|
||||||
return table.concat( bits ):reverse()
|
|
||||||
end ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- base16(hex) decode and encode function
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function basexx.from_hex( str, ignore )
|
|
||||||
str = ignore_set( str, ignore )
|
|
||||||
local pos = string.find( str, "[^%x]" )
|
|
||||||
if pos then return nil, unexpected_char_error( str, pos ) end
|
|
||||||
|
|
||||||
return ( str:gsub( '..', function ( cc )
|
|
||||||
return string.char( tonumber( cc, 16 ) )
|
|
||||||
end ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
function basexx.to_hex( str )
|
|
||||||
return ( str:gsub( '.', function ( c )
|
|
||||||
return string.format('%02X', string.byte( c ) )
|
|
||||||
end ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- generic function to decode and encode base32/base64
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function basexx.from_basexx( str, alphabet, bits )
|
|
||||||
local result = {}
|
|
||||||
for i = 1, #str do
|
|
||||||
local c = string.sub( str, i, i )
|
|
||||||
if c ~= '=' then
|
|
||||||
local index = string.find( alphabet, c, 1, true )
|
|
||||||
if not index then
|
|
||||||
return nil, unexpected_char_error( str, i )
|
|
||||||
end
|
|
||||||
table.insert( result, number_to_bit( index - 1, bits ) )
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local value = table.concat( result )
|
|
||||||
local pad = #value % 8
|
|
||||||
return pure_from_bit( string.sub( value, 1, #value - pad ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
function basexx.to_basexx( str, alphabet, bits, pad )
|
|
||||||
local bitString = basexx.to_bit( str )
|
|
||||||
|
|
||||||
local chunks = divide_string( bitString, bits )
|
|
||||||
local result = {}
|
|
||||||
for _,value in ipairs( chunks ) do
|
|
||||||
if ( #value < bits ) then
|
|
||||||
value = value .. string.rep( '0', bits - #value )
|
|
||||||
end
|
|
||||||
local pos = tonumber( value, 2 ) + 1
|
|
||||||
table.insert( result, alphabet:sub( pos, pos ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert( result, pad )
|
|
||||||
return table.concat( result )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- rfc 3548: http://www.rfc-editor.org/rfc/rfc3548.txt
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
|
|
||||||
local base32PadMap = { "", "======", "====", "===", "=" }
|
|
||||||
|
|
||||||
function basexx.from_base32( str, ignore )
|
|
||||||
str = ignore_set( str, ignore )
|
|
||||||
return basexx.from_basexx( string.upper( str ), base32Alphabet, 5 )
|
|
||||||
end
|
|
||||||
|
|
||||||
function basexx.to_base32( str )
|
|
||||||
return basexx.to_basexx( str, base32Alphabet, 5, base32PadMap[ #str % 5 + 1 ] )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- crockford: http://www.crockford.com/wrmg/base32.html
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
||||||
local crockfordMap = { O = "0", I = "1", L = "1" }
|
|
||||||
|
|
||||||
function basexx.from_crockford( str, ignore )
|
|
||||||
str = ignore_set( str, ignore )
|
|
||||||
str = string.upper( str )
|
|
||||||
str = str:gsub( '[ILOU]', function( c ) return crockfordMap[ c ] end )
|
|
||||||
return basexx.from_basexx( str, crockfordAlphabet, 5 )
|
|
||||||
end
|
|
||||||
|
|
||||||
function basexx.to_crockford( str )
|
|
||||||
return basexx.to_basexx( str, crockfordAlphabet, 5, "" )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- base64 decode and encode function
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
|
|
||||||
"abcdefghijklmnopqrstuvwxyz"..
|
|
||||||
"0123456789+/"
|
|
||||||
local base64PadMap = { "", "==", "=" }
|
|
||||||
|
|
||||||
function basexx.from_base64( str, ignore )
|
|
||||||
str = ignore_set( str, ignore )
|
|
||||||
return basexx.from_basexx( str, base64Alphabet, 6 )
|
|
||||||
end
|
|
||||||
|
|
||||||
function basexx.to_base64( str )
|
|
||||||
return basexx.to_basexx( str, base64Alphabet, 6, base64PadMap[ #str % 3 + 1 ] )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- URL safe base64 decode and encode function
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local url64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
|
|
||||||
"abcdefghijklmnopqrstuvwxyz"..
|
|
||||||
"0123456789-_"
|
|
||||||
|
|
||||||
function basexx.from_url64( str, ignore )
|
|
||||||
str = ignore_set( str, ignore )
|
|
||||||
return basexx.from_basexx( str, url64Alphabet, 6 )
|
|
||||||
end
|
|
||||||
|
|
||||||
function basexx.to_url64( str )
|
|
||||||
return basexx.to_basexx( str, url64Alphabet, 6, "" )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
--
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
local function length_error( len, d )
|
|
||||||
return string.format( "invalid length: %d - must be a multiple of %d", len, d )
|
|
||||||
end
|
|
||||||
|
|
||||||
local z85Decoder = { 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00,
|
|
||||||
0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45,
|
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
||||||
0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47,
|
|
||||||
0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
|
|
||||||
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
|
||||||
0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
|
|
||||||
0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00,
|
|
||||||
0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
|
|
||||||
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
|
||||||
0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
|
|
||||||
0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 }
|
|
||||||
|
|
||||||
function basexx.from_z85( str, ignore )
|
|
||||||
str = ignore_set( str, ignore )
|
|
||||||
if ( #str % 5 ) ~= 0 then
|
|
||||||
return nil, length_error( #str, 5 )
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
local value = 0
|
|
||||||
for i = 1, #str do
|
|
||||||
local index = string.byte( str, i ) - 31
|
|
||||||
if index < 1 or index >= #z85Decoder then
|
|
||||||
return nil, unexpected_char_error( str, i )
|
|
||||||
end
|
|
||||||
value = ( value * 85 ) + z85Decoder[ index ]
|
|
||||||
if ( i % 5 ) == 0 then
|
|
||||||
local divisor = 256 * 256 * 256
|
|
||||||
while divisor ~= 0 do
|
|
||||||
local b = math.floor( value / divisor ) % 256
|
|
||||||
table.insert( result, string.char( b ) )
|
|
||||||
divisor = math.floor( divisor / 256 )
|
|
||||||
end
|
|
||||||
value = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat( result )
|
|
||||||
end
|
|
||||||
|
|
||||||
local z85Encoder = "0123456789"..
|
|
||||||
"abcdefghijklmnopqrstuvwxyz"..
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
|
|
||||||
".-:+=^!/*?&<>()[]{}@%$#"
|
|
||||||
|
|
||||||
function basexx.to_z85( str )
|
|
||||||
if ( #str % 4 ) ~= 0 then
|
|
||||||
return nil, length_error( #str, 4 )
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
local value = 0
|
|
||||||
for i = 1, #str do
|
|
||||||
local b = string.byte( str, i )
|
|
||||||
value = ( value * 256 ) + b
|
|
||||||
if ( i % 4 ) == 0 then
|
|
||||||
local divisor = 85 * 85 * 85 * 85
|
|
||||||
while divisor ~= 0 do
|
|
||||||
local index = ( math.floor( value / divisor ) % 85 ) + 1
|
|
||||||
table.insert( result, z85Encoder:sub( index, index ) )
|
|
||||||
divisor = math.floor( divisor / 85 )
|
|
||||||
end
|
|
||||||
value = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat( result )
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
return basexx
|
|
|
@ -1,153 +0,0 @@
|
||||||
local _M = {}
|
|
||||||
|
|
||||||
-- constant tier ids
|
|
||||||
local tier_id_anonymous = 0
|
|
||||||
local tier_id_free = 1
|
|
||||||
|
|
||||||
-- fallback - remember to keep those updated
|
|
||||||
local anon_limits = {
|
|
||||||
["tierID"] = tier_id_anonymous,
|
|
||||||
["tierName"] = "anonymous",
|
|
||||||
["upload"] = 655360,
|
|
||||||
["download"] = 655360,
|
|
||||||
["maxUploadSize"] = 1073741824,
|
|
||||||
["registry"] = 250
|
|
||||||
}
|
|
||||||
|
|
||||||
-- get all non empty authentication headers from request, we want to return
|
|
||||||
-- all of them and let accounts service deal with validation and prioritisation
|
|
||||||
function _M.get_auth_headers()
|
|
||||||
local utils = require("utils")
|
|
||||||
local request_headers = ngx.req.get_headers()
|
|
||||||
local headers = {}
|
|
||||||
|
|
||||||
-- try to extract skynet-jwt cookie from cookie header
|
|
||||||
local skynet_jwt_cookie = utils.extract_cookie(request_headers["Cookie"], "skynet[-]jwt")
|
|
||||||
|
|
||||||
-- if skynet-jwt cookie is present, pass it as is
|
|
||||||
if skynet_jwt_cookie then
|
|
||||||
headers["Cookie"] = skynet_jwt_cookie
|
|
||||||
end
|
|
||||||
|
|
||||||
-- if authorization header is set, pass it as is
|
|
||||||
if request_headers["Authorization"] then
|
|
||||||
headers["Authorization"] = request_headers["Authorization"]
|
|
||||||
end
|
|
||||||
|
|
||||||
-- if skynet api key header is set, pass it as is
|
|
||||||
if request_headers["Skynet-Api-Key"] then
|
|
||||||
headers["Skynet-Api-Key"] = request_headers["Skynet-Api-Key"]
|
|
||||||
end
|
|
||||||
|
|
||||||
return headers
|
|
||||||
end
|
|
||||||
|
|
||||||
-- handle request exit when access to portal should be restricted to authenticated users only
|
|
||||||
function _M.exit_access_unauthorized(message)
|
|
||||||
ngx.status = ngx.HTTP_UNAUTHORIZED
|
|
||||||
ngx.header["content-type"] = "text/plain"
|
|
||||||
ngx.say(message or "Portal operator restricted access to authenticated users only")
|
|
||||||
return ngx.exit(ngx.status)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- handle request exit when access to portal should be restricted to subscription users only
|
|
||||||
function _M.exit_access_forbidden(message)
|
|
||||||
ngx.status = ngx.HTTP_FORBIDDEN
|
|
||||||
ngx.header["content-type"] = "text/plain"
|
|
||||||
ngx.say(message or "Portal operator restricted access to users with active subscription only")
|
|
||||||
return ngx.exit(ngx.status)
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.accounts_enabled()
|
|
||||||
local skynet_modules = require("skynet.modules")
|
|
||||||
|
|
||||||
return skynet_modules.is_enabled("a")
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.get_account_limits()
|
|
||||||
local cjson = require('cjson')
|
|
||||||
local utils = require('utils')
|
|
||||||
local auth_headers = _M.get_auth_headers()
|
|
||||||
|
|
||||||
-- simple case of anonymous request - none of available auth headers exist
|
|
||||||
if utils.is_table_empty(auth_headers) then
|
|
||||||
return anon_limits
|
|
||||||
end
|
|
||||||
|
|
||||||
if ngx.var.account_limits == "" then
|
|
||||||
local httpc = require("resty.http").new()
|
|
||||||
local uri = "http://10.10.10.70:3000/user/limits"
|
|
||||||
|
|
||||||
-- include skylink if it is available in the context of request
|
|
||||||
-- todo: this should not rely on skylink variable to be defined
|
|
||||||
if ngx.var.skylink ~= nil and ngx.var.skylink ~= "" then
|
|
||||||
uri = uri .. "/" .. ngx.var.skylink
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
|
|
||||||
local res, err = httpc:request_uri(uri .. "?unit=byte", {
|
|
||||||
headers = auth_headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- fail gracefully in case /user/limits failed
|
|
||||||
if err or (res and res.status ~= ngx.HTTP_OK) then
|
|
||||||
local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
|
|
||||||
ngx.log(ngx.ERR, "Failed accounts service request /user/limits?unit=byte: ", error_response)
|
|
||||||
ngx.var.account_limits = cjson.encode(anon_limits)
|
|
||||||
elseif res and res.status == ngx.HTTP_OK then
|
|
||||||
ngx.var.account_limits = res.body
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return cjson.decode(ngx.var.account_limits)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- detect whether current user is authenticated
|
|
||||||
function _M.is_authenticated()
|
|
||||||
if not _M.accounts_enabled() then return false end
|
|
||||||
|
|
||||||
local limits = _M.get_account_limits()
|
|
||||||
|
|
||||||
return limits.tierID > tier_id_anonymous
|
|
||||||
end
|
|
||||||
|
|
||||||
-- detect whether current user has active subscription
|
|
||||||
function _M.has_subscription()
|
|
||||||
local limits = _M.get_account_limits()
|
|
||||||
|
|
||||||
return limits.tierID > tier_id_free
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.is_auth_required()
|
|
||||||
-- authentication is required if mode is set to "authenticated"
|
|
||||||
-- or "subscription" (require active subscription to a premium plan)
|
|
||||||
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "authenticated" or _M.is_subscription_required()
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.is_subscription_required()
|
|
||||||
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "subscription"
|
|
||||||
end
|
|
||||||
|
|
||||||
local is_access_always_allowed = function ()
|
|
||||||
-- options requests do not attach cookies - should always be available
|
|
||||||
-- requests should not be limited based on accounts if accounts are not enabled
|
|
||||||
return ngx.req.get_method() == "OPTIONS" or not _M.accounts_enabled()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check whether access is restricted if portal requires authorization
|
|
||||||
function _M.is_access_unauthorized()
|
|
||||||
if is_access_always_allowed() then return false end
|
|
||||||
|
|
||||||
-- check if authentication is required and request is not authenticated
|
|
||||||
return _M.is_auth_required() and not _M.is_authenticated()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check whether user is authenticated but does not have access to given resources
|
|
||||||
function _M.is_access_forbidden()
|
|
||||||
if is_access_always_allowed() then return false end
|
|
||||||
|
|
||||||
-- check if active subscription is required and request is from user without it
|
|
||||||
return _M.is_subscription_required() and not _M.has_subscription()
|
|
||||||
end
|
|
||||||
|
|
||||||
return _M
|
|
|
@ -1,23 +0,0 @@
|
||||||
local _M = {}
|
|
||||||
|
|
||||||
local utils = require("utils")
|
|
||||||
|
|
||||||
function _M.is_enabled(module_abbr)
|
|
||||||
if type(module_abbr) ~= "string" or module_abbr:len() ~= 1 then
|
|
||||||
error("Module abbreviation '" .. tostring(module_abbr) .. "' should be exactly one character long string")
|
|
||||||
end
|
|
||||||
|
|
||||||
local enabled_modules = utils.getenv("PORTAL_MODULES")
|
|
||||||
|
|
||||||
if not enabled_modules then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
return enabled_modules:find(module_abbr) ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.is_disabled(module_abbr)
|
|
||||||
return not _M.is_enabled(module_abbr)
|
|
||||||
end
|
|
||||||
|
|
||||||
return _M
|
|
|
@ -1,95 +0,0 @@
|
||||||
-- luacheck: ignore os
|
|
||||||
|
|
||||||
local skynet_modules = require("skynet.modules")
|
|
||||||
|
|
||||||
describe("is_enabled", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(os, "getenv")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
os.getenv:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return false if PORTAL_MODULES are not defined", function()
|
|
||||||
os.getenv.on_call_with("PORTAL_MODULES").returns(nil)
|
|
||||||
|
|
||||||
assert.is_false(skynet_modules.is_enabled("a"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return false if PORTAL_MODULES are empty", function()
|
|
||||||
os.getenv.on_call_with("PORTAL_MODULES").returns("")
|
|
||||||
|
|
||||||
assert.is_false(skynet_modules.is_enabled("a"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return false if module is not enabled", function()
|
|
||||||
os.getenv.on_call_with("PORTAL_MODULES").returns("qwerty")
|
|
||||||
|
|
||||||
assert.is_false(skynet_modules.is_enabled("a"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return true if module is enabled", function()
|
|
||||||
os.getenv.on_call_with("PORTAL_MODULES").returns("asdfg")
|
|
||||||
|
|
||||||
assert.is_true(skynet_modules.is_enabled("a"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should throw an error for empty module", function()
|
|
||||||
assert.has_error(function()
|
|
||||||
skynet_modules.is_enabled()
|
|
||||||
end, "Module abbreviation 'nil' should be exactly one character long string")
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should throw an error for too long module", function()
|
|
||||||
assert.has_error(function()
|
|
||||||
skynet_modules.is_enabled("gandalf")
|
|
||||||
end, "Module abbreviation 'gandalf' should be exactly one character long string")
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("is_disabled", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(os, "getenv")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
os.getenv:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return true if PORTAL_MODULES are not defined", function()
|
|
||||||
os.getenv.on_call_with("PORTAL_MODULES").returns(nil)
|
|
||||||
|
|
||||||
assert.is_true(skynet_modules.is_disabled("a"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return true if PORTAL_MODULES are empty", function()
|
|
||||||
os.getenv.on_call_with("PORTAL_MODULES").returns("")
|
|
||||||
|
|
||||||
assert.is_true(skynet_modules.is_disabled("a"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return true if module is not enabled", function()
|
|
||||||
os.getenv.on_call_with("PORTAL_MODULES").returns("qwerty")
|
|
||||||
|
|
||||||
assert.is_true(skynet_modules.is_disabled("a"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return false if module is enabled", function()
|
|
||||||
os.getenv.on_call_with("PORTAL_MODULES").returns("asdfg")
|
|
||||||
|
|
||||||
assert.is_false(skynet_modules.is_disabled("a"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should throw an error for empty module", function()
|
|
||||||
assert.has_error(function()
|
|
||||||
skynet_modules.is_disabled()
|
|
||||||
end, "Module abbreviation 'nil' should be exactly one character long string")
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should throw an error for too long module", function()
|
|
||||||
assert.has_error(function()
|
|
||||||
skynet_modules.is_disabled("gandalf")
|
|
||||||
end, "Module abbreviation 'gandalf' should be exactly one character long string")
|
|
||||||
end)
|
|
||||||
end)
|
|
|
@ -1,26 +0,0 @@
|
||||||
local _M = {}
|
|
||||||
|
|
||||||
function _M.scan_skylink_timer(premature, skylink)
|
|
||||||
if premature then return end
|
|
||||||
|
|
||||||
local httpc = require("resty.http").new()
|
|
||||||
|
|
||||||
-- 10.10.10.101 points to malware-scanner service (alias not available when using resty-http)
|
|
||||||
local res, err = httpc:request_uri("http://10.10.10.101:4000/scan/" .. skylink, {
|
|
||||||
method = "POST",
|
|
||||||
})
|
|
||||||
|
|
||||||
if err or (res and res.status ~= ngx.HTTP_OK) then
|
|
||||||
local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
|
|
||||||
ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", error_response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.scan_skylink(skylink)
|
|
||||||
if not skylink then return end
|
|
||||||
|
|
||||||
local ok, err = ngx.timer.at(0, _M.scan_skylink_timer, skylink)
|
|
||||||
if not ok then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
|
|
||||||
return _M
|
|
|
@ -1,119 +0,0 @@
|
||||||
-- luacheck: ignore ngx
|
|
||||||
|
|
||||||
local skynet_scanner = require("skynet.scanner")
|
|
||||||
local skylink = "AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA"
|
|
||||||
|
|
||||||
describe("scan_skylink", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(ngx.timer, "at")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
ngx.timer.at:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should schedule a timer when skylink is provided", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_scanner.scan_skylink(skylink)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(0, skynet_scanner.scan_skylink_timer, skylink)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log an error if timer failed to create", function()
|
|
||||||
stub(ngx, "log")
|
|
||||||
|
|
||||||
ngx.timer.at.invokes(function() return false, "such a failure" end)
|
|
||||||
|
|
||||||
skynet_scanner.scan_skylink(skylink)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(0, skynet_scanner.scan_skylink_timer, skylink)
|
|
||||||
assert.stub(ngx.log).was_called_with(ngx.ERR, "Failed to create timer: ", "such a failure")
|
|
||||||
|
|
||||||
ngx.log:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should not schedule a timer if skylink is not provided", function()
|
|
||||||
skynet_scanner.scan_skylink()
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_not_called()
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("scan_skylink_timer", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(ngx, "log")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
|
|
||||||
ngx.log:revert()
|
|
||||||
resty_http.new:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should exit early on premature", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new()
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_scanner.scan_skylink_timer(true, skylink)
|
|
||||||
|
|
||||||
assert.stub(request_uri).was_not_called()
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should make a post request with skylink to scanner service", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 200 } -- return 200 success
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_scanner.scan_skylink_timer(false, skylink)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.101:4000/scan/" .. skylink
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST" })
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log message on scanner request failure with response code", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 404, body = "baz" } -- return 404 failure
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_scanner.scan_skylink_timer(false, skylink)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.101:4000/scan/" .. skylink
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST" })
|
|
||||||
assert.stub(ngx.log).was_called_with(
|
|
||||||
ngx.ERR,
|
|
||||||
"Failed malware-scanner request /scan/AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA: ",
|
|
||||||
"[HTTP 404] baz"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log message on scanner request error", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return nil, "foo != bar" -- return error
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_scanner.scan_skylink_timer(false, skylink)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.101:4000/scan/" .. skylink
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST" })
|
|
||||||
assert.stub(ngx.log).was_called_with(
|
|
||||||
ngx.ERR,
|
|
||||||
"Failed malware-scanner request /scan/AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA: ",
|
|
||||||
"foo != bar"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
end)
|
|
|
@ -1,40 +0,0 @@
|
||||||
local _M = {}
|
|
||||||
|
|
||||||
local basexx = require("basexx")
|
|
||||||
local hasher = require("hasher")
|
|
||||||
|
|
||||||
-- parse any skylink and return base64 version
|
|
||||||
function _M.parse(skylink)
|
|
||||||
if string.len(skylink) == 55 then
|
|
||||||
local decoded = basexx.from_basexx(string.upper(skylink), "0123456789ABCDEFGHIJKLMNOPQRSTUV", 5)
|
|
||||||
|
|
||||||
return basexx.to_url64(decoded)
|
|
||||||
end
|
|
||||||
|
|
||||||
return skylink
|
|
||||||
end
|
|
||||||
|
|
||||||
-- hash skylink into 32 bytes hash used in blocklist
|
|
||||||
function _M.hash(skylink)
|
|
||||||
-- ensure that the skylink is base64 encoded
|
|
||||||
local base64Skylink = _M.parse(skylink)
|
|
||||||
|
|
||||||
-- decode skylink from base64 encoding
|
|
||||||
local rawSkylink = basexx.from_url64(base64Skylink)
|
|
||||||
|
|
||||||
-- drop first two bytes and leave just merkle root
|
|
||||||
local rawMerkleRoot = string.sub(rawSkylink, 3)
|
|
||||||
|
|
||||||
-- parse with blake2b with key length of 32
|
|
||||||
local blake2bHashed = hasher.blake2b(rawMerkleRoot, 32)
|
|
||||||
|
|
||||||
-- hex encode the blake hash
|
|
||||||
local hexHashed = basexx.to_hex(blake2bHashed)
|
|
||||||
|
|
||||||
-- lowercase the hex encoded hash
|
|
||||||
local lowerHexHashed = string.lower(hexHashed)
|
|
||||||
|
|
||||||
return lowerHexHashed
|
|
||||||
end
|
|
||||||
|
|
||||||
return _M
|
|
|
@ -1,23 +0,0 @@
|
||||||
local skynet_skylink = require("skynet.skylink")
|
|
||||||
|
|
||||||
describe("parse", function()
|
|
||||||
local base32 = "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30"
|
|
||||||
local base64 = "AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA"
|
|
||||||
|
|
||||||
it("should return unchanged base64 skylink", function()
|
|
||||||
assert.is.same(skynet_skylink.parse(base64), base64)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should transform base32 skylink into base64", function()
|
|
||||||
assert.is.same(skynet_skylink.parse(base32), base64)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("hash", function()
|
|
||||||
local base64 = "EADi4QZWt87sSDCSjVTcmyI5tE_YAsuC90BcCi_jEmG5NA"
|
|
||||||
local hash = "6cfb9996ad74e5614bbb8e7228e72f1c1bc14dd9ce8a83b3ccabdb6d8d70f330"
|
|
||||||
|
|
||||||
it("should hash skylink", function()
|
|
||||||
assert.is.same(hash, skynet_skylink.hash(base64))
|
|
||||||
end)
|
|
||||||
end)
|
|
|
@ -1,99 +0,0 @@
|
||||||
local _M = {}
|
|
||||||
|
|
||||||
local utils = require("utils")
|
|
||||||
|
|
||||||
function _M.track_download_timer(premature, skylink, status, auth_headers, body_bytes_sent)
|
|
||||||
if premature then return end
|
|
||||||
|
|
||||||
local httpc = require("resty.http").new()
|
|
||||||
local query = table.concat({ "status=" .. status, "bytes=" .. body_bytes_sent }, "&")
|
|
||||||
|
|
||||||
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
|
|
||||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/track/download/" .. skylink .. "?" .. query, {
|
|
||||||
method = "POST",
|
|
||||||
headers = auth_headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err or (res and res.status ~= 204) then
|
|
||||||
local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
|
|
||||||
ngx.log(ngx.ERR, "Failed accounts service request /track/download/" .. skylink .. ": ", error_response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.track_download(skylink, status_code, auth_headers, body_bytes_sent)
|
|
||||||
local has_auth_headers = not utils.is_table_empty(auth_headers)
|
|
||||||
local status_success = status_code >= 200 and status_code <= 299
|
|
||||||
|
|
||||||
if skylink and status_success and has_auth_headers then
|
|
||||||
local ok, err = ngx.timer.at(0, _M.track_download_timer, skylink, status_code, auth_headers, body_bytes_sent)
|
|
||||||
if not ok then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.track_upload_timer(premature, skylink, auth_headers, uploader_ip)
|
|
||||||
if premature then return end
|
|
||||||
|
|
||||||
local httpc = require("resty.http").new()
|
|
||||||
|
|
||||||
-- set correct content type header and include auth headers
|
|
||||||
local headers = {
|
|
||||||
["Content-Type"] = "application/x-www-form-urlencoded",
|
|
||||||
}
|
|
||||||
for key, value in pairs(auth_headers) do
|
|
||||||
headers[key] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
|
|
||||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/track/upload/" .. skylink, {
|
|
||||||
method = "POST",
|
|
||||||
headers = headers,
|
|
||||||
body = "ip=" .. uploader_ip,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err or (res and res.status ~= 204) then
|
|
||||||
local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
|
|
||||||
ngx.log(ngx.ERR, "Failed accounts service request /track/upload/" .. skylink .. ": ", error_response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.track_upload(skylink, status_code, auth_headers, uploader_ip)
|
|
||||||
local status_success = status_code >= 200 and status_code <= 299
|
|
||||||
|
|
||||||
if skylink and status_success then
|
|
||||||
local ok, err = ngx.timer.at(0, _M.track_upload_timer, skylink, auth_headers, uploader_ip)
|
|
||||||
if not ok then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.track_registry_timer(premature, auth_headers, request_method)
|
|
||||||
if premature then return end
|
|
||||||
|
|
||||||
local httpc = require("resty.http").new()
|
|
||||||
|
|
||||||
-- based on request method we assign a registry action string used
|
|
||||||
-- in track endpoint namely "read" for GET and "write" for POST
|
|
||||||
local registry_action = request_method == "GET" and "read" or "write"
|
|
||||||
|
|
||||||
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
|
|
||||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/track/registry/" .. registry_action, {
|
|
||||||
method = "POST",
|
|
||||||
headers = auth_headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err or (res and res.status ~= 204) then
|
|
||||||
local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
|
|
||||||
ngx.log(ngx.ERR, "Failed accounts service request /track/registry/" .. registry_action .. ": ", error_response)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function _M.track_registry(status_code, auth_headers, request_method)
|
|
||||||
local has_auth_headers = not utils.is_table_empty(auth_headers)
|
|
||||||
local tracked_status = status_code == 200 or status_code == 404
|
|
||||||
|
|
||||||
if tracked_status and has_auth_headers then
|
|
||||||
local ok, err = ngx.timer.at(0, _M.track_registry_timer, auth_headers, request_method)
|
|
||||||
if not ok then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return _M
|
|
|
@ -1,584 +0,0 @@
|
||||||
-- luacheck: ignore ngx
|
|
||||||
|
|
||||||
local skynet_tracker = require("skynet.tracker")
|
|
||||||
|
|
||||||
local valid_skylink = "AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA"
|
|
||||||
local valid_status_code = 200
|
|
||||||
local valid_auth_headers = { ["Skynet-Api-Key"] = "foo" }
|
|
||||||
local valid_ip = "12.34.56.78"
|
|
||||||
|
|
||||||
describe("track_download", function()
|
|
||||||
local valid_body_bytes_sent = 12345
|
|
||||||
|
|
||||||
before_each(function()
|
|
||||||
stub(ngx.timer, "at")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
ngx.timer.at:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should schedule a timer when conditions are met", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_download(valid_skylink, valid_status_code, valid_auth_headers, valid_body_bytes_sent)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(
|
|
||||||
0,
|
|
||||||
skynet_tracker.track_download_timer,
|
|
||||||
valid_skylink,
|
|
||||||
valid_status_code,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_body_bytes_sent
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should not schedule a timer if skylink is empty", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_download(nil, valid_status_code, valid_auth_headers, valid_body_bytes_sent)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should not schedule a timer if status code is not in 2XX range", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
-- couple of example of 4XX and 5XX codes
|
|
||||||
skynet_tracker.track_download(valid_skylink, 401, valid_auth_headers, valid_body_bytes_sent)
|
|
||||||
skynet_tracker.track_download(valid_skylink, 403, valid_auth_headers, valid_body_bytes_sent)
|
|
||||||
skynet_tracker.track_download(valid_skylink, 490, valid_auth_headers, valid_body_bytes_sent)
|
|
||||||
skynet_tracker.track_download(valid_skylink, 500, valid_auth_headers, valid_body_bytes_sent)
|
|
||||||
skynet_tracker.track_download(valid_skylink, 502, valid_auth_headers, valid_body_bytes_sent)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should not schedule a timer if auth headers are empty", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_download(valid_skylink, valid_status_code, {}, valid_body_bytes_sent)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log an error if timer failed to create", function()
|
|
||||||
stub(ngx, "log")
|
|
||||||
ngx.timer.at.invokes(function() return false, "such a failure" end)
|
|
||||||
|
|
||||||
skynet_tracker.track_download(valid_skylink, valid_status_code, valid_auth_headers, valid_body_bytes_sent)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(
|
|
||||||
0,
|
|
||||||
skynet_tracker.track_download_timer,
|
|
||||||
valid_skylink,
|
|
||||||
valid_status_code,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_body_bytes_sent
|
|
||||||
)
|
|
||||||
assert.stub(ngx.log).was_called_with(ngx.ERR, "Failed to create timer: ", "such a failure")
|
|
||||||
|
|
||||||
ngx.log:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("track_download_timer", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(ngx, "log")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
|
|
||||||
ngx.log:revert()
|
|
||||||
resty_http.new:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should exit early on premature", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 200 } -- return 200 success
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_download_timer(
|
|
||||||
true,
|
|
||||||
valid_skylink,
|
|
||||||
valid_status_code,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_body_bytes_sent
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.stub(request_uri).was_not_called()
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should make a post request to tracker service", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 204 } -- return 204 success
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_download_timer(
|
|
||||||
false,
|
|
||||||
valid_skylink,
|
|
||||||
valid_status_code,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_body_bytes_sent
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri_params = "status=" .. valid_status_code .. "&bytes=" .. valid_body_bytes_sent
|
|
||||||
local uri = "http://10.10.10.70:3000/track/download/" .. valid_skylink .. "?" .. uri_params
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log message on tracker request failure with response code", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 404, body = "baz" } -- return 404 failure
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_download_timer(
|
|
||||||
false,
|
|
||||||
valid_skylink,
|
|
||||||
valid_status_code,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_body_bytes_sent
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri_params = "status=" .. valid_status_code .. "&bytes=" .. valid_body_bytes_sent
|
|
||||||
local uri = "http://10.10.10.70:3000/track/download/" .. valid_skylink .. "?" .. uri_params
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
|
|
||||||
assert.stub(ngx.log).was_called_with(
|
|
||||||
ngx.ERR,
|
|
||||||
"Failed accounts service request /track/download/" .. valid_skylink .. ": ",
|
|
||||||
"[HTTP 404] baz"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log message on tracker request error", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return nil, "foo != bar" -- return error
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_download_timer(
|
|
||||||
false,
|
|
||||||
valid_skylink,
|
|
||||||
valid_status_code,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_body_bytes_sent
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri_params = "status=" .. valid_status_code .. "&bytes=" .. valid_body_bytes_sent
|
|
||||||
local uri = "http://10.10.10.70:3000/track/download/" .. valid_skylink .. "?" .. uri_params
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
|
|
||||||
assert.stub(ngx.log).was_called_with(
|
|
||||||
ngx.ERR,
|
|
||||||
"Failed accounts service request /track/download/" .. valid_skylink .. ": ",
|
|
||||||
"foo != bar"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("track_upload", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(ngx.timer, "at")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
ngx.timer.at:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should schedule a timer when conditions are met", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_upload(valid_skylink, valid_status_code, valid_auth_headers, valid_ip)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(
|
|
||||||
0,
|
|
||||||
skynet_tracker.track_upload_timer,
|
|
||||||
valid_skylink,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_ip
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should not schedule a timer if skylink is empty", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_upload(nil, valid_status_code, valid_auth_headers, valid_ip)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should not schedule a timer if status code is not in 2XX range", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
-- couple of example of 4XX and 5XX codes
|
|
||||||
skynet_tracker.track_upload(valid_skylink, 401, valid_auth_headers, valid_ip)
|
|
||||||
skynet_tracker.track_upload(valid_skylink, 403, valid_auth_headers, valid_ip)
|
|
||||||
skynet_tracker.track_upload(valid_skylink, 490, valid_auth_headers, valid_ip)
|
|
||||||
skynet_tracker.track_upload(valid_skylink, 500, valid_auth_headers, valid_ip)
|
|
||||||
skynet_tracker.track_upload(valid_skylink, 502, valid_auth_headers, valid_ip)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should schedule a timer if auth headers are empty", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_upload(valid_skylink, valid_status_code, {}, valid_ip)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(
|
|
||||||
0,
|
|
||||||
skynet_tracker.track_upload_timer,
|
|
||||||
valid_skylink,
|
|
||||||
{},
|
|
||||||
valid_ip
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log an error if timer failed to create", function()
|
|
||||||
stub(ngx, "log")
|
|
||||||
ngx.timer.at.invokes(function() return false, "such a failure" end)
|
|
||||||
|
|
||||||
skynet_tracker.track_upload(valid_skylink, valid_status_code, valid_auth_headers, valid_ip)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(
|
|
||||||
0,
|
|
||||||
skynet_tracker.track_upload_timer,
|
|
||||||
valid_skylink,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_ip
|
|
||||||
)
|
|
||||||
assert.stub(ngx.log).was_called_with(ngx.ERR, "Failed to create timer: ", "such a failure")
|
|
||||||
|
|
||||||
ngx.log:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("track_upload_timer", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(ngx, "log")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
|
|
||||||
ngx.log:revert()
|
|
||||||
resty_http.new:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should exit early on premature", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 200 } -- return 200 success
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_upload_timer(
|
|
||||||
true,
|
|
||||||
valid_skylink,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_ip
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.stub(request_uri).was_not_called()
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should make a post request to tracker service", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 204 } -- return 204 success
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_upload_timer(
|
|
||||||
false,
|
|
||||||
valid_skylink,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_ip
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, {
|
|
||||||
method = "POST",
|
|
||||||
headers = {
|
|
||||||
["Content-Type"] = "application/x-www-form-urlencoded",
|
|
||||||
["Skynet-Api-Key"] = "foo",
|
|
||||||
},
|
|
||||||
body = "ip=" .. valid_ip
|
|
||||||
})
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log message on tracker request failure with response code", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 404, body = "baz" } -- return 404 failure
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_upload_timer(
|
|
||||||
false,
|
|
||||||
valid_skylink,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_ip
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, {
|
|
||||||
method = "POST",
|
|
||||||
headers = {
|
|
||||||
["Content-Type"] = "application/x-www-form-urlencoded",
|
|
||||||
["Skynet-Api-Key"] = "foo",
|
|
||||||
},
|
|
||||||
body = "ip=" .. valid_ip
|
|
||||||
})
|
|
||||||
assert.stub(ngx.log).was_called_with(
|
|
||||||
ngx.ERR,
|
|
||||||
"Failed accounts service request /track/upload/" .. valid_skylink .. ": ",
|
|
||||||
"[HTTP 404] baz"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log message on tracker request error", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return nil, "foo != bar" -- return error
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_upload_timer(
|
|
||||||
false,
|
|
||||||
valid_skylink,
|
|
||||||
valid_auth_headers,
|
|
||||||
valid_ip
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, {
|
|
||||||
method = "POST",
|
|
||||||
headers = {
|
|
||||||
["Content-Type"] = "application/x-www-form-urlencoded",
|
|
||||||
["Skynet-Api-Key"] = "foo",
|
|
||||||
},
|
|
||||||
body = "ip=" .. valid_ip
|
|
||||||
})
|
|
||||||
assert.stub(ngx.log).was_called_with(
|
|
||||||
ngx.ERR,
|
|
||||||
"Failed accounts service request /track/upload/" .. valid_skylink .. ": ",
|
|
||||||
"foo != bar"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("track_registry", function()
|
|
||||||
local status_code_ok = 200
|
|
||||||
local status_code_not_found = 404
|
|
||||||
local request_method_write = "POST"
|
|
||||||
local request_method_read = "GET"
|
|
||||||
|
|
||||||
before_each(function()
|
|
||||||
stub(ngx.timer, "at")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
ngx.timer.at:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should schedule a timer when status code was 200", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry(status_code_ok, valid_auth_headers, request_method_write)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(
|
|
||||||
0,
|
|
||||||
skynet_tracker.track_registry_timer,
|
|
||||||
valid_auth_headers,
|
|
||||||
request_method_write
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should schedule a timer when status code was 404", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry(status_code_not_found, valid_auth_headers, request_method_write)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(
|
|
||||||
0,
|
|
||||||
skynet_tracker.track_registry_timer,
|
|
||||||
valid_auth_headers,
|
|
||||||
request_method_write
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should not schedule a timer if status code is not in 200 or 404", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
-- couple of example of invalid 2XX, 4XX and 5XX codes
|
|
||||||
skynet_tracker.track_registry(204, valid_auth_headers, request_method_write)
|
|
||||||
skynet_tracker.track_registry(206, valid_auth_headers, request_method_write)
|
|
||||||
skynet_tracker.track_registry(401, valid_auth_headers, request_method_write)
|
|
||||||
skynet_tracker.track_registry(403, valid_auth_headers, request_method_write)
|
|
||||||
skynet_tracker.track_registry(490, valid_auth_headers, request_method_write)
|
|
||||||
skynet_tracker.track_registry(500, valid_auth_headers, request_method_write)
|
|
||||||
skynet_tracker.track_registry(502, valid_auth_headers, request_method_write)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should not schedule a timer if auth headers are empty", function()
|
|
||||||
ngx.timer.at.invokes(function() return true, nil end)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry(status_code_ok, {}, request_method_write)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log an error if timer failed to create", function()
|
|
||||||
stub(ngx, "log")
|
|
||||||
ngx.timer.at.invokes(function() return false, "such a failure" end)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry(status_code_ok, valid_auth_headers, request_method_write)
|
|
||||||
|
|
||||||
assert.stub(ngx.timer.at).was_called_with(
|
|
||||||
0,
|
|
||||||
skynet_tracker.track_registry_timer,
|
|
||||||
valid_auth_headers,
|
|
||||||
request_method_write
|
|
||||||
)
|
|
||||||
assert.stub(ngx.log).was_called_with(ngx.ERR, "Failed to create timer: ", "such a failure")
|
|
||||||
|
|
||||||
ngx.log:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("track_registry_timer", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(ngx, "log")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
|
|
||||||
ngx.log:revert()
|
|
||||||
resty_http.new:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should exit early on premature", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 200 } -- return 200 success
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry_timer(
|
|
||||||
true,
|
|
||||||
valid_auth_headers,
|
|
||||||
request_method_write
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.stub(request_uri).was_not_called()
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should make a post request to registry write tracker service", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 204 } -- return 204 success
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry_timer(
|
|
||||||
false,
|
|
||||||
valid_auth_headers,
|
|
||||||
request_method_write
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.70:3000/track/registry/write"
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should make a post request to registry read tracker service", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 204 } -- return 204 success
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry_timer(
|
|
||||||
false,
|
|
||||||
valid_auth_headers,
|
|
||||||
request_method_read
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.70:3000/track/registry/read"
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
|
|
||||||
assert.stub(ngx.log).was_not_called()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log message on tracker request failure with response code", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return { status = 404, body = "baz" } -- return 404 failure
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry_timer(
|
|
||||||
false,
|
|
||||||
valid_auth_headers,
|
|
||||||
request_method_write
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.70:3000/track/registry/write"
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
|
|
||||||
assert.stub(ngx.log).was_called_with(
|
|
||||||
ngx.ERR,
|
|
||||||
"Failed accounts service request /track/registry/write: ",
|
|
||||||
"[HTTP 404] baz"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should log message on tracker request error", function()
|
|
||||||
local resty_http = require("resty.http")
|
|
||||||
local request_uri = spy.new(function()
|
|
||||||
return nil, "foo != bar" -- return error
|
|
||||||
end)
|
|
||||||
local httpc = mock({ request_uri = request_uri })
|
|
||||||
stub(resty_http, "new").returns(httpc)
|
|
||||||
|
|
||||||
skynet_tracker.track_registry_timer(
|
|
||||||
false,
|
|
||||||
valid_auth_headers,
|
|
||||||
request_method_write
|
|
||||||
)
|
|
||||||
|
|
||||||
local uri = "http://10.10.10.70:3000/track/registry/write"
|
|
||||||
assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
|
|
||||||
assert.stub(ngx.log).was_called_with(
|
|
||||||
ngx.ERR,
|
|
||||||
"Failed accounts service request /track/registry/write: ",
|
|
||||||
"foo != bar"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
|
@ -1,30 +0,0 @@
|
||||||
local _M = {}
|
|
||||||
|
|
||||||
local ngx_base64 = require("ngx.base64")
|
|
||||||
local utils = require("utils")
|
|
||||||
|
|
||||||
function _M.authorization_header()
|
|
||||||
-- read api password from env variable
|
|
||||||
local apipassword = utils.getenv("SIA_API_PASSWORD")
|
|
||||||
-- if api password is not available as env variable, read it from disk
|
|
||||||
if not apipassword then
|
|
||||||
-- open apipassword file for reading (b flag is required for some reason)
|
|
||||||
-- (file /etc/.sia/apipassword has to be mounted from the host system)
|
|
||||||
local apipassword_file = io.open("/data/sia/apipassword", "rb")
|
|
||||||
-- make sure to throw a meaningful error if apipassword file does not exist
|
|
||||||
if not apipassword_file then
|
|
||||||
error("Error reading /data/sia/apipassword file")
|
|
||||||
end
|
|
||||||
-- read apipassword file contents and trim newline (important)
|
|
||||||
apipassword = apipassword_file:read("*all"):gsub("%s+", "")
|
|
||||||
-- make sure to close file after reading the password
|
|
||||||
apipassword_file.close()
|
|
||||||
end
|
|
||||||
-- encode the user:password authorization string
|
|
||||||
-- (in our case user is empty so it is just :password)
|
|
||||||
local content = ngx_base64.encode_base64url(":" .. apipassword)
|
|
||||||
-- set authorization header with proper base64 encoded string
|
|
||||||
return "Basic " .. content
|
|
||||||
end
|
|
||||||
|
|
||||||
return _M
|
|
|
@ -1,65 +0,0 @@
|
||||||
-- luacheck: ignore io
|
|
||||||
|
|
||||||
local utils = require('utils')
|
|
||||||
local skynet_utils = require('skynet.utils')
|
|
||||||
|
|
||||||
describe("authorization_header", function()
|
|
||||||
local apipassword = "ddd0c1430fbf2708"
|
|
||||||
local expected_header = "Basic OmRkZDBjMTQzMGZiZjI3MDg"
|
|
||||||
|
|
||||||
it("reads SIA_API_PASSWORD from env variable and returns a header", function()
|
|
||||||
-- stub getenv on SIA_API_PASSWORD
|
|
||||||
stub(utils, "getenv")
|
|
||||||
utils.getenv.on_call_with("SIA_API_PASSWORD").returns(apipassword)
|
|
||||||
|
|
||||||
local header = skynet_utils.authorization_header()
|
|
||||||
|
|
||||||
assert.is_equal(header, expected_header)
|
|
||||||
|
|
||||||
-- revert stub to original function
|
|
||||||
utils.getenv:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("uses /data/sia/apipassword file if SIA_API_PASSWORD env var is missing", function()
|
|
||||||
-- stub /data/sia/apipassword file
|
|
||||||
stub(io, "open")
|
|
||||||
io.open.on_call_with("/data/sia/apipassword", "rb").returns(mock({
|
|
||||||
read = spy.new(function() return apipassword end),
|
|
||||||
close = spy.new()
|
|
||||||
}))
|
|
||||||
|
|
||||||
local header = skynet_utils.authorization_header()
|
|
||||||
|
|
||||||
assert.is_equal(header, expected_header)
|
|
||||||
|
|
||||||
-- revert stub to original function
|
|
||||||
io.open:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should choose env variable over file if both are available", function()
|
|
||||||
-- stub getenv on SIA_API_PASSWORD
|
|
||||||
stub(utils, "getenv")
|
|
||||||
utils.getenv.on_call_with("SIA_API_PASSWORD").returns(apipassword)
|
|
||||||
|
|
||||||
-- stub /data/sia/apipassword file
|
|
||||||
stub(io, "open")
|
|
||||||
io.open.on_call_with("/data/sia/apipassword", "rb").returns(mock({
|
|
||||||
read = spy.new(function() return "foooooooooooooo" end),
|
|
||||||
close = spy.new()
|
|
||||||
}))
|
|
||||||
|
|
||||||
local header = skynet_utils.authorization_header()
|
|
||||||
|
|
||||||
assert.is_equal(header, "Basic OmRkZDBjMTQzMGZiZjI3MDg")
|
|
||||||
|
|
||||||
-- revert stubs to original function
|
|
||||||
utils.getenv:revert()
|
|
||||||
io.open:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should error out if neither env variable is available nor file exists", function()
|
|
||||||
assert.has_error(function()
|
|
||||||
skynet_utils.authorization_header()
|
|
||||||
end, "Error reading /data/sia/apipassword file")
|
|
||||||
end)
|
|
||||||
end)
|
|
|
@ -1,83 +0,0 @@
|
||||||
local _M = {}
|
|
||||||
|
|
||||||
-- utility function for checking if table is empty
|
|
||||||
function _M.is_table_empty(check)
|
|
||||||
-- bind next to local variable to achieve ultimate efficiency
|
|
||||||
-- https://stackoverflow.com/a/1252776
|
|
||||||
local next = next
|
|
||||||
|
|
||||||
return next(check) == nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- extract full cookie name and value by its name from cookie string
|
|
||||||
-- note: name matcher argument is a pattern so you will need to escape
|
|
||||||
-- any special characters, read more https://www.lua.org/pil/20.2.html
|
|
||||||
function _M.extract_cookie(cookie_string, name_matcher)
|
|
||||||
-- nil cookie string safeguard
|
|
||||||
if cookie_string == nil then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local start, stop = string.find(cookie_string, name_matcher .. "=[^;]+")
|
|
||||||
|
|
||||||
if start then
|
|
||||||
return string.sub(cookie_string, start, stop)
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- extract just the cookie value by its name from cookie string
|
|
||||||
-- note: name matcher argument is a pattern so you will need to escape
|
|
||||||
-- any special characters, read more https://www.lua.org/pil/20.2.html
|
|
||||||
function _M.extract_cookie_value(cookie_string, name_matcher)
|
|
||||||
local cookie = _M.extract_cookie(cookie_string, name_matcher)
|
|
||||||
|
|
||||||
if cookie == nil then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local value_start = string.find(cookie, "=") + 1
|
|
||||||
|
|
||||||
return string.sub(cookie, value_start)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- utility function that builds on os.getenv to get environment variable value
|
|
||||||
-- * will always return nil for both unset and empty env vars
|
|
||||||
-- * parse "boolean": "true" and "1" as true, "false" and "0" as false, throws for others
|
|
||||||
-- * parse "integer": any numerical string gets converted, otherwise returns nil
|
|
||||||
function _M.getenv(name, parse)
|
|
||||||
local value = os.getenv(name)
|
|
||||||
|
|
||||||
-- treat empty string value as nil to simplify comparisons
|
|
||||||
if value == nil or value == "" then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- do not parse the value
|
|
||||||
if parse == nil then
|
|
||||||
return value
|
|
||||||
end
|
|
||||||
|
|
||||||
-- try to parse as boolean
|
|
||||||
if parse == "boolean" then
|
|
||||||
if string.lower(value) == "true" or value == "1" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
if string.lower(value) == "false" or value == "0" then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
error("utils.getenv: Parsing value '" .. tostring(value) .. "' to boolean is not supported")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- try to parse as integer
|
|
||||||
if parse == "integer" then
|
|
||||||
return tonumber(value)
|
|
||||||
end
|
|
||||||
|
|
||||||
error("utils.getenv: Parsing to '" .. parse .. "' is not supported")
|
|
||||||
end
|
|
||||||
|
|
||||||
return _M
|
|
|
@ -1,215 +0,0 @@
|
||||||
-- luacheck: ignore os
|
|
||||||
|
|
||||||
local utils = require('utils')
|
|
||||||
|
|
||||||
describe("is_table_empty", function()
|
|
||||||
it("should return true for empty table", function()
|
|
||||||
assert.is_true(utils.is_table_empty({}))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return false for not empty table", function()
|
|
||||||
assert.is_false(utils.is_table_empty({ ["foo"] = "bar" }))
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("extract_cookie", function()
|
|
||||||
local cookie_string = "aaa=bbb; skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==; xxx=yyy"
|
|
||||||
|
|
||||||
it("should return nil if cookie string is nil", function()
|
|
||||||
local cookie = utils.extract_cookie_value(nil, "aaa")
|
|
||||||
|
|
||||||
assert.is_nil(cookie)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return nil if cookie name is not found", function()
|
|
||||||
local cookie = utils.extract_cookie(cookie_string, "foo")
|
|
||||||
|
|
||||||
assert.is_nil(cookie)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return cookie if cookie_string starts with that cookie name", function()
|
|
||||||
local cookie = utils.extract_cookie(cookie_string, "aaa")
|
|
||||||
|
|
||||||
assert.are.equals(cookie, "aaa=bbb")
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return cookie if cookie_string ends with that cookie name", function()
|
|
||||||
local cookie = utils.extract_cookie(cookie_string, "xxx")
|
|
||||||
|
|
||||||
assert.are.equals(cookie, "xxx=yyy")
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return cookie with custom matcher", function()
|
|
||||||
local cookie = utils.extract_cookie(cookie_string, "skynet[-]jwt")
|
|
||||||
|
|
||||||
assert.are.equals(cookie, "skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("extract_cookie_value", function()
|
|
||||||
local cookie_string = "aaa=bbb; skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==; xxx=yyy"
|
|
||||||
|
|
||||||
it("should return nil if cookie string is nil", function()
|
|
||||||
local value = utils.extract_cookie_value(nil, "aaa")
|
|
||||||
|
|
||||||
assert.is_nil(value)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return nil if cookie name is not found", function()
|
|
||||||
local value = utils.extract_cookie_value(cookie_string, "foo")
|
|
||||||
|
|
||||||
assert.is_nil(value)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return value if cookie_string starts with that cookie name", function()
|
|
||||||
local value = utils.extract_cookie_value(cookie_string, "aaa")
|
|
||||||
|
|
||||||
assert.are.equals(value, "bbb")
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return cookie if cookie_string ends with that cookie name", function()
|
|
||||||
local value = utils.extract_cookie_value(cookie_string, "xxx")
|
|
||||||
|
|
||||||
assert.are.equals(value, "yyy")
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return cookie with custom matcher", function()
|
|
||||||
local value = utils.extract_cookie_value(cookie_string, "skynet[-]jwt")
|
|
||||||
|
|
||||||
assert.are.equals(value, "MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("getenv", function()
|
|
||||||
before_each(function()
|
|
||||||
stub(os, "getenv")
|
|
||||||
end)
|
|
||||||
|
|
||||||
after_each(function()
|
|
||||||
os.getenv:revert()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return nil for not existing env var", function()
|
|
||||||
os.getenv.on_call_with("foo").returns(nil)
|
|
||||||
|
|
||||||
assert.is_nil(utils.getenv("foo"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return nil for env var that is an empty string", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("")
|
|
||||||
|
|
||||||
assert.is_nil(utils.getenv("foo"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return the value as is when it is non empty string", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("bar")
|
|
||||||
|
|
||||||
assert.is_equal(utils.getenv("foo"), "bar")
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("parse", function()
|
|
||||||
it("should throw on not supported parser", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("test")
|
|
||||||
|
|
||||||
assert.has_error(function()
|
|
||||||
utils.getenv("foo", "starwars")
|
|
||||||
end, "utils.getenv: Parsing to 'starwars' is not supported")
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("as boolean", function()
|
|
||||||
it("should return nil for not existing env var", function()
|
|
||||||
os.getenv.on_call_with("foo").returns(nil)
|
|
||||||
|
|
||||||
assert.is_nil(utils.getenv("foo", "boolean"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return nil for env var that is an empty string", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("")
|
|
||||||
|
|
||||||
assert.is_nil(utils.getenv("foo", "boolean"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse 'true' string as true", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("true")
|
|
||||||
|
|
||||||
assert.is_true(utils.getenv("foo", "boolean"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse 'True' string as true", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("True")
|
|
||||||
|
|
||||||
assert.is_true(utils.getenv("foo", "boolean"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse '1' string as true", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("1")
|
|
||||||
|
|
||||||
assert.is_true(utils.getenv("foo", "boolean"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse 'false' string as false", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("false")
|
|
||||||
|
|
||||||
assert.is_false(utils.getenv("foo", "boolean"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse 'False' string as false", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("False")
|
|
||||||
|
|
||||||
assert.is_false(utils.getenv("foo", "boolean"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse '0' string as false", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("0")
|
|
||||||
|
|
||||||
assert.is_false(utils.getenv("foo", "boolean"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should throw an error for not supported string", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("mandalorian")
|
|
||||||
|
|
||||||
assert.has_error(function()
|
|
||||||
utils.getenv("foo", "boolean")
|
|
||||||
end, "utils.getenv: Parsing value 'mandalorian' to boolean is not supported")
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("as integer", function()
|
|
||||||
it("should return nil for not existing env var", function()
|
|
||||||
os.getenv.on_call_with("foo").returns(nil)
|
|
||||||
|
|
||||||
assert.is_nil(utils.getenv("foo", "integer"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return nil for env var that is an empty string", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("")
|
|
||||||
|
|
||||||
assert.is_nil(utils.getenv("foo", "integer"))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse '0' string as 0", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("0")
|
|
||||||
|
|
||||||
assert.equals(utils.getenv("foo", "integer"), 0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse '1' string as 1", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("1")
|
|
||||||
|
|
||||||
assert.equals(utils.getenv("foo", "integer"), 1)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should parse '-1' string as -1", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("-1")
|
|
||||||
|
|
||||||
assert.equals(utils.getenv("foo", "integer"), -1)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should return nil for non numerical string", function()
|
|
||||||
os.getenv.on_call_with("foo").returns("test")
|
|
||||||
|
|
||||||
assert.is_nil(utils.getenv("foo", "integer"))
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
|
@ -1,125 +0,0 @@
|
||||||
# nginx.conf -- docker-openresty
|
|
||||||
#
|
|
||||||
# This file is installed to:
|
|
||||||
# `/usr/local/openresty/nginx/conf/nginx.conf`
|
|
||||||
# and is the file loaded by nginx at startup,
|
|
||||||
# unless the user specifies otherwise.
|
|
||||||
#
|
|
||||||
# It tracks the upstream OpenResty's `nginx.conf`, but removes the `server`
|
|
||||||
# section and adds this directive:
|
|
||||||
# `include /etc/nginx/conf.d/*.conf;`
|
|
||||||
#
|
|
||||||
# The `docker-openresty` file `nginx.vh.default.conf` is copied to
|
|
||||||
# `/etc/nginx/conf.d/default.conf`. It contains the `server section
|
|
||||||
# of the upstream `nginx.conf`.
|
|
||||||
#
|
|
||||||
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
|
|
||||||
#
|
|
||||||
|
|
||||||
user root;
|
|
||||||
worker_processes auto;
|
|
||||||
|
|
||||||
# Enables the use of JIT for regular expressions to speed-up their processing.
|
|
||||||
pcre_jit on;
|
|
||||||
|
|
||||||
#error_log logs/error.log;
|
|
||||||
#error_log logs/error.log notice;
|
|
||||||
#error_log logs/error.log info;
|
|
||||||
|
|
||||||
#pid logs/nginx.pid;
|
|
||||||
|
|
||||||
# declare env variables to use it in config
|
|
||||||
env PORTAL_DOMAIN;
|
|
||||||
env SERVER_DOMAIN;
|
|
||||||
env PORTAL_MODULES;
|
|
||||||
env ACCOUNTS_LIMIT_ACCESS;
|
|
||||||
env SIA_API_PASSWORD;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 8192;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
lua_package_path "/etc/nginx/libs/?.lua;;";
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" $upstream_response_time '
|
|
||||||
'$upstream_bytes_sent $upstream_bytes_received '
|
|
||||||
'"$upstream_http_content_type" "$upstream_cache_status" '
|
|
||||||
'"$server_alias" "$sent_http_skynet_skylink" '
|
|
||||||
'$upstream_connect_time $upstream_header_time '
|
|
||||||
'$request_time "$hns_domain" "$skylink" $upstream_http_skynet_cache_ratio';
|
|
||||||
|
|
||||||
access_log logs/access.log main;
|
|
||||||
|
|
||||||
# See Move default writable paths to a dedicated directory (#119)
|
|
||||||
# https://github.com/openresty/docker-openresty/issues/119
|
|
||||||
client_body_temp_path /var/run/openresty/nginx-client-body;
|
|
||||||
proxy_temp_path /var/run/openresty/nginx-proxy;
|
|
||||||
fastcgi_temp_path /var/run/openresty/nginx-fastcgi;
|
|
||||||
uwsgi_temp_path /var/run/openresty/nginx-uwsgi;
|
|
||||||
scgi_temp_path /var/run/openresty/nginx-scgi;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
#tcp_nopush on;
|
|
||||||
|
|
||||||
#keepalive_timeout 0;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
# globally enable http 1.1 on all proxied requests
|
|
||||||
# http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_http_version
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# proxy cache definition
|
|
||||||
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=skynet:10m max_size=50g min_free=100g inactive=48h use_temp_path=off;
|
|
||||||
|
|
||||||
# this runs before forking out nginx worker processes
|
|
||||||
init_by_lua_block {
|
|
||||||
require "cjson"
|
|
||||||
require "resty.http"
|
|
||||||
require "skynet.skylink"
|
|
||||||
require "skynet.utils"
|
|
||||||
}
|
|
||||||
|
|
||||||
# include skynet-portal-api and skynet-server-api header on every request
|
|
||||||
header_filter_by_lua_block {
|
|
||||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
|
||||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
|
||||||
}
|
|
||||||
|
|
||||||
# ratelimit specified IPs
|
|
||||||
geo $limit {
|
|
||||||
default 0;
|
|
||||||
include /etc/nginx/conf.d/include/ratelimited;
|
|
||||||
}
|
|
||||||
|
|
||||||
map $limit $limit_key {
|
|
||||||
0 "";
|
|
||||||
1 $binary_remote_addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
limit_req_zone $binary_remote_addr zone=uploads_by_ip:10m rate=10r/s;
|
|
||||||
limit_req_zone $limit_key zone=uploads_by_ip_throttled:10m rate=10r/m;
|
|
||||||
|
|
||||||
limit_req_zone $binary_remote_addr zone=registry_access_by_ip:10m rate=60r/m;
|
|
||||||
limit_req_zone $limit_key zone=registry_access_by_ip_throttled:10m rate=20r/m;
|
|
||||||
|
|
||||||
limit_conn_zone $binary_remote_addr zone=upload_conn:10m;
|
|
||||||
limit_conn_zone $limit_key zone=upload_conn_rl:10m;
|
|
||||||
|
|
||||||
limit_conn_zone $binary_remote_addr zone=downloads_by_ip:10m;
|
|
||||||
|
|
||||||
limit_req_status 429;
|
|
||||||
limit_conn_status 429;
|
|
||||||
|
|
||||||
# Add X-Forwarded-* headers
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
include /etc/nginx/conf.extra.d/*.conf;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
exclude = {
|
|
||||||
"/usr/local/openresty", -- internal openresty libraries
|
|
||||||
"rbusted", -- busted executable
|
|
||||||
"basexx", -- external library https://github.com/aiq/basexx
|
|
||||||
}
|
|
||||||
includeuntestedfiles = true
|
|
|
@ -1,11 +0,0 @@
|
||||||
FROM openresty/openresty:1.19.9.1-focal
|
|
||||||
|
|
||||||
WORKDIR /etc/nginx
|
|
||||||
|
|
||||||
RUN luarocks install lua-resty-http && \
|
|
||||||
luarocks install hasher && \
|
|
||||||
luarocks install busted
|
|
||||||
|
|
||||||
COPY rbusted /etc/nginx/
|
|
||||||
|
|
||||||
CMD ["/etc/nginx/rbusted", "--verbose", "--pattern=spec", "/usr/local/openresty/site/lualib"]
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Running tests locally
|
|
||||||
|
|
||||||
`docker run -v $(pwd)/docker/nginx/libs:/usr/local/openresty/site/lualib --rm -it $(docker build -q docker/nginx/testing)`
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/usr/bin/env resty
|
|
||||||
|
|
||||||
setmetatable(_G, nil)
|
|
||||||
|
|
||||||
pcall(require, "luarocks.loader")
|
|
||||||
|
|
||||||
-- Busted command-line runner
|
|
||||||
require "busted.runner"({ standalone = false })
|
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules/
|
|
||||||
.cache/
|
|
||||||
public/
|
|
||||||
storybook-build/
|
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
globals: {
|
|
||||||
__PATH_PREFIX__: true,
|
|
||||||
},
|
|
||||||
extends: ["react-app", "plugin:storybook/recommended"],
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules/
|
|
||||||
.cache/
|
|
||||||
public/
|
|
||||||
storybook-build/
|
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules/
|
|
||||||
.cache/
|
|
||||||
public/
|
|
||||||
storybook-build/
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
|
||||||
addons: [
|
|
||||||
"@storybook/addon-links",
|
|
||||||
"@storybook/addon-essentials",
|
|
||||||
"storybook-addon-gatsby",
|
|
||||||
{
|
|
||||||
name: "@storybook/addon-postcss",
|
|
||||||
options: {
|
|
||||||
postcssLoaderOptions: {
|
|
||||||
implementation: require("postcss"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
core: {
|
|
||||||
builder: "webpack5",
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
import "tailwindcss/tailwind.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
|
|
||||||
|
|
||||||
import "../src/styles/global.css";
|
|
||||||
|
|
||||||
export const parameters = {
|
|
||||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layout: "fullscreen",
|
|
||||||
};
|
|
|
@ -1,18 +0,0 @@
|
||||||
FROM node:16.14.2-alpine
|
|
||||||
|
|
||||||
WORKDIR /usr/app
|
|
||||||
|
|
||||||
COPY package.json \
|
|
||||||
yarn.lock \
|
|
||||||
./
|
|
||||||
|
|
||||||
RUN yarn --frozen-lockfile
|
|
||||||
|
|
||||||
COPY static ./static
|
|
||||||
COPY src ./src
|
|
||||||
COPY gatsby*.js \
|
|
||||||
postcss.config.js \
|
|
||||||
tailwind.config.js \
|
|
||||||
./
|
|
||||||
|
|
||||||
CMD ["sh", "-c", "yarn build && yarn serve --host 0.0.0.0 -p 9000"]
|
|
|
@ -1,30 +0,0 @@
|
||||||
# Skynet Account Dashboard
|
|
||||||
|
|
||||||
Code behind [account.skynetpro.net](https://account.skynetpro.net/)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
This is a Gatsby application. To run it locally, all you need is:
|
|
||||||
|
|
||||||
- `yarn install`
|
|
||||||
- `yarn start`
|
|
||||||
|
|
||||||
## Accessing remote APIs
|
|
||||||
|
|
||||||
To have a fully functioning local environment, you'll need to make the browser believe you're actually on the same domain as a working API (i.e. a remote dev or production server) -- otherwise the browser will block the session cookie.
|
|
||||||
To do the trick, configure proper environment variables in the `.env.development` file.
|
|
||||||
This file allows to easily control which domain name you want to use locally and which API you'd like to access.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```env
|
|
||||||
GATSBY_PORTAL_DOMAIN=skynetfree.net # Use skynetfree.net APIs
|
|
||||||
GATSBY_HOST=local.skynetfree.net # Address of your local build
|
|
||||||
```
|
|
||||||
|
|
||||||
> It's recommended to keep the 2LD the same, so any cookies dispatched by the API work without issues.
|
|
||||||
|
|
||||||
With the file configured, run `yarn develop:secure` -- it will run `gatsby develop` with `--https -p=443` options.
|
|
||||||
If you're on macOS, you may need to `sudo` the command to successfully bind to port `443` (https).
|
|
||||||
|
|
||||||
Gatsby will automatically add a proper entry to your `/etc/hosts` file and clean it up when process exits.
|
|
|
@ -1,32 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import { SWRConfig } from "swr";
|
|
||||||
import { Elements } from "@stripe/react-stripe-js";
|
|
||||||
import { loadStripe } from "@stripe/stripe-js";
|
|
||||||
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
|
|
||||||
import "./src/styles/global.css";
|
|
||||||
import swrConfig from "./src/lib/swrConfig";
|
|
||||||
import { MODAL_ROOT_ID } from "./src/components/Modal";
|
|
||||||
import { PortalSettingsProvider } from "./src/contexts/portal-settings";
|
|
||||||
|
|
||||||
const stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY);
|
|
||||||
|
|
||||||
export function wrapPageElement({ element, props }) {
|
|
||||||
const Layout = element.type.Layout ?? React.Fragment;
|
|
||||||
return (
|
|
||||||
<PortalSettingsProvider>
|
|
||||||
<SWRConfig value={swrConfig}>
|
|
||||||
<Elements stripe={stripePromise}>
|
|
||||||
<Layout {...props}>
|
|
||||||
{element}
|
|
||||||
<div id={MODAL_ROOT_ID} />
|
|
||||||
</Layout>
|
|
||||||
</Elements>
|
|
||||||
</SWRConfig>
|
|
||||||
</PortalSettingsProvider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
require("dotenv").config({
|
|
||||||
path: `.env.${process.env.NODE_ENV}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
|
||||||
|
|
||||||
const { GATSBY_PORTAL_DOMAIN } = process.env;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
siteMetadata: {
|
|
||||||
title: `Account Dashboard`,
|
|
||||||
siteUrl: `https://account.${GATSBY_PORTAL_DOMAIN}`,
|
|
||||||
},
|
|
||||||
pathPrefix: "/v2",
|
|
||||||
trailingSlash: "never",
|
|
||||||
plugins: [
|
|
||||||
"gatsby-plugin-image",
|
|
||||||
"gatsby-plugin-provide-react",
|
|
||||||
"gatsby-plugin-react-helmet",
|
|
||||||
"gatsby-plugin-sharp",
|
|
||||||
"gatsby-transformer-sharp",
|
|
||||||
"gatsby-plugin-styled-components",
|
|
||||||
"gatsby-plugin-postcss",
|
|
||||||
{
|
|
||||||
resolve: "gatsby-source-filesystem",
|
|
||||||
options: {
|
|
||||||
name: "images",
|
|
||||||
path: "./static/images/",
|
|
||||||
},
|
|
||||||
__key: "images",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
developMiddleware: (app) => {
|
|
||||||
// Proxy Accounts service API requests:
|
|
||||||
app.use(
|
|
||||||
"/api/",
|
|
||||||
createProxyMiddleware({
|
|
||||||
target: `https://account.${GATSBY_PORTAL_DOMAIN}`,
|
|
||||||
secure: false, // Do not reject self-signed certificates.
|
|
||||||
changeOrigin: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Proxy /skynet requests (e.g. uploads)
|
|
||||||
app.use(
|
|
||||||
["/skynet", "/__internal/"],
|
|
||||||
createProxyMiddleware({
|
|
||||||
target: `https://${GATSBY_PORTAL_DOMAIN}`,
|
|
||||||
secure: false, // Do not reject self-signed certificates.
|
|
||||||
changeOrigin: true,
|
|
||||||
pathRewrite: {
|
|
||||||
"^/skynet": "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import { SWRConfig } from "swr";
|
|
||||||
import { Elements } from "@stripe/react-stripe-js";
|
|
||||||
import { loadStripe } from "@stripe/stripe-js";
|
|
||||||
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
|
|
||||||
import "./src/styles/global.css";
|
|
||||||
import swrConfig from "./src/lib/swrConfig";
|
|
||||||
import { MODAL_ROOT_ID } from "./src/components/Modal";
|
|
||||||
import { PortalSettingsProvider } from "./src/contexts/portal-settings";
|
|
||||||
|
|
||||||
const stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY);
|
|
||||||
|
|
||||||
export function wrapPageElement({ element, props }) {
|
|
||||||
const Layout = element.type.Layout ?? React.Fragment;
|
|
||||||
return (
|
|
||||||
<PortalSettingsProvider>
|
|
||||||
<SWRConfig value={swrConfig}>
|
|
||||||
<Elements stripe={stripePromise}>
|
|
||||||
<Layout {...props}>
|
|
||||||
{element}
|
|
||||||
<div id={MODAL_ROOT_ID} />
|
|
||||||
</Layout>
|
|
||||||
</Elements>
|
|
||||||
</SWRConfig>
|
|
||||||
</PortalSettingsProvider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
{
|
|
||||||
"name": "accounts-dashboard",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"description": "Accounts Dashboard",
|
|
||||||
"author": "Skynet Labs",
|
|
||||||
"keywords": [
|
|
||||||
"gatsby"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"develop": "gatsby develop",
|
|
||||||
"develop:secure": "dotenv -e .env.development -- gatsby develop --https -p=443",
|
|
||||||
"start": "gatsby develop",
|
|
||||||
"build": "gatsby build --prefix-paths",
|
|
||||||
"serve": "gatsby serve --prefix-paths",
|
|
||||||
"clean": "gatsby clean",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"prettier": "prettier .",
|
|
||||||
"storybook": "start-storybook -p 6006",
|
|
||||||
"build-storybook": "build-storybook -o storybook-build"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@fontsource/sora": "^4.5.3",
|
|
||||||
"@fontsource/source-sans-pro": "^4.5.3",
|
|
||||||
"@stripe/react-stripe-js": "^1.7.1",
|
|
||||||
"@stripe/stripe-js": "^1.27.0",
|
|
||||||
"classnames": "^2.3.1",
|
|
||||||
"copy-text-to-clipboard": "^3.0.1",
|
|
||||||
"dayjs": "^1.10.8",
|
|
||||||
"formik": "^2.2.9",
|
|
||||||
"gatsby": "^4.6.2",
|
|
||||||
"gatsby-plugin-postcss": "^5.7.0",
|
|
||||||
"http-status-codes": "^2.2.0",
|
|
||||||
"ky": "^0.30.0",
|
|
||||||
"nanoid": "^3.3.1",
|
|
||||||
"path-browserify": "^1.0.1",
|
|
||||||
"postcss": "^8.4.6",
|
|
||||||
"react": "^17.0.1",
|
|
||||||
"react-dom": "^17.0.1",
|
|
||||||
"react-dropzone": "^12.0.4",
|
|
||||||
"react-helmet": "^6.1.0",
|
|
||||||
"react-use": "^17.3.2",
|
|
||||||
"skynet-js": "4.0.27-beta",
|
|
||||||
"swr": "^1.2.2",
|
|
||||||
"tailwindcss": "^3.0.23",
|
|
||||||
"yup": "^0.32.11"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.17.4",
|
|
||||||
"@storybook/addon-actions": "^6.4.19",
|
|
||||||
"@storybook/addon-essentials": "^6.4.19",
|
|
||||||
"@storybook/addon-interactions": "^6.4.19",
|
|
||||||
"@storybook/addon-links": "^6.4.19",
|
|
||||||
"@storybook/addon-postcss": "^2.0.0",
|
|
||||||
"@storybook/builder-webpack5": "^6.4.19",
|
|
||||||
"@storybook/manager-webpack5": "^6.4.19",
|
|
||||||
"@storybook/react": "^6.4.19",
|
|
||||||
"@storybook/testing-library": "^0.0.9",
|
|
||||||
"autoprefixer": "^10.4.2",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"babel-loader": "^8.2.3",
|
|
||||||
"babel-plugin-preval": "^5.1.0",
|
|
||||||
"babel-plugin-styled-components": "^2.0.2",
|
|
||||||
"dotenv": "^16.0.0",
|
|
||||||
"dotenv-cli": "^5.1.0",
|
|
||||||
"eslint": "^8.9.0",
|
|
||||||
"eslint-config-react-app": "^7.0.0",
|
|
||||||
"eslint-plugin-storybook": "^0.5.6",
|
|
||||||
"gatsby-plugin-alias-imports": "^1.0.5",
|
|
||||||
"gatsby-plugin-image": "^2.6.0",
|
|
||||||
"gatsby-plugin-preval": "^1.0.0",
|
|
||||||
"gatsby-plugin-provide-react": "^1.0.2",
|
|
||||||
"gatsby-plugin-react-helmet": "^5.6.0",
|
|
||||||
"gatsby-plugin-sharp": "^4.6.0",
|
|
||||||
"gatsby-plugin-styled-components": "^5.8.0",
|
|
||||||
"gatsby-source-filesystem": "^4.6.0",
|
|
||||||
"gatsby-transformer-sharp": "^4.6.0",
|
|
||||||
"http-proxy-middleware": "^1.3.1",
|
|
||||||
"prettier": "2.5.1",
|
|
||||||
"react-is": "^17.0.2",
|
|
||||||
"storybook-addon-gatsby": "^0.0.5",
|
|
||||||
"styled-components": "^5.3.3"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
plugins: [require("tailwindcss/nesting"), require("tailwindcss"), require("autoprefixer")],
|
|
||||||
};
|
|
|
@ -1,157 +0,0 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import cn from "classnames";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
import { Alert } from "../Alert";
|
|
||||||
import { Button } from "../Button";
|
|
||||||
import { AddSkylinkToSponsorKeyForm } from "../forms/AddSkylinkToSponsorKeyForm";
|
|
||||||
import { CogIcon, TrashIcon } from "../Icons";
|
|
||||||
import { Modal } from "../Modal";
|
|
||||||
|
|
||||||
import { useAPIKeyEdit } from "./useAPIKeyEdit";
|
|
||||||
import { useAPIKeyRemoval } from "./useAPIKeyRemoval";
|
|
||||||
|
|
||||||
export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
|
|
||||||
const { id, name, createdAt, skylinks } = apiKey;
|
|
||||||
const isSponsorKey = apiKey.public === "true";
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
const onSkylinkListEdited = useCallback(() => {
|
|
||||||
setError(null);
|
|
||||||
onEdited();
|
|
||||||
}, [onEdited]);
|
|
||||||
|
|
||||||
const onSkylinkListEditFailure = (errorMessage) => setError(errorMessage);
|
|
||||||
|
|
||||||
const {
|
|
||||||
removalError,
|
|
||||||
removalInitiated,
|
|
||||||
prompt: promptRemoval,
|
|
||||||
abort: abortRemoval,
|
|
||||||
confirm: confirmRemoval,
|
|
||||||
} = useAPIKeyRemoval({
|
|
||||||
key: apiKey,
|
|
||||||
onSuccess: onRemoved,
|
|
||||||
onFailure: onRemovalError,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
editInitiated,
|
|
||||||
prompt: promptEdit,
|
|
||||||
abort: abortEdit,
|
|
||||||
addSkylink,
|
|
||||||
removeSkylink,
|
|
||||||
} = useAPIKeyEdit({
|
|
||||||
key: apiKey,
|
|
||||||
onSkylinkListUpdate: onSkylinkListEdited,
|
|
||||||
onSkylinkListUpdateFailure: onSkylinkListEditFailure,
|
|
||||||
});
|
|
||||||
|
|
||||||
const closeEditModal = useCallback(() => {
|
|
||||||
setError(null);
|
|
||||||
abortEdit();
|
|
||||||
}, [abortEdit]);
|
|
||||||
|
|
||||||
const skylinksNumber = skylinks?.length ?? 0;
|
|
||||||
const isNotConfigured = isSponsorKey && skylinksNumber === 0;
|
|
||||||
const skylinksPhrasePrefix = skylinksNumber === 0 ? "No" : skylinksNumber;
|
|
||||||
const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} sponsored`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
"grid grid-cols-2 sm:grid-cols-[1fr_repeat(2,_max-content)] py-3 px-4 gap-x-8 items-center bg-white odd:bg-palette-100/50"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="col-span-2 sm:col-span-1 flex items-center">
|
|
||||||
<span className="flex flex-col">
|
|
||||||
<span className={cn("truncate", { "text-palette-300": !name })}>{name || "unnamed key"}</span>
|
|
||||||
{isSponsorKey && (
|
|
||||||
<button
|
|
||||||
onClick={promptEdit}
|
|
||||||
className={cn("text-xs hover:underline decoration-dotted", {
|
|
||||||
"text-error": isNotConfigured,
|
|
||||||
"text-palette-400": !isNotConfigured,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{skylinksPhrase}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="col-span-2 my-4 border-t border-t-palette-200/50 sm:hidden" />
|
|
||||||
<span className="text-palette-400">{dayjs(createdAt).format("MMM DD, YYYY")}</span>
|
|
||||||
<span className="flex items-center justify-end">
|
|
||||||
{isSponsorKey && (
|
|
||||||
<button
|
|
||||||
title="Add or remove skylinks"
|
|
||||||
aria-label="Add or remove skylinks"
|
|
||||||
className="p-1 transition-colors hover:text-primary"
|
|
||||||
onClick={promptEdit}
|
|
||||||
>
|
|
||||||
<CogIcon size={22} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
title="Delete this API key"
|
|
||||||
aria-label="Delete this API key"
|
|
||||||
className="p-1 transition-colors hover:text-error"
|
|
||||||
onClick={promptRemoval}
|
|
||||||
>
|
|
||||||
<TrashIcon size={16} />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{removalInitiated && (
|
|
||||||
<Modal onClose={abortRemoval} className="flex flex-col gap-4 text-center">
|
|
||||||
<h4>Delete API key</h4>
|
|
||||||
<div>
|
|
||||||
<p>Are you sure you want to delete the following API key?</p>
|
|
||||||
<p className="font-semibold">{name || id}</p>
|
|
||||||
</div>
|
|
||||||
{removalError && <Alert $variant="error">{removalError}</Alert>}
|
|
||||||
|
|
||||||
<div className="flex gap-4 justify-center mt-4">
|
|
||||||
<Button $primary onClick={abortRemoval}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={confirmRemoval}>Delete</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{editInitiated && (
|
|
||||||
<Modal onClose={closeEditModal} className="flex flex-col gap-4 text-center sm:px-8 sm:py-6">
|
|
||||||
<h4>Sponsored skylinks</h4>
|
|
||||||
{skylinks?.length > 0 ? (
|
|
||||||
<ul className="text-xs flex flex-col gap-2">
|
|
||||||
{skylinks.map((skylink) => (
|
|
||||||
<li key={skylink} className="grid grid-cols-[1fr_min-content] w-full gap-4 items-center">
|
|
||||||
<code className="whitespace-nowrap select-all truncate bg-palette-100 odd:bg-white p-1">
|
|
||||||
{skylink}
|
|
||||||
</code>
|
|
||||||
<button
|
|
||||||
className="p-1 transition-colors hover:text-error"
|
|
||||||
onClick={() => removeSkylink(skylink)}
|
|
||||||
aria-label="Remove skylink"
|
|
||||||
>
|
|
||||||
<TrashIcon size={16} />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<Alert $variant="info">No skylinks here yet. You can add the first one below 🙃</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{error && <Alert $variant="error">{error}</Alert>}
|
|
||||||
<AddSkylinkToSponsorKeyForm addSkylink={addSkylink} />
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4 justify-center mt-4">
|
|
||||||
<Button onClick={closeEditModal}>Close</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { APIKey } from "./APIKey";
|
|
||||||
|
|
||||||
export const APIKeyList = ({ keys, reloadKeys, title }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h6 className="text-palette-300 mb-4">{title}</h6>
|
|
||||||
<ul className="mt-4">
|
|
||||||
{keys.map((key) => (
|
|
||||||
<APIKey key={key.id} apiKey={key} onEdited={reloadKeys} onRemoved={reloadKeys} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./APIKeyList";
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import accountsService from "../../services/accountsService";
|
|
||||||
|
|
||||||
export const useAPIKeyEdit = ({ key, onSkylinkListUpdate, onSkylinkListUpdateFailure }) => {
|
|
||||||
const [editInitiated, setEditInitiated] = useState(false);
|
|
||||||
|
|
||||||
const prompt = () => setEditInitiated(true);
|
|
||||||
const abort = () => setEditInitiated(false);
|
|
||||||
const updateSkylinkList = useCallback(
|
|
||||||
async (action, skylink) => {
|
|
||||||
try {
|
|
||||||
await accountsService.patch(`user/apikeys/${key.id}`, {
|
|
||||||
json: {
|
|
||||||
[action]: [skylink],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
onSkylinkListUpdate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
if (err.response) {
|
|
||||||
const { message } = await err.response.json();
|
|
||||||
onSkylinkListUpdateFailure(message);
|
|
||||||
} else {
|
|
||||||
onSkylinkListUpdateFailure("Unknown error occured, please try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onSkylinkListUpdate, onSkylinkListUpdateFailure, key]
|
|
||||||
);
|
|
||||||
const addSkylink = (skylink) => updateSkylinkList("add", skylink);
|
|
||||||
const removeSkylink = (skylink) => updateSkylinkList("remove", skylink);
|
|
||||||
|
|
||||||
return {
|
|
||||||
editInitiated,
|
|
||||||
prompt,
|
|
||||||
abort,
|
|
||||||
addSkylink,
|
|
||||||
removeSkylink,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import accountsService from "../../services/accountsService";
|
|
||||||
|
|
||||||
export const useAPIKeyRemoval = ({ key, onSuccess }) => {
|
|
||||||
const [removalInitiated, setRemovalInitiated] = useState(false);
|
|
||||||
const [removalError, setRemovalError] = useState(null);
|
|
||||||
|
|
||||||
const prompt = () => {
|
|
||||||
setRemovalError(null);
|
|
||||||
setRemovalInitiated(true);
|
|
||||||
};
|
|
||||||
const abort = () => setRemovalInitiated(false);
|
|
||||||
|
|
||||||
const confirm = useCallback(async () => {
|
|
||||||
setRemovalError(null);
|
|
||||||
try {
|
|
||||||
await accountsService.delete(`user/apikeys/${key.id}`);
|
|
||||||
setRemovalInitiated(false);
|
|
||||||
onSuccess();
|
|
||||||
} catch (err) {
|
|
||||||
let message = "There was an error processing your request. Please try again later.";
|
|
||||||
|
|
||||||
if (err.response) {
|
|
||||||
const response = await err.response.json();
|
|
||||||
if (response.message) {
|
|
||||||
message = response.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setRemovalError(message);
|
|
||||||
}
|
|
||||||
}, [onSuccess, key]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
removalInitiated,
|
|
||||||
removalError,
|
|
||||||
prompt,
|
|
||||||
abort,
|
|
||||||
confirm,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
import styled from "styled-components";
|
|
||||||
import cn from "classnames";
|
|
||||||
|
|
||||||
export const Alert = styled.div.attrs(({ $variant }) => ({
|
|
||||||
className: cn("px-3 py-2 sm:px-6 sm:py-4 rounded border", {
|
|
||||||
"bg-blue-100 border-blue-200 text-palette-400": $variant === "info",
|
|
||||||
"bg-red-100 border-red-200 text-error": $variant === "error",
|
|
||||||
"bg-green-100 border-green-200 text-palette-400": $variant === "success",
|
|
||||||
}),
|
|
||||||
}))``;
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./Alert";
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
|
||||||
// import { SimpleUploadIcon } from "../Icons";
|
|
||||||
|
|
||||||
import avatarPlaceholder from "../../../static/images/avatar-placeholder.svg";
|
|
||||||
|
|
||||||
export const AvatarUploader = (props) => {
|
|
||||||
const { user } = useUser();
|
|
||||||
const [imageUrl, setImageUrl] = useState(avatarPlaceholder);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setImageUrl(user.avatarUrl ?? avatarPlaceholder);
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...props}>
|
|
||||||
<div
|
|
||||||
className={`flex justify-center items-center xl:w-[245px] xl:h-[245px] bg-contain bg-none xl:bg-[url(/images/avatar-bg.svg)]`}
|
|
||||||
>
|
|
||||||
<img src={imageUrl} className="w-[160px]" alt="" />
|
|
||||||
</div>
|
|
||||||
{/* TODO: uncomment when avatar uploads work
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<button
|
|
||||||
className="flex items-center gap-4 hover:underline decoration-1 decoration-dashed underline-offset-2 decoration-gray-400"
|
|
||||||
type="button"
|
|
||||||
onClick={console.info.bind(console)}
|
|
||||||
>
|
|
||||||
<SimpleUploadIcon size={20} className="shrink-0" /> Upload profile picture
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./AvatarUploader";
|
|
|
@ -1,47 +0,0 @@
|
||||||
import cn from "classnames";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Primary UI component for user interaction
|
|
||||||
*/
|
|
||||||
export const Button = styled.button.attrs(({ as: polymorphicAs, disabled, $primary, type }) => {
|
|
||||||
// We want to default to type=button in most cases, but sometimes we use this component
|
|
||||||
// as a polymorphic one (i.e. for links), and then we should avoid setting `type` property,
|
|
||||||
// as it breaks styling in Safari.
|
|
||||||
const typeAttr = polymorphicAs && polymorphicAs !== "button" ? undefined : type;
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: typeAttr,
|
|
||||||
className: cn(
|
|
||||||
"px-6 py-2.5 inline-block rounded-full font-sans uppercase text-xs tracking-wide transition-[opacity_filter]",
|
|
||||||
{
|
|
||||||
"bg-primary text-palette-600": $primary,
|
|
||||||
"bg-white border-2 border-black text-palette-600": !$primary,
|
|
||||||
"cursor-not-allowed opacity-60": disabled,
|
|
||||||
"hover:brightness-90": !disabled,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
};
|
|
||||||
})``;
|
|
||||||
|
|
||||||
Button.propTypes = {
|
|
||||||
/**
|
|
||||||
* Is this the principal call to action on the page?
|
|
||||||
*/
|
|
||||||
$primary: PropTypes.bool,
|
|
||||||
/**
|
|
||||||
* Prevent interaction on the button
|
|
||||||
*/
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
/**
|
|
||||||
* Type of button (button / submit)
|
|
||||||
*/
|
|
||||||
type: PropTypes.oneOf(["button", "submit"]),
|
|
||||||
};
|
|
||||||
|
|
||||||
Button.defaultProps = {
|
|
||||||
$primary: false,
|
|
||||||
disabled: false,
|
|
||||||
type: "button",
|
|
||||||
};
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { Button } from "./Button";
|
|
||||||
|
|
||||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
|
||||||
export default {
|
|
||||||
title: "SkynetLibrary/Button",
|
|
||||||
component: Button,
|
|
||||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
|
||||||
argTypes: {
|
|
||||||
backgroundColor: { control: "color" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
|
||||||
const Template = (args) => <Button {...args} />;
|
|
||||||
|
|
||||||
export const Primary = Template.bind({});
|
|
||||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
|
||||||
Primary.args = {
|
|
||||||
primary: true,
|
|
||||||
label: "Button",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Secondary = Template.bind({});
|
|
||||||
Secondary.args = {
|
|
||||||
label: "Button",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Large = Template.bind({});
|
|
||||||
Large.args = {
|
|
||||||
size: "large",
|
|
||||||
label: "Button",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Small = Template.bind({});
|
|
||||||
Small.args = {
|
|
||||||
size: "small",
|
|
||||||
label: "Button",
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./Button";
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import copy from "copy-text-to-clipboard";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import { useClickAway } from "react-use";
|
|
||||||
|
|
||||||
import { CopyIcon } from "./Icons";
|
|
||||||
|
|
||||||
const Button = styled.button.attrs({
|
|
||||||
className: "relative inline-flex items-center hover:text-primary",
|
|
||||||
})``;
|
|
||||||
|
|
||||||
const TooltipContainer = styled.div.attrs(({ $visible }) => ({
|
|
||||||
className: `absolute left-full top-1/2 z-10
|
|
||||||
bg-white rounded border border-primary/30 shadow-md
|
|
||||||
pointer-events-none transition-opacity duration-150 ease-in-out
|
|
||||||
${$visible ? "opacity-100" : "opacity-0"}`,
|
|
||||||
}))`
|
|
||||||
transform: translateY(-50%);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TooltipContent = styled.div.attrs({
|
|
||||||
className: "bg-primary-light/10 text-palette-600 py-2 px-4 ",
|
|
||||||
})``;
|
|
||||||
|
|
||||||
export const CopyButton = ({ value, className, ariaLabel = "Copy" }) => {
|
|
||||||
const containerRef = useRef();
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const [timer, setTimer] = useState(null);
|
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
copy(value);
|
|
||||||
setCopied(true);
|
|
||||||
|
|
||||||
setTimer(setTimeout(() => setCopied(false), 1500));
|
|
||||||
}, [value, timer]);
|
|
||||||
|
|
||||||
useClickAway(containerRef, () => setCopied(false));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={containerRef} className={`inline-flex relative overflow-visible pr-2 ${className ?? ""}`}>
|
|
||||||
<Button onClick={handleCopy} className={copied ? "text-primary" : ""} aria-label={ariaLabel}>
|
|
||||||
<CopyIcon size={16} />
|
|
||||||
</Button>
|
|
||||||
<TooltipContainer $visible={copied}>
|
|
||||||
<TooltipContent>Copied to clipboard</TooltipContent>
|
|
||||||
</TooltipContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,52 +0,0 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
|
||||||
import useActivePlan from "../../hooks/useActivePlan";
|
|
||||||
import humanBytes from "../../lib/humanBytes";
|
|
||||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
|
||||||
|
|
||||||
import LatestPayment from "./LatestPayment";
|
|
||||||
import SuggestedPlan from "./SuggestedPlan";
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
const CurrentPlan = () => {
|
|
||||||
const { user, error: userError } = useUser();
|
|
||||||
const { plans, activePlan, error: plansError } = useActivePlan(user);
|
|
||||||
|
|
||||||
if (!user || !activePlan) {
|
|
||||||
return <ContainerLoadingIndicator />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userError || plansError) {
|
|
||||||
return (
|
|
||||||
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
|
|
||||||
<p>An error occurred while loading this data.</p>
|
|
||||||
<p>We'll retry automatically.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<h4>{activePlan.name}</h4>
|
|
||||||
<div className="text-palette-400 justify-between flex flex-col grow">
|
|
||||||
{activePlan.price === 0 && activePlan.limits && (
|
|
||||||
<p>{humanBytes(activePlan.limits.storageLimit)} without paying a dime! 🎉</p>
|
|
||||||
)}
|
|
||||||
{activePlan.price !== 0 &&
|
|
||||||
(user.subscriptionCancelAtPeriodEnd ? (
|
|
||||||
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
|
|
||||||
) : (
|
|
||||||
<p className="first-letter:uppercase">{dayjs(user.subscribedUntil).fromNow(true)} until the next payment</p>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{user.subscriptionStatus && <LatestPayment user={user} />}
|
|
||||||
<SuggestedPlan plans={plans} activePlan={activePlan} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CurrentPlan;
|
|
|
@ -1,18 +0,0 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
// TODO: this is not an accurate information, we need this data from the backend
|
|
||||||
const LatestPayment = ({ user }) => (
|
|
||||||
<div className="flex mt-6 justify-between items-center bg-palette-100/50 py-4 px-6 border-l-2 border-primary">
|
|
||||||
<div className="flex flex-col lg:flex-row">
|
|
||||||
<span>Latest payment</span>
|
|
||||||
<span className="lg:before:content-['-'] lg:before:px-2 text-xs lg:text-base">
|
|
||||||
{dayjs(user.subscribedUntil).subtract(1, "month").format("MM/DD/YYYY")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="rounded py-1 px-2 bg-primary/10 font-sans text-primary uppercase text-xs">Success</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default LatestPayment;
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Link } from "gatsby";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
import { Button } from "../Button";
|
|
||||||
|
|
||||||
const SuggestedPlan = ({ plans, activePlan }) => {
|
|
||||||
const nextPlan = useMemo(() => plans.find(({ tier }) => tier > activePlan.tier), [plans, activePlan]);
|
|
||||||
|
|
||||||
if (!nextPlan) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-7">
|
|
||||||
<p className="font-sans font-semibold text-xs uppercase text-primary">Discover {nextPlan.name}</p>
|
|
||||||
<p className="pt-1 text-xs sm:text-base">{nextPlan.description}</p>
|
|
||||||
<Button $primary as={Link} to="/payments" className="mt-6">
|
|
||||||
Upgrade
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SuggestedPlan;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import CurrentPlan from "./CurrentPlan";
|
|
||||||
|
|
||||||
export default CurrentPlan;
|
|
|
@ -1,116 +0,0 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { Link } from "gatsby";
|
|
||||||
import cn from "classnames";
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
|
||||||
import useActivePlan from "../../hooks/useActivePlan";
|
|
||||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
|
||||||
|
|
||||||
import { GraphBar } from "./GraphBar";
|
|
||||||
import { UsageGraph } from "./UsageGraph";
|
|
||||||
import humanBytes from "../../lib/humanBytes";
|
|
||||||
|
|
||||||
const useUsageData = () => {
|
|
||||||
const { user } = useUser();
|
|
||||||
const { activePlan, error } = useActivePlan(user);
|
|
||||||
const { data: stats, error: statsError } = useSWR("user/stats");
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [usage, setUsage] = useState({});
|
|
||||||
|
|
||||||
const hasError = error || statsError;
|
|
||||||
const hasData = activePlan && stats;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasData || hasError) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasData && !hasError) {
|
|
||||||
setUsage({
|
|
||||||
filesUsed: stats?.numUploads,
|
|
||||||
filesLimit: activePlan?.limits?.maxNumberUploads,
|
|
||||||
storageUsed: stats?.totalUploadsSize,
|
|
||||||
storageLimit: activePlan?.limits?.storageLimit,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [hasData, hasError, stats, activePlan]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
error: error || statsError,
|
|
||||||
loading,
|
|
||||||
usage,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const size = (bytes) => {
|
|
||||||
const text = humanBytes(bytes ?? 0, { precision: 0 });
|
|
||||||
const [value, unit] = text.split(" ");
|
|
||||||
|
|
||||||
return {
|
|
||||||
text,
|
|
||||||
value,
|
|
||||||
unit,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ErrorMessage = () => (
|
|
||||||
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
|
|
||||||
<p>We were not able to fetch the current usage data.</p>
|
|
||||||
<p>We'll try again automatically.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function CurrentUsage() {
|
|
||||||
const { activePlan, plans } = useActivePlan();
|
|
||||||
const { usage, error, loading } = useUsageData();
|
|
||||||
const nextPlan = useMemo(() => plans.find(({ tier }) => tier > activePlan?.tier), [plans, activePlan]);
|
|
||||||
const storageUsage = size(usage.storageUsed);
|
|
||||||
const storageLimit = size(usage.storageLimit);
|
|
||||||
const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <ContainerLoadingIndicator />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ErrorMessage />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h4>
|
|
||||||
{storageUsage.text} of {storageLimit.text}
|
|
||||||
</h4>
|
|
||||||
<p className="text-palette-400">
|
|
||||||
{usage.filesUsed} of {usage.filesLimit} files
|
|
||||||
</p>
|
|
||||||
<div className="relative mt-7 font-sans uppercase text-xs">
|
|
||||||
<div className="flex place-content-between">
|
|
||||||
<span>Storage</span>
|
|
||||||
<span>{storageLimit.text}</span>
|
|
||||||
</div>
|
|
||||||
<UsageGraph>
|
|
||||||
<GraphBar value={usage.storageUsed} limit={usage.storageLimit} label={storageUsage} className="normal-case" />
|
|
||||||
<GraphBar value={usage.filesUsed} limit={usage.filesLimit} label={filesUsedLabel} />
|
|
||||||
</UsageGraph>
|
|
||||||
<div className="flex place-content-between">
|
|
||||||
<span>Files</span>
|
|
||||||
<span className="inline-flex place-content-between w-[37%]">
|
|
||||||
<Link
|
|
||||||
to="/payments"
|
|
||||||
className={cn(
|
|
||||||
"text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline",
|
|
||||||
{ invisible: !nextPlan }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
UPGRADE
|
|
||||||
</Link>{" "}
|
|
||||||
<span>{usage.filesLimit}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
const Bar = styled.div.attrs({
|
|
||||||
className: `relative flex justify-end h-4 bg-primary rounded-l rounded-r-lg`,
|
|
||||||
})`
|
|
||||||
min-width: 1rem;
|
|
||||||
width: ${({ $percentage }) => $percentage}%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BarTip = styled.span.attrs({
|
|
||||||
className: "relative w-4 h-4 border-2 rounded-full bg-white border-primary",
|
|
||||||
})``;
|
|
||||||
|
|
||||||
const BarLabel = styled.span.attrs({
|
|
||||||
className: "usage-label bg-white rounded border-2 border-palette-200 px-3 whitespace-nowrap absolute shadow",
|
|
||||||
})`
|
|
||||||
${({ $percentage }) => `
|
|
||||||
left: max(0%, ${$percentage}%);
|
|
||||||
top: -0.5rem;
|
|
||||||
transform: translateX(-${$percentage}%);
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const GraphBar = ({ value, limit, label, className }) => {
|
|
||||||
const percentage = typeof limit !== "number" || limit === 0 ? 0 : (value / limit) * 100;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`relative flex items-center ${className}`}>
|
|
||||||
<Bar $percentage={percentage}>
|
|
||||||
<BarTip />
|
|
||||||
</Bar>
|
|
||||||
<BarLabel $percentage={percentage}>
|
|
||||||
<span className="font-sora text-lg">{label.value}</span> <span>{label.unit}</span>
|
|
||||||
</BarLabel>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,11 +0,0 @@
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
import usageGraphBg from "../../../static/images/usage-graph-bg.svg";
|
|
||||||
|
|
||||||
export const UsageGraph = styled.div.attrs({
|
|
||||||
className: "w-full my-3 grid grid-flow-row grid-rows-2",
|
|
||||||
})`
|
|
||||||
height: 146px;
|
|
||||||
background: url(${usageGraphBg}) no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
`;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import CurrentUsage from "./CurrentUsage";
|
|
||||||
|
|
||||||
export default CurrentUsage;
|
|
|
@ -1,63 +0,0 @@
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import { useClickAway } from "react-use";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import styled, { css, keyframes } from "styled-components";
|
|
||||||
|
|
||||||
import { ChevronDownIcon } from "../Icons";
|
|
||||||
|
|
||||||
const dropDown = keyframes`
|
|
||||||
0% {
|
|
||||||
transform: rotateX(-90deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotateX(0deg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Container = styled.div.attrs({ className: `relative inline-flex` })``;
|
|
||||||
|
|
||||||
const Trigger = styled.button.attrs({
|
|
||||||
className: "flex items-center",
|
|
||||||
})``;
|
|
||||||
|
|
||||||
const TriggerIcon = styled(ChevronDownIcon).attrs({
|
|
||||||
className: "transition-transform text-primary",
|
|
||||||
})`
|
|
||||||
transform: ${({ open }) => (open ? "rotateX(180deg)" : "none")};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Flyout = styled.div.attrs(({ open }) => ({
|
|
||||||
className: `absolute top-full right-0 p-0 z-10
|
|
||||||
border rounded border-palette-100
|
|
||||||
bg-white shadow-md shadow-palette-200/50
|
|
||||||
${open ? "visible" : "invisible"}`,
|
|
||||||
}))`
|
|
||||||
transform-origin: top center;
|
|
||||||
animation: ${({ open }) =>
|
|
||||||
open
|
|
||||||
? css`
|
|
||||||
${dropDown} .15s ease-in-out forwards;
|
|
||||||
`
|
|
||||||
: "none"};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DropdownMenu = ({ title, children }) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const menuRef = useRef();
|
|
||||||
|
|
||||||
useClickAway(menuRef, () => setOpen(false));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container ref={menuRef}>
|
|
||||||
<Trigger onClick={() => setOpen((open) => !open)}>
|
|
||||||
{title} <TriggerIcon open={open} />
|
|
||||||
</Trigger>
|
|
||||||
<Flyout open={open}>{children}</Flyout>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DropdownMenu.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
};
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { Panel } from "../Panel";
|
|
||||||
import { DropdownMenu, DropdownMenuLink } from ".";
|
|
||||||
import { CogIcon, LockClosedIcon } from "../Icons";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "SkynetLibrary/DropdownMenu",
|
|
||||||
component: DropdownMenu,
|
|
||||||
subcomponents: {
|
|
||||||
DropdownMenuLink,
|
|
||||||
},
|
|
||||||
decorators: [
|
|
||||||
(Story) => (
|
|
||||||
<Panel style={{ margin: 50, textAlign: "center" }}>
|
|
||||||
<Story />
|
|
||||||
</Panel>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NavigationDropdown = () => (
|
|
||||||
<DropdownMenu title="My account">
|
|
||||||
<DropdownMenuLink href="/settings" icon={CogIcon} label="Settings" active />
|
|
||||||
<DropdownMenuLink href="/logout" icon={LockClosedIcon} label="Log out" />
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
|
@ -1,22 +0,0 @@
|
||||||
import styled from "styled-components";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
const DropdownLink = styled.a.attrs({
|
|
||||||
className: `m-0 border-t border-palette-200/50 h-[60px]
|
|
||||||
whitespace-nowrap transition-colors
|
|
||||||
hover:bg-palette-100/50 flex items-center
|
|
||||||
pr-8 pl-6 py-4 gap-4 first:border-0`,
|
|
||||||
})``;
|
|
||||||
|
|
||||||
export const DropdownMenuLink = ({ active, icon: Icon, label, ...props }) => (
|
|
||||||
<DropdownLink {...props}>
|
|
||||||
{Icon ? <Icon /> : null}
|
|
||||||
<span className="text-palette-500">{label}</span>
|
|
||||||
</DropdownLink>
|
|
||||||
);
|
|
||||||
|
|
||||||
DropdownMenuLink.propTypes = {
|
|
||||||
label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired,
|
|
||||||
active: PropTypes.bool,
|
|
||||||
icon: PropTypes.func,
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue