Merge branch 'master' into switch-authenticated-health-checks-to-api-key
This commit is contained in:
commit
ce1d450edd
|
@ -3,48 +3,48 @@ updates:
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/packages/dashboard"
|
directory: "/packages/dashboard"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/packages/dnslink-api"
|
directory: "/packages/dnslink-api"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/packages/handshake-api"
|
directory: "/packages/handshake-api"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/packages/health-check"
|
directory: "/packages/health-check"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/packages/website"
|
directory: "/packages/website"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/docker/nginx"
|
directory: "/docker/nginx"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/docker/sia"
|
directory: "/docker/sia"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/packages/dashboard"
|
directory: "/packages/dashboard"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/packages/dnslink-api"
|
directory: "/packages/dnslink-api"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/packages/handshake-api"
|
directory: "/packages/handshake-api"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/packages/health-check"
|
directory: "/packages/health-check"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/packages/website"
|
directory: "/packages/website"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
|
|
|
@ -45,7 +45,5 @@ jobs:
|
||||||
uses: skynetlabs/deploy-to-skynet-action@v2
|
uses: skynetlabs/deploy-to-skynet-action@v2
|
||||||
with:
|
with:
|
||||||
upload-dir: packages/website/public
|
upload-dir: packages/website/public
|
||||||
portal-url: https://skynetpro.net
|
|
||||||
skynet-jwt: ${{ secrets.SKYNET_JWT }}
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
registry-seed: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && secrets.WEBSITE_REGISTRY_SEED || '' }}
|
registry-seed: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && secrets.WEBSITE_REGISTRY_SEED || '' }}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Dockerfile Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
hadolint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
dockerfile:
|
||||||
|
- docker/nginx/Dockerfile
|
||||||
|
- docker/nginx/testing/Dockerfile
|
||||||
|
- docker/sia/Dockerfile
|
||||||
|
- packages/dashboard/Dockerfile
|
||||||
|
- packages/dashboard-v2/Dockerfile
|
||||||
|
- packages/dnslink-api/Dockerfile
|
||||||
|
- packages/handshake-api/Dockerfile
|
||||||
|
- packages/health-check/Dockerfile
|
||||||
|
- packages/website/Dockerfile
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: hadolint/hadolint-action@v2.0.0
|
||||||
|
with:
|
||||||
|
dockerfile: ${{ matrix.dockerfile }}
|
|
@ -4,30 +4,43 @@
|
||||||
name: Nginx Lua Unit Tests
|
name: Nginx Lua Unit Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- docker/nginx/libs/**
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "docker/nginx/libs/**.lua"
|
- docker/nginx/libs/**
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: openresty/openresty:1.19.9.1-focal
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
architecture: "x64"
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install hererocks
|
luarocks install lua-resty-http
|
||||||
hererocks env --lua=5.1 -rlatest
|
|
||||||
source env/bin/activate
|
|
||||||
luarocks install busted
|
|
||||||
luarocks install hasher
|
luarocks install hasher
|
||||||
|
luarocks install busted
|
||||||
|
luarocks install luacov
|
||||||
|
luarocks install luacheck
|
||||||
|
|
||||||
- name: Unit Tests
|
- name: Lint Code With Luacheck
|
||||||
run: |
|
run: luacheck docker/nginx/libs --std ngx_lua+busted
|
||||||
source env/bin/activate
|
|
||||||
busted --verbose --pattern=spec --directory=docker/nginx/libs .
|
- 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
|
||||||
|
|
|
@ -86,6 +86,10 @@ __pycache__
|
||||||
/.idea/
|
/.idea/
|
||||||
/venv*
|
/venv*
|
||||||
|
|
||||||
|
# Luacov file
|
||||||
|
luacov.stats.out
|
||||||
|
luacov.report.out
|
||||||
|
|
||||||
# Setup-script log files
|
# Setup-script log files
|
||||||
setup-scripts/serverload.log
|
setup-scripts/serverload.log
|
||||||
setup-scripts/serverload.json
|
setup-scripts/serverload.json
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
## Latest Setup Documentation
|
## Latest Setup Documentation
|
||||||
|
|
||||||
Latest Skynet Webportal setup documentation and the setup process Skynet Labs
|
Latest Skynet Webportal setup documentation and the setup process Skynet Labs
|
||||||
supports is located at https://docs.siasky.net/webportal-management/overview.
|
supports is located at https://portal-docs.skynetlabs.com/.
|
||||||
|
|
||||||
Some of the scripts and setup documentation contained in this repository
|
Some scripts and setup documentation contained in this repository
|
||||||
(`skynet-webportal`) can be outdated and generally should not be used.
|
(`skynet-webportal`) may be outdated and generally should not be used.
|
||||||
|
|
||||||
## Web application
|
## Web application
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ For the purposes of complying with our code license, you can use the following S
|
||||||
`fb6c9320bc7e01fbb9cd8d8c3caaa371386928793c736837832e634aaaa484650a3177d6714a`
|
`fb6c9320bc7e01fbb9cd8d8c3caaa371386928793c736837832e634aaaa484650a3177d6714a`
|
||||||
|
|
||||||
## Running a Portal
|
## Running a Portal
|
||||||
For those interested in running a Webportal, head over to our developer docs [here](https://docs.siasky.net/webportal-management/overview.) to learn more.
|
For those interested in running a Webportal, head over to our developer docs [here](https://portal-docs.skynetlabs.com/) to learn more.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
- Fix `dashboard-v2` Dockerfile context in `docker-compose.accounts.yml` to
|
||||||
|
avoid Ansible deploy (docker compose build) `permission denied` issues.
|
|
@ -8,7 +8,9 @@ x-logging: &default-logging
|
||||||
|
|
||||||
services:
|
services:
|
||||||
abuse-scanner:
|
abuse-scanner:
|
||||||
image: skynetlabs/abuse-scanner
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
|
# build: https://github.com/SkynetLabs/abuse-scanner.git#main
|
||||||
|
image: skynetlabs/abuse-scanner:0.1.1
|
||||||
container_name: abuse-scanner
|
container_name: abuse-scanner
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
|
@ -20,11 +20,9 @@ services:
|
||||||
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only
|
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only
|
||||||
|
|
||||||
accounts:
|
accounts:
|
||||||
build:
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
context: ./docker/accounts
|
# build: https://github.com/SkynetLabs/skynet-accounts.git#main
|
||||||
dockerfile: Dockerfile
|
image: skynetlabs/skynet-accounts
|
||||||
args:
|
|
||||||
branch: main
|
|
||||||
container_name: accounts
|
container_name: accounts
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
@ -57,9 +55,11 @@ services:
|
||||||
- mongo
|
- mongo
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
build:
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
context: ./packages/dashboard
|
# build:
|
||||||
dockerfile: Dockerfile
|
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
||||||
|
# dockerfile: ./packages/dashboard/Dockerfile
|
||||||
|
image: skynetlabs/dashboard
|
||||||
container_name: dashboard
|
container_name: dashboard
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
@ -77,3 +77,26 @@ services:
|
||||||
- 3000
|
- 3000
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongo
|
- mongo
|
||||||
|
|
||||||
|
dashboard-v2:
|
||||||
|
build:
|
||||||
|
context: ./packages/dashboard-v2
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: dashboard-v2
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- GATSBY_PORTAL_DOMAIN=${PORTAL_DOMAIN}
|
||||||
|
- GATSBY_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
|
||||||
|
volumes:
|
||||||
|
- ./docker/data/dashboard-v2/.cache:/usr/app/.cache
|
||||||
|
- ./docker/data/dashboard-v2/public:/usr/app/public
|
||||||
|
networks:
|
||||||
|
shared:
|
||||||
|
ipv4_address: 10.10.10.86
|
||||||
|
expose:
|
||||||
|
- 9000
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
|
|
@ -13,7 +13,9 @@ services:
|
||||||
- BLOCKER_PORT=4000
|
- BLOCKER_PORT=4000
|
||||||
|
|
||||||
blocker:
|
blocker:
|
||||||
image: skynetlabs/blocker
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
|
# build: https://github.com/SkynetLabs/blocker.git#main
|
||||||
|
image: skynetlabs/blocker:0.1.1
|
||||||
container_name: blocker
|
container_name: blocker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
|
@ -26,7 +26,9 @@ services:
|
||||||
ipv4_address: 10.10.10.100
|
ipv4_address: 10.10.10.100
|
||||||
|
|
||||||
malware-scanner:
|
malware-scanner:
|
||||||
image: skynetlabs/malware-scanner
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
|
# build: https://github.com/SkynetLabs/malware-scanner.git#main
|
||||||
|
image: skynetlabs/malware-scanner:0.1.0
|
||||||
container_name: malware-scanner
|
container_name: malware-scanner
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
|
@ -15,7 +15,7 @@ services:
|
||||||
|
|
||||||
mongo:
|
mongo:
|
||||||
image: mongo:4.4.1
|
image: mongo:4.4.1
|
||||||
command: --keyFile=/data/mgkey --replSet=${SKYNET_DB_REPLICASET:-skynet}
|
command: --keyFile=/data/mgkey --replSet=${SKYNET_DB_REPLICASET:-skynet} --setParameter ShardingTaskExecutorPoolMinSize=10
|
||||||
container_name: mongo
|
container_name: mongo
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
|
@ -54,9 +54,11 @@ services:
|
||||||
- ./docker/data/certbot:/etc/letsencrypt
|
- ./docker/data/certbot:/etc/letsencrypt
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
build:
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
context: ./docker/nginx
|
# build:
|
||||||
dockerfile: Dockerfile
|
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
||||||
|
# dockerfile: ./docker/nginx/Dockerfile
|
||||||
|
image: skynetlabs/nginx
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
@ -69,6 +71,10 @@ 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
|
||||||
|
@ -82,9 +88,11 @@ services:
|
||||||
- website
|
- website
|
||||||
|
|
||||||
website:
|
website:
|
||||||
build:
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
context: ./packages/website
|
# build:
|
||||||
dockerfile: Dockerfile
|
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
||||||
|
# dockerfile: ./packages/website/Dockerfile
|
||||||
|
image: skynetlabs/website
|
||||||
container_name: website
|
container_name: website
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
@ -118,9 +126,11 @@ services:
|
||||||
- 12037
|
- 12037
|
||||||
|
|
||||||
handshake-api:
|
handshake-api:
|
||||||
build:
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
context: ./packages/handshake-api
|
# build:
|
||||||
dockerfile: Dockerfile
|
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
||||||
|
# dockerfile: ./packages/handshake-api/Dockerfile
|
||||||
|
image: skynetlabs/handshake-api
|
||||||
container_name: handshake-api
|
container_name: handshake-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
@ -140,9 +150,11 @@ services:
|
||||||
- handshake
|
- handshake
|
||||||
|
|
||||||
dnslink-api:
|
dnslink-api:
|
||||||
build:
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
context: ./packages/dnslink-api
|
# build:
|
||||||
dockerfile: Dockerfile
|
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
||||||
|
# dockerfile: ./packages/dnslink-api/Dockerfile
|
||||||
|
image: skynetlabs/dnslink-api
|
||||||
container_name: dnslink-api
|
container_name: dnslink-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
@ -153,9 +165,11 @@ services:
|
||||||
- 3100
|
- 3100
|
||||||
|
|
||||||
health-check:
|
health-check:
|
||||||
build:
|
# uncomment "build" and comment out "image" to build from sources
|
||||||
context: ./packages/health-check
|
# build:
|
||||||
dockerfile: Dockerfile
|
# context: https://github.com/SkynetLabs/skynet-webportal.git#master
|
||||||
|
# dockerfile: ./packages/health-check/Dockerfile
|
||||||
|
image: skynetlabs/health-check
|
||||||
container_name: health-check
|
container_name: health-check
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
FROM golang:1.16.7
|
|
||||||
LABEL maintainer="SkynetLabs <devs@siasky.net>"
|
|
||||||
|
|
||||||
ENV GOOS linux
|
|
||||||
ENV GOARCH amd64
|
|
||||||
|
|
||||||
ARG branch=main
|
|
||||||
|
|
||||||
WORKDIR /root
|
|
||||||
|
|
||||||
RUN git clone --single-branch --branch ${branch} https://github.com/SkynetLabs/skynet-accounts.git && \
|
|
||||||
cd skynet-accounts && \
|
|
||||||
go mod download && \
|
|
||||||
make release
|
|
||||||
|
|
||||||
ENV SKYNET_DB_HOST="localhost"
|
|
||||||
ENV SKYNET_DB_PORT="27017"
|
|
||||||
ENV SKYNET_DB_USER="username"
|
|
||||||
ENV SKYNET_DB_PASS="password"
|
|
||||||
ENV SKYNET_ACCOUNTS_PORT=3000
|
|
||||||
|
|
||||||
ENTRYPOINT ["skynet-accounts"]
|
|
|
@ -1,23 +1,21 @@
|
||||||
FROM openresty/openresty:1.19.9.1-focal
|
FROM openresty/openresty:1.19.9.1-focal
|
||||||
|
|
||||||
RUN luarocks install lua-resty-http && \
|
WORKDIR /
|
||||||
luarocks install hasher && \
|
|
||||||
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
|
|
||||||
|
|
||||||
COPY mo ./
|
RUN apt-get update && apt-get --no-install-recommends -y install bc=1.07.1-2build1 && \
|
||||||
COPY libs /etc/nginx/libs
|
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
||||||
COPY conf.d /etc/nginx/conf.d
|
luarocks install lua-resty-http && \
|
||||||
COPY conf.d.templates /etc/nginx/conf.d.templates
|
luarocks install hasher
|
||||||
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
|
|
||||||
|
|
||||||
CMD [ "bash", "-c", \
|
# reload nginx every 6 hours (for reloading certificates)
|
||||||
"./mo < /etc/nginx/conf.d.templates/server.account.conf > /etc/nginx/conf.d/server.account.conf ; \
|
ENV NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS 6
|
||||||
./mo < /etc/nginx/conf.d.templates/server.api.conf > /etc/nginx/conf.d/server.api.conf; \
|
|
||||||
./mo < /etc/nginx/conf.d.templates/server.hns.conf > /etc/nginx/conf.d/server.hns.conf; \
|
# copy entrypoint and entrypoint scripts
|
||||||
./mo < /etc/nginx/conf.d.templates/server.skylink.conf > /etc/nginx/conf.d/server.skylink.conf ; \
|
COPY docker/nginx/docker-entrypoint.sh /
|
||||||
while :; do sleep 6h & wait ${!}; /usr/local/openresty/bin/openresty -s reload; done & \
|
COPY docker/nginx/docker-entrypoint.d /docker-entrypoint.d
|
||||||
/usr/local/openresty/bin/openresty '-g daemon off;'" \
|
|
||||||
]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
STOPSIGNAL SIGQUIT
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
{{#ACCOUNTS_ENABLED}}
|
|
||||||
{{#PORTAL_DOMAIN}}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
{{/PORTAL_DOMAIN}}
|
|
||||||
|
|
||||||
{{#SERVER_DOMAIN}}
|
|
||||||
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 { 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}}", "^([^.]+)") }
|
|
||||||
}
|
|
||||||
{{/SERVER_DOMAIN}}
|
|
||||||
{{/ACCOUNTS_ENABLED}}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
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,43 +0,0 @@
|
||||||
{{#PORTAL_DOMAIN}}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
{{/PORTAL_DOMAIN}}
|
|
||||||
|
|
||||||
{{#SERVER_DOMAIN}}
|
|
||||||
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 { 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}}", "^([^.]+)") }
|
|
||||||
}
|
|
||||||
{{/SERVER_DOMAIN}}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
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}", "^([^.]+)") }
|
||||||
|
}
|
|
@ -12,5 +12,14 @@ server {
|
||||||
ssl_certificate /etc/ssl/local-certificate.crt;
|
ssl_certificate /etc/ssl/local-certificate.crt;
|
||||||
ssl_certificate_key /etc/ssl/local-certificate.key;
|
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;
|
include /etc/nginx/conf.d/server/server.dnslink;
|
||||||
}
|
}
|
|
@ -1,45 +0,0 @@
|
||||||
{{#PORTAL_DOMAIN}}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
{{/PORTAL_DOMAIN}}
|
|
||||||
|
|
||||||
{{#SERVER_DOMAIN}}
|
|
||||||
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 { 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}}", "^([^.]+)") }
|
|
||||||
}
|
|
||||||
{{/SERVER_DOMAIN}}
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
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,43 +0,0 @@
|
||||||
{{#PORTAL_DOMAIN}}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
{{/PORTAL_DOMAIN}}
|
|
||||||
|
|
||||||
{{#SERVER_DOMAIN}}
|
|
||||||
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 { 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}}", "^([^.]+)") }
|
|
||||||
}
|
|
||||||
{{/SERVER_DOMAIN}}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
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,5 +1,4 @@
|
||||||
include /etc/nginx/conf.d/include/cors;
|
include /etc/nginx/conf.d/include/cors;
|
||||||
include /etc/nginx/conf.d/include/track-download;
|
|
||||||
|
|
||||||
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
||||||
|
|
||||||
|
@ -37,3 +36,18 @@ proxy_read_timeout 600;
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
proxy_set_header User-Agent: Sia-Agent;
|
||||||
|
|
||||||
proxy_pass http://sia:9980/skynet/skylink/$skylink$path$is_args$args;
|
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,6 +1,5 @@
|
||||||
include /etc/nginx/conf.d/include/cors;
|
include /etc/nginx/conf.d/include/cors;
|
||||||
include /etc/nginx/conf.d/include/sia-auth;
|
include /etc/nginx/conf.d/include/sia-auth;
|
||||||
include /etc/nginx/conf.d/include/track-registry;
|
|
||||||
|
|
||||||
limit_req zone=registry_access_by_ip burst=600 nodelay;
|
limit_req zone=registry_access_by_ip burst=600 nodelay;
|
||||||
limit_req zone=registry_access_by_ip_throttled burst=200 nodelay;
|
limit_req zone=registry_access_by_ip_throttled burst=200 nodelay;
|
||||||
|
@ -30,3 +29,10 @@ access_by_lua_block {
|
||||||
end
|
end
|
||||||
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,53 +0,0 @@
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
|
|
||||||
-- tracking runs only when request comes from authenticated user
|
|
||||||
if skynet_account.is_authenticated() then
|
|
||||||
local function track(premature, skylink, status, body_bytes_sent, auth_headers)
|
|
||||||
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 ~= ngx.HTTP_NO_CONTENT) then
|
|
||||||
ngx.log(ngx.ERR, "Failed accounts service request /track/download/" .. skylink .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if ngx.header["Skynet-Skylink"] and ngx.status >= ngx.HTTP_OK and ngx.status < ngx.HTTP_SPECIAL_RESPONSE then
|
|
||||||
local auth_headers = skynet_account.get_auth_headers()
|
|
||||||
local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], ngx.status, ngx.var.body_bytes_sent, auth_headers)
|
|
||||||
if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- this block runs only when scanner module is enabled
|
|
||||||
if os.getenv("PORTAL_MODULES"):match("s") then
|
|
||||||
local function scan(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
|
|
||||||
ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- scan all skylinks but make sure to only run if skylink is present (empty if request failed)
|
|
||||||
if ngx.header["Skynet-Skylink"] then
|
|
||||||
local ok, err = ngx.timer.at(0, scan, ngx.header["Skynet-Skylink"])
|
|
||||||
if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
|
|
||||||
-- tracking runs only when request comes from authenticated user
|
|
||||||
if skynet_account.is_authenticated() then
|
|
||||||
local function track(premature, request_method, auth_headers)
|
|
||||||
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 ~= ngx.HTTP_NO_CONTENT) then
|
|
||||||
ngx.log(ngx.ERR, "Failed accounts service request /track/registry/" .. registry_action .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if ngx.status == ngx.HTTP_OK or ngx.status == ngx.HTTP_NOT_FOUND then
|
|
||||||
local auth_headers = skynet_account.get_auth_headers()
|
|
||||||
local ok, err = ngx.timer.at(0, track, ngx.req.get_method(), auth_headers)
|
|
||||||
if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
log_by_lua_block {
|
|
||||||
local skynet_account = require("skynet.account")
|
|
||||||
|
|
||||||
-- tracking runs only when request comes from authenticated user
|
|
||||||
if skynet_account.is_authenticated() then
|
|
||||||
local function track(premature, skylink, auth_headers)
|
|
||||||
if premature then return end
|
|
||||||
|
|
||||||
local httpc = require("resty.http").new()
|
|
||||||
|
|
||||||
-- 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 = auth_headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
|
|
||||||
ngx.log(ngx.ERR, "Failed accounts service request /track/upload/" .. skylink .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- report all skylinks (header empty if request failed) but only if jwt is preset (user is authenticated)
|
|
||||||
if ngx.header["Skynet-Skylink"] then
|
|
||||||
local auth_headers = skynet_account.get_auth_headers()
|
|
||||||
local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], auth_headers)
|
|
||||||
if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- this block runs only when scanner module is enabled
|
|
||||||
if os.getenv("PORTAL_MODULES"):match("s") then
|
|
||||||
local function scan(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
|
|
||||||
ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- scan all skylinks but make sure to only run if skylink is present (empty if request failed)
|
|
||||||
if ngx.header["Skynet-Skylink"] then
|
|
||||||
local ok, err = ngx.timer.at(0, scan, ngx.header["Skynet-Skylink"])
|
|
||||||
if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
|
|
@ -3,6 +3,11 @@ listen 443 ssl http2;
|
||||||
include /etc/nginx/conf.d/include/ssl-settings;
|
include /etc/nginx/conf.d/include/ssl-settings;
|
||||||
include /etc/nginx/conf.d/include/init-optional-variables;
|
include /etc/nginx/conf.d/include/init-optional-variables;
|
||||||
|
|
||||||
|
# Uncomment to launch new Dashboard under /v2 path
|
||||||
|
# location /v2 {
|
||||||
|
# proxy_pass http://dashboard-v2:9000;
|
||||||
|
# }
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://dashboard:3000;
|
proxy_pass http://dashboard:3000;
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,7 +206,6 @@ location /skynet/registry/subscription {
|
||||||
location /skynet/skyfile {
|
location /skynet/skyfile {
|
||||||
include /etc/nginx/conf.d/include/cors;
|
include /etc/nginx/conf.d/include/cors;
|
||||||
include /etc/nginx/conf.d/include/sia-auth;
|
include /etc/nginx/conf.d/include/sia-auth;
|
||||||
include /etc/nginx/conf.d/include/track-upload;
|
|
||||||
include /etc/nginx/conf.d/include/generate-siapath;
|
include /etc/nginx/conf.d/include/generate-siapath;
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
include /etc/nginx/conf.d/include/portal-access-check;
|
||||||
|
|
||||||
|
@ -228,12 +227,31 @@ location /skynet/skyfile {
|
||||||
|
|
||||||
# proxy this call to siad endpoint (make sure the ip is correct)
|
# proxy this call to siad endpoint (make sure the ip is correct)
|
||||||
proxy_pass http://sia:9980/skynet/skyfile/$dir1/$dir2/$dir3$is_args$args;
|
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
|
# endpoint implementing resumable file uploads open protocol https://tus.io
|
||||||
location /skynet/tus {
|
location /skynet/tus {
|
||||||
include /etc/nginx/conf.d/include/cors-headers; # include cors headers but do not overwrite OPTIONS response
|
include /etc/nginx/conf.d/include/cors-headers; # include cors headers but do not overwrite OPTIONS response
|
||||||
include /etc/nginx/conf.d/include/track-upload;
|
|
||||||
|
|
||||||
limit_req zone=uploads_by_ip burst=10 nodelay;
|
limit_req zone=uploads_by_ip burst=10 nodelay;
|
||||||
limit_req zone=uploads_by_ip_throttled;
|
limit_req zone=uploads_by_ip_throttled;
|
||||||
|
@ -241,8 +259,8 @@ location /skynet/tus {
|
||||||
limit_conn upload_conn 5;
|
limit_conn upload_conn 5;
|
||||||
limit_conn upload_conn_rl 1;
|
limit_conn upload_conn_rl 1;
|
||||||
|
|
||||||
# TUS chunks size is 40M + leaving 10M of breathing room
|
# Do not limit body size in nginx, skyd will reject early on too large upload
|
||||||
client_max_body_size 50M;
|
client_max_body_size 0;
|
||||||
|
|
||||||
# Those timeouts need to be elevated since skyd can stall reading
|
# Those timeouts need to be elevated since skyd can stall reading
|
||||||
# data for a while when overloaded which would terminate connection
|
# data for a while when overloaded which would terminate connection
|
||||||
|
@ -294,12 +312,31 @@ location /skynet/tus {
|
||||||
end
|
end
|
||||||
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 {
|
location /skynet/pin {
|
||||||
include /etc/nginx/conf.d/include/cors;
|
include /etc/nginx/conf.d/include/cors;
|
||||||
include /etc/nginx/conf.d/include/sia-auth;
|
include /etc/nginx/conf.d/include/sia-auth;
|
||||||
include /etc/nginx/conf.d/include/track-upload;
|
|
||||||
include /etc/nginx/conf.d/include/generate-siapath;
|
include /etc/nginx/conf.d/include/generate-siapath;
|
||||||
include /etc/nginx/conf.d/include/portal-access-check;
|
include /etc/nginx/conf.d/include/portal-access-check;
|
||||||
|
|
||||||
|
@ -311,6 +348,26 @@ location /skynet/pin {
|
||||||
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
proxy_set_header User-Agent: Sia-Agent;
|
||||||
proxy_pass http://sia:9980$uri?siapath=$dir1/$dir2/$dir3&$args;
|
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 {
|
location /skynet/metadata {
|
||||||
|
@ -357,7 +414,6 @@ location ~ "^/file/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
|
||||||
|
|
||||||
location /skynet/trustless/basesector {
|
location /skynet/trustless/basesector {
|
||||||
include /etc/nginx/conf.d/include/cors;
|
include /etc/nginx/conf.d/include/cors;
|
||||||
include /etc/nginx/conf.d/include/track-download;
|
|
||||||
|
|
||||||
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
||||||
|
|
||||||
|
@ -391,6 +447,21 @@ location /skynet/trustless/basesector {
|
||||||
|
|
||||||
proxy_set_header User-Agent: Sia-Agent;
|
proxy_set_header User-Agent: Sia-Agent;
|
||||||
proxy_pass http://sia:9980;
|
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 {
|
location /__internal/do/not/use/accounts {
|
||||||
|
|
|
@ -5,6 +5,7 @@ location / {
|
||||||
set $path $uri;
|
set $path $uri;
|
||||||
|
|
||||||
rewrite_by_lua_block {
|
rewrite_by_lua_block {
|
||||||
|
local cjson = require("cjson")
|
||||||
local cache = ngx.shared.dnslink
|
local cache = ngx.shared.dnslink
|
||||||
local cache_value = cache:get(ngx.var.host)
|
local cache_value = cache:get(ngx.var.host)
|
||||||
|
|
||||||
|
@ -28,13 +29,23 @@ location / {
|
||||||
ngx.exit(ngx.status)
|
ngx.exit(ngx.status)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
ngx.var.skylink = res.body
|
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
|
local cache_ttl = 300 -- 5 minutes cache expire time
|
||||||
cache:set(ngx.var.host, ngx.var.skylink, cache_ttl)
|
cache:set(ngx.var.host, res.body, cache_ttl)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
ngx.var.skylink = cache_value
|
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
|
end
|
||||||
|
|
||||||
ngx.var.skylink = require("skynet.skylink").parse(ngx.var.skylink)
|
ngx.var.skylink = require("skynet.skylink").parse(ngx.var.skylink)
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/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
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/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
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/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
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/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
|
||||||
|
|
||||||
|
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 "$@"
|
|
@ -21,7 +21,7 @@ local function divide_string( str, max )
|
||||||
|
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
local function number_to_bit( num, length )
|
local function number_to_bit( num, length )
|
||||||
local bits = {}
|
local bits = {}
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ function basexx.to_basexx( str, alphabet, bits, pad )
|
||||||
end
|
end
|
||||||
|
|
||||||
table.insert( result, pad )
|
table.insert( result, pad )
|
||||||
return table.concat( result )
|
return table.concat( result )
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
@ -225,16 +225,16 @@ local function length_error( len, d )
|
||||||
end
|
end
|
||||||
|
|
||||||
local z85Decoder = { 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00,
|
local z85Decoder = { 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00,
|
||||||
0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45,
|
0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45,
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||||
0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47,
|
0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47,
|
||||||
0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
|
0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
|
||||||
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
|
||||||
0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
|
0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
|
||||||
0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00,
|
0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00,
|
||||||
0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
|
0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
|
||||||
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
||||||
0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
|
0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
|
||||||
0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 }
|
0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 }
|
||||||
|
|
||||||
function basexx.from_z85( str, ignore )
|
function basexx.from_z85( str, ignore )
|
||||||
|
|
|
@ -59,7 +59,9 @@ function _M.exit_access_forbidden(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
function _M.accounts_enabled()
|
function _M.accounts_enabled()
|
||||||
return os.getenv("PORTAL_MODULES"):match("a") ~= nil
|
local skynet_modules = require("skynet.modules")
|
||||||
|
|
||||||
|
return skynet_modules.is_enabled("a")
|
||||||
end
|
end
|
||||||
|
|
||||||
function _M.get_account_limits()
|
function _M.get_account_limits()
|
||||||
|
@ -74,15 +76,23 @@ function _M.get_account_limits()
|
||||||
|
|
||||||
if ngx.var.account_limits == "" then
|
if ngx.var.account_limits == "" then
|
||||||
local httpc = require("resty.http").new()
|
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)
|
-- 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/user/limits?unit=byte", {
|
local res, err = httpc:request_uri(uri .. "?unit=byte", {
|
||||||
headers = auth_headers,
|
headers = auth_headers,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- fail gracefully in case /user/limits failed
|
-- fail gracefully in case /user/limits failed
|
||||||
if err or (res and res.status ~= ngx.HTTP_OK) then
|
if err or (res and res.status ~= ngx.HTTP_OK) then
|
||||||
ngx.log(ngx.ERR, "Failed accounts service request /user/limits?unit=byte: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
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)
|
ngx.var.account_limits = cjson.encode(anon_limits)
|
||||||
elseif res and res.status == ngx.HTTP_OK then
|
elseif res and res.status == ngx.HTTP_OK then
|
||||||
ngx.var.account_limits = res.body
|
ngx.var.account_limits = res.body
|
||||||
|
@ -109,7 +119,7 @@ function _M.has_subscription()
|
||||||
end
|
end
|
||||||
|
|
||||||
function _M.is_auth_required()
|
function _M.is_auth_required()
|
||||||
-- authentication is required if mode is set to "authenticated"
|
-- authentication is required if mode is set to "authenticated"
|
||||||
-- or "subscription" (require active subscription to a premium plan)
|
-- or "subscription" (require active subscription to a premium plan)
|
||||||
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "authenticated" or _M.is_subscription_required()
|
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "authenticated" or _M.is_subscription_required()
|
||||||
end
|
end
|
||||||
|
@ -118,7 +128,7 @@ function _M.is_subscription_required()
|
||||||
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "subscription"
|
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "subscription"
|
||||||
end
|
end
|
||||||
|
|
||||||
function is_access_always_allowed()
|
local is_access_always_allowed = function ()
|
||||||
-- options requests do not attach cookies - should always be available
|
-- options requests do not attach cookies - should always be available
|
||||||
-- requests should not be limited based on accounts if accounts are not enabled
|
-- requests should not be limited based on accounts if accounts are not enabled
|
||||||
return ngx.req.get_method() == "OPTIONS" or not _M.accounts_enabled()
|
return ngx.req.get_method() == "OPTIONS" or not _M.accounts_enabled()
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
|
@ -0,0 +1,95 @@
|
||||||
|
-- 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)
|
|
@ -0,0 +1,26 @@
|
||||||
|
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
|
|
@ -0,0 +1,119 @@
|
||||||
|
-- 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)
|
|
@ -27,7 +27,7 @@ function _M.hash(skylink)
|
||||||
|
|
||||||
-- parse with blake2b with key length of 32
|
-- parse with blake2b with key length of 32
|
||||||
local blake2bHashed = hasher.blake2b(rawMerkleRoot, 32)
|
local blake2bHashed = hasher.blake2b(rawMerkleRoot, 32)
|
||||||
|
|
||||||
-- hex encode the blake hash
|
-- hex encode the blake hash
|
||||||
local hexHashed = basexx.to_hex(blake2bHashed)
|
local hexHashed = basexx.to_hex(blake2bHashed)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ describe("parse", function()
|
||||||
it("should return unchanged base64 skylink", function()
|
it("should return unchanged base64 skylink", function()
|
||||||
assert.is.same(skynet_skylink.parse(base64), base64)
|
assert.is.same(skynet_skylink.parse(base64), base64)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should transform base32 skylink into base64", function()
|
it("should transform base32 skylink into base64", function()
|
||||||
assert.is.same(skynet_skylink.parse(base32), base64)
|
assert.is.same(skynet_skylink.parse(base32), base64)
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
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
|
|
@ -0,0 +1,584 @@
|
||||||
|
-- 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,13 +1,20 @@
|
||||||
local _M = {}
|
local _M = {}
|
||||||
|
|
||||||
|
local ngx_base64 = require("ngx.base64")
|
||||||
|
local utils = require("utils")
|
||||||
|
|
||||||
function _M.authorization_header()
|
function _M.authorization_header()
|
||||||
-- read api password from env variable
|
-- read api password from env variable
|
||||||
local apipassword = os.getenv("SIA_API_PASSWORD")
|
local apipassword = utils.getenv("SIA_API_PASSWORD")
|
||||||
-- if api password is not available as env variable, read it from disk
|
-- if api password is not available as env variable, read it from disk
|
||||||
if apipassword == nil or apipassword == "" then
|
if not apipassword then
|
||||||
-- open apipassword file for reading (b flag is required for some reason)
|
-- open apipassword file for reading (b flag is required for some reason)
|
||||||
-- (file /etc/.sia/apipassword has to be mounted from the host system)
|
-- (file /etc/.sia/apipassword has to be mounted from the host system)
|
||||||
local apipassword_file = io.open("/data/sia/apipassword", "rb")
|
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)
|
-- read apipassword file contents and trim newline (important)
|
||||||
apipassword = apipassword_file:read("*all"):gsub("%s+", "")
|
apipassword = apipassword_file:read("*all"):gsub("%s+", "")
|
||||||
-- make sure to close file after reading the password
|
-- make sure to close file after reading the password
|
||||||
|
@ -15,7 +22,7 @@ function _M.authorization_header()
|
||||||
end
|
end
|
||||||
-- encode the user:password authorization string
|
-- encode the user:password authorization string
|
||||||
-- (in our case user is empty so it is just :password)
|
-- (in our case user is empty so it is just :password)
|
||||||
local content = require("ngx.base64").encode_base64url(":" .. apipassword)
|
local content = ngx_base64.encode_base64url(":" .. apipassword)
|
||||||
-- set authorization header with proper base64 encoded string
|
-- set authorization header with proper base64 encoded string
|
||||||
return "Basic " .. content
|
return "Basic " .. content
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
-- 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)
|
|
@ -42,4 +42,42 @@ function _M.extract_cookie_value(cookie_string, name_matcher)
|
||||||
return string.sub(cookie, value_start)
|
return string.sub(cookie, value_start)
|
||||||
end
|
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
|
return _M
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
-- luacheck: ignore os
|
||||||
|
|
||||||
local utils = require('utils')
|
local utils = require('utils')
|
||||||
|
|
||||||
describe("is_table_empty", function()
|
describe("is_table_empty", function()
|
||||||
|
@ -15,31 +17,31 @@ describe("extract_cookie", function()
|
||||||
|
|
||||||
it("should return nil if cookie string is nil", function()
|
it("should return nil if cookie string is nil", function()
|
||||||
local cookie = utils.extract_cookie_value(nil, "aaa")
|
local cookie = utils.extract_cookie_value(nil, "aaa")
|
||||||
|
|
||||||
assert.is_nil(cookie)
|
assert.is_nil(cookie)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should return nil if cookie name is not found", function()
|
it("should return nil if cookie name is not found", function()
|
||||||
local cookie = utils.extract_cookie(cookie_string, "foo")
|
local cookie = utils.extract_cookie(cookie_string, "foo")
|
||||||
|
|
||||||
assert.is_nil(cookie)
|
assert.is_nil(cookie)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should return cookie if cookie_string starts with that cookie name", function()
|
it("should return cookie if cookie_string starts with that cookie name", function()
|
||||||
local cookie = utils.extract_cookie(cookie_string, "aaa")
|
local cookie = utils.extract_cookie(cookie_string, "aaa")
|
||||||
|
|
||||||
assert.are.equals(cookie, "aaa=bbb")
|
assert.are.equals(cookie, "aaa=bbb")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should return cookie if cookie_string ends with that cookie name", function()
|
it("should return cookie if cookie_string ends with that cookie name", function()
|
||||||
local cookie = utils.extract_cookie(cookie_string, "xxx")
|
local cookie = utils.extract_cookie(cookie_string, "xxx")
|
||||||
|
|
||||||
assert.are.equals(cookie, "xxx=yyy")
|
assert.are.equals(cookie, "xxx=yyy")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should return cookie with custom matcher", function()
|
it("should return cookie with custom matcher", function()
|
||||||
local cookie = utils.extract_cookie(cookie_string, "skynet[-]jwt")
|
local cookie = utils.extract_cookie(cookie_string, "skynet[-]jwt")
|
||||||
|
|
||||||
assert.are.equals(cookie, "skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
|
assert.are.equals(cookie, "skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
@ -49,31 +51,165 @@ describe("extract_cookie_value", function()
|
||||||
|
|
||||||
it("should return nil if cookie string is nil", function()
|
it("should return nil if cookie string is nil", function()
|
||||||
local value = utils.extract_cookie_value(nil, "aaa")
|
local value = utils.extract_cookie_value(nil, "aaa")
|
||||||
|
|
||||||
assert.is_nil(value)
|
assert.is_nil(value)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should return nil if cookie name is not found", function()
|
it("should return nil if cookie name is not found", function()
|
||||||
local value = utils.extract_cookie_value(cookie_string, "foo")
|
local value = utils.extract_cookie_value(cookie_string, "foo")
|
||||||
|
|
||||||
assert.is_nil(value)
|
assert.is_nil(value)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should return value if cookie_string starts with that cookie name", function()
|
it("should return value if cookie_string starts with that cookie name", function()
|
||||||
local value = utils.extract_cookie_value(cookie_string, "aaa")
|
local value = utils.extract_cookie_value(cookie_string, "aaa")
|
||||||
|
|
||||||
assert.are.equals(value, "bbb")
|
assert.are.equals(value, "bbb")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should return cookie if cookie_string ends with that cookie name", function()
|
it("should return cookie if cookie_string ends with that cookie name", function()
|
||||||
local value = utils.extract_cookie_value(cookie_string, "xxx")
|
local value = utils.extract_cookie_value(cookie_string, "xxx")
|
||||||
|
|
||||||
assert.are.equals(value, "yyy")
|
assert.are.equals(value, "yyy")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("should return cookie with custom matcher", function()
|
it("should return cookie with custom matcher", function()
|
||||||
local value = utils.extract_cookie_value(cookie_string, "skynet[-]jwt")
|
local value = utils.extract_cookie_value(cookie_string, "skynet[-]jwt")
|
||||||
|
|
||||||
assert.are.equals(value, "MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
|
assert.are.equals(value, "MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
|
||||||
end)
|
end)
|
||||||
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)
|
||||||
|
|
1106
docker/nginx/mo
1106
docker/nginx/mo
File diff suppressed because it is too large
Load Diff
|
@ -19,6 +19,9 @@
|
||||||
user root;
|
user root;
|
||||||
worker_processes auto;
|
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;
|
||||||
#error_log logs/error.log notice;
|
#error_log logs/error.log notice;
|
||||||
#error_log logs/error.log info;
|
#error_log logs/error.log info;
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
exclude = {
|
||||||
|
"/usr/local/openresty", -- internal openresty libraries
|
||||||
|
"rbusted", -- busted executable
|
||||||
|
"basexx", -- external library https://github.com/aiq/basexx
|
||||||
|
}
|
||||||
|
includeuntestedfiles = true
|
|
@ -0,0 +1,11 @@
|
||||||
|
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"]
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Running tests locally
|
||||||
|
|
||||||
|
`docker run -v $(pwd)/docker/nginx/libs:/usr/local/openresty/site/lualib --rm -it $(docker build -q docker/nginx/testing)`
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env resty
|
||||||
|
|
||||||
|
setmetatable(_G, nil)
|
||||||
|
|
||||||
|
pcall(require, "luarocks.loader")
|
||||||
|
|
||||||
|
-- Busted command-line runner
|
||||||
|
require "busted.runner"({ standalone = false })
|
|
@ -5,12 +5,12 @@ ENV GOARCH amd64
|
||||||
|
|
||||||
ARG branch=portal-latest
|
ARG branch=portal-latest
|
||||||
|
|
||||||
RUN git clone https://gitlab.com/SkynetLabs/skyd.git Sia --single-branch --branch ${branch}
|
RUN git clone https://gitlab.com/SkynetLabs/skyd.git Sia --single-branch --branch ${branch} && \
|
||||||
RUN make release --directory Sia
|
make release --directory Sia
|
||||||
|
|
||||||
FROM nebulouslabs/sia:latest
|
FROM nebulouslabs/sia:1.5.6
|
||||||
|
|
||||||
COPY --from=sia-builder /go/bin/ /usr/bin/
|
COPY --from=sia-builder /go/bin/ /usr/bin/
|
||||||
|
|
||||||
RUN mv /usr/bin/skyd /usr/bin/siad || true && \
|
RUN if [ -f "/usr/bin/skyd" ]; then mv /usr/bin/skyd /usr/bin/siad; fi && \
|
||||||
mv /usr/bin/skyc /usr/bin/siac || true
|
if [ -f "/usr/bin/skyc" ]; then mv /usr/bin/skyc /usr/bin/siac; fi
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
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"]
|
|
@ -8,3 +8,23 @@ This is a Gatsby application. To run it locally, all you need is:
|
||||||
|
|
||||||
- `yarn install`
|
- `yarn install`
|
||||||
- `yarn start`
|
- `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,4 +1,7 @@
|
||||||
import * as React from "react";
|
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/300.css"; // light
|
||||||
import "@fontsource/sora/400.css"; // normal
|
import "@fontsource/sora/400.css"; // normal
|
||||||
import "@fontsource/sora/500.css"; // medium
|
import "@fontsource/sora/500.css"; // medium
|
||||||
|
@ -6,8 +9,24 @@ import "@fontsource/sora/600.css"; // semibold
|
||||||
import "@fontsource/source-sans-pro/400.css"; // normal
|
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||||
import "@fontsource/source-sans-pro/600.css"; // semibold
|
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||||
import "./src/styles/global.css";
|
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 }) {
|
export function wrapPageElement({ element, props }) {
|
||||||
const Layout = element.type.Layout ?? React.Fragment;
|
const Layout = element.type.Layout ?? React.Fragment;
|
||||||
return <Layout {...props}>{element}</Layout>;
|
return (
|
||||||
|
<PortalSettingsProvider>
|
||||||
|
<SWRConfig value={swrConfig}>
|
||||||
|
<Elements stripe={stripePromise}>
|
||||||
|
<Layout {...props}>
|
||||||
|
{element}
|
||||||
|
<div id={MODAL_ROOT_ID} />
|
||||||
|
</Layout>
|
||||||
|
</Elements>
|
||||||
|
</SWRConfig>
|
||||||
|
</PortalSettingsProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
|
require("dotenv").config({
|
||||||
|
path: `.env.${process.env.NODE_ENV}`,
|
||||||
|
});
|
||||||
|
|
||||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||||
|
|
||||||
|
const { GATSBY_PORTAL_DOMAIN } = process.env;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
siteMetadata: {
|
siteMetadata: {
|
||||||
title: `Accounts Dashboard`,
|
title: `Account Dashboard`,
|
||||||
siteUrl: `https://www.yourdomain.tld`,
|
siteUrl: `https://account.${GATSBY_PORTAL_DOMAIN}`,
|
||||||
},
|
},
|
||||||
|
pathPrefix: "/v2",
|
||||||
|
trailingSlash: "never",
|
||||||
plugins: [
|
plugins: [
|
||||||
"gatsby-plugin-image",
|
"gatsby-plugin-image",
|
||||||
"gatsby-plugin-provide-react",
|
"gatsby-plugin-provide-react",
|
||||||
|
@ -17,19 +25,33 @@ module.exports = {
|
||||||
resolve: "gatsby-source-filesystem",
|
resolve: "gatsby-source-filesystem",
|
||||||
options: {
|
options: {
|
||||||
name: "images",
|
name: "images",
|
||||||
path: "./src/images/",
|
path: "./static/images/",
|
||||||
},
|
},
|
||||||
__key: "images",
|
__key: "images",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
developMiddleware: (app) => {
|
developMiddleware: (app) => {
|
||||||
|
// Proxy Accounts service API requests:
|
||||||
app.use(
|
app.use(
|
||||||
"/api/",
|
"/api/",
|
||||||
createProxyMiddleware({
|
createProxyMiddleware({
|
||||||
target: "https://account.siasky.net",
|
target: `https://account.${GATSBY_PORTAL_DOMAIN}`,
|
||||||
secure: false, // Do not reject self-signed certificates.
|
secure: false, // Do not reject self-signed certificates.
|
||||||
changeOrigin: true,
|
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,4 +1,7 @@
|
||||||
import * as React from "react";
|
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/300.css"; // light
|
||||||
import "@fontsource/sora/400.css"; // normal
|
import "@fontsource/sora/400.css"; // normal
|
||||||
import "@fontsource/sora/500.css"; // medium
|
import "@fontsource/sora/500.css"; // medium
|
||||||
|
@ -6,8 +9,24 @@ import "@fontsource/sora/600.css"; // semibold
|
||||||
import "@fontsource/source-sans-pro/400.css"; // normal
|
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||||
import "@fontsource/source-sans-pro/600.css"; // semibold
|
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||||
import "./src/styles/global.css";
|
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 }) {
|
export function wrapPageElement({ element, props }) {
|
||||||
const Layout = element.type.Layout ?? React.Fragment;
|
const Layout = element.type.Layout ?? React.Fragment;
|
||||||
return <Layout {...props}>{element}</Layout>;
|
return (
|
||||||
|
<PortalSettingsProvider>
|
||||||
|
<SWRConfig value={swrConfig}>
|
||||||
|
<Elements stripe={stripePromise}>
|
||||||
|
<Layout {...props}>
|
||||||
|
{element}
|
||||||
|
<div id={MODAL_ROOT_ID} />
|
||||||
|
</Layout>
|
||||||
|
</Elements>
|
||||||
|
</SWRConfig>
|
||||||
|
</PortalSettingsProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,10 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"develop": "gatsby develop",
|
"develop": "gatsby develop",
|
||||||
|
"develop:secure": "dotenv -e .env.development -- gatsby develop --https -p=443",
|
||||||
"start": "gatsby develop",
|
"start": "gatsby develop",
|
||||||
"build": "gatsby build",
|
"build": "gatsby build --prefix-paths",
|
||||||
"serve": "gatsby serve",
|
"serve": "gatsby serve --prefix-paths",
|
||||||
"clean": "gatsby clean",
|
"clean": "gatsby clean",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"prettier": "prettier .",
|
"prettier": "prettier .",
|
||||||
|
@ -21,24 +22,28 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/sora": "^4.5.3",
|
"@fontsource/sora": "^4.5.3",
|
||||||
"@fontsource/source-sans-pro": "^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",
|
"classnames": "^2.3.1",
|
||||||
"copy-text-to-clipboard": "^3.0.1",
|
"copy-text-to-clipboard": "^3.0.1",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
|
"formik": "^2.2.9",
|
||||||
"gatsby": "^4.6.2",
|
"gatsby": "^4.6.2",
|
||||||
"gatsby-plugin-postcss": "^5.7.0",
|
"gatsby-plugin-postcss": "^5.7.0",
|
||||||
"http-status-codes": "^2.2.0",
|
"http-status-codes": "^2.2.0",
|
||||||
|
"ky": "^0.30.0",
|
||||||
"nanoid": "^3.3.1",
|
"nanoid": "^3.3.1",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"postcss": "^8.4.6",
|
"postcss": "^8.4.6",
|
||||||
"pretty-bytes": "^6.0.0",
|
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-dropzone": "^12.0.4",
|
"react-dropzone": "^12.0.4",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-use": "^17.3.2",
|
"react-use": "^17.3.2",
|
||||||
"skynet-js": "^3.0.2",
|
"skynet-js": "4.0.27-beta",
|
||||||
"swr": "^1.2.2",
|
"swr": "^1.2.2",
|
||||||
"tailwindcss": "^3.0.23"
|
"tailwindcss": "^3.0.23",
|
||||||
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.4",
|
"@babel/core": "^7.17.4",
|
||||||
|
@ -56,6 +61,8 @@
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
"babel-plugin-preval": "^5.1.0",
|
"babel-plugin-preval": "^5.1.0",
|
||||||
"babel-plugin-styled-components": "^2.0.2",
|
"babel-plugin-styled-components": "^2.0.2",
|
||||||
|
"dotenv": "^16.0.0",
|
||||||
|
"dotenv-cli": "^5.1.0",
|
||||||
"eslint": "^8.9.0",
|
"eslint": "^8.9.0",
|
||||||
"eslint-config-react-app": "^7.0.0",
|
"eslint-config-react-app": "^7.0.0",
|
||||||
"eslint-plugin-storybook": "^0.5.6",
|
"eslint-plugin-storybook": "^0.5.6",
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./APIKeyList";
|
|
@ -0,0 +1,43 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,41 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
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",
|
||||||
|
}),
|
||||||
|
}))``;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./Alert";
|
|
@ -1,16 +1,16 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
import { useUser } from "../../contexts/user";
|
||||||
import { SimpleUploadIcon } from "../Icons";
|
// import { SimpleUploadIcon } from "../Icons";
|
||||||
|
|
||||||
const AVATAR_PLACEHOLDER = "/images/avatar-placeholder.svg";
|
import avatarPlaceholder from "../../../static/images/avatar-placeholder.svg";
|
||||||
|
|
||||||
export const AvatarUploader = (props) => {
|
export const AvatarUploader = (props) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const [imageUrl, setImageUrl] = useState(AVATAR_PLACEHOLDER);
|
const [imageUrl, setImageUrl] = useState(avatarPlaceholder);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImageUrl(user.avatarUrl ?? AVATAR_PLACEHOLDER);
|
setImageUrl(user.avatarUrl ?? avatarPlaceholder);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,6 +20,7 @@ export const AvatarUploader = (props) => {
|
||||||
>
|
>
|
||||||
<img src={imageUrl} className="w-[160px]" alt="" />
|
<img src={imageUrl} className="w-[160px]" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
{/* TODO: uncomment when avatar uploads work
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-4 hover:underline decoration-1 decoration-dashed underline-offset-2 decoration-gray-400"
|
className="flex items-center gap-4 hover:underline decoration-1 decoration-dashed underline-offset-2 decoration-gray-400"
|
||||||
|
@ -28,8 +29,8 @@ export const AvatarUploader = (props) => {
|
||||||
>
|
>
|
||||||
<SimpleUploadIcon size={20} className="shrink-0" /> Upload profile picture
|
<SimpleUploadIcon size={20} className="shrink-0" /> Upload profile picture
|
||||||
</button>
|
</button>
|
||||||
{/* TODO: actual uploading */}
|
|
||||||
</div>
|
</div>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,30 @@
|
||||||
|
import cn from "classnames";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary UI component for user interaction
|
* Primary UI component for user interaction
|
||||||
*/
|
*/
|
||||||
export const Button = styled.button.attrs(({ disabled, $primary }) => ({
|
export const Button = styled.button.attrs(({ as: polymorphicAs, disabled, $primary, type }) => {
|
||||||
type: "button",
|
// We want to default to type=button in most cases, but sometimes we use this component
|
||||||
className: `px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 transition-[filter]
|
// as a polymorphic one (i.e. for links), and then we should avoid setting `type` property,
|
||||||
${$primary ? "bg-primary" : "bg-white border-2 border-black"}
|
// as it breaks styling in Safari.
|
||||||
${disabled ? "saturate-50 brightness-125 cursor-default text-palette-400" : "hover:brightness-90"}`,
|
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 = {
|
Button.propTypes = {
|
||||||
/**
|
/**
|
||||||
* Is this the principal call to action on the page?
|
* Is this the principal call to action on the page?
|
||||||
|
@ -19,9 +34,14 @@ Button.propTypes = {
|
||||||
* Prevent interaction on the button
|
* Prevent interaction on the button
|
||||||
*/
|
*/
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Type of button (button / submit)
|
||||||
|
*/
|
||||||
|
type: PropTypes.oneOf(["button", "submit"]),
|
||||||
};
|
};
|
||||||
|
|
||||||
Button.defaultProps = {
|
Button.defaultProps = {
|
||||||
$primary: false,
|
$primary: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
type: "button",
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,7 @@ const TooltipContent = styled.div.attrs({
|
||||||
className: "bg-primary-light/10 text-palette-600 py-2 px-4 ",
|
className: "bg-primary-light/10 text-palette-600 py-2 px-4 ",
|
||||||
})``;
|
})``;
|
||||||
|
|
||||||
export const CopyButton = ({ value, className }) => {
|
export const CopyButton = ({ value, className, ariaLabel = "Copy" }) => {
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [timer, setTimer] = useState(null);
|
const [timer, setTimer] = useState(null);
|
||||||
|
@ -39,7 +39,7 @@ export const CopyButton = ({ value, className }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={`inline-flex relative overflow-visible pr-2 ${className ?? ""}`}>
|
<div ref={containerRef} className={`inline-flex relative overflow-visible pr-2 ${className ?? ""}`}>
|
||||||
<Button onClick={handleCopy} className={copied ? "text-primary" : ""}>
|
<Button onClick={handleCopy} className={copied ? "text-primary" : ""} aria-label={ariaLabel}>
|
||||||
<CopyIcon size={16} />
|
<CopyIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
<TooltipContainer $visible={copied}>
|
<TooltipContainer $visible={copied}>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
import { useUser } from "../../contexts/user";
|
||||||
import useActivePlan from "../../hooks/useActivePlan";
|
import useActivePlan from "../../hooks/useActivePlan";
|
||||||
|
import humanBytes from "../../lib/humanBytes";
|
||||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
|
||||||
import LatestPayment from "./LatestPayment";
|
import LatestPayment from "./LatestPayment";
|
||||||
|
@ -28,17 +29,20 @@ const CurrentPlan = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col h-full">
|
||||||
<h4>{activePlan.name}</h4>
|
<h4>{activePlan.name}</h4>
|
||||||
<div className="text-palette-400">
|
<div className="text-palette-400 justify-between flex flex-col grow">
|
||||||
{activePlan.price === 0 && <p>100GB without paying a dime! 🎉</p>}
|
{activePlan.price === 0 && activePlan.limits && (
|
||||||
|
<p>{humanBytes(activePlan.limits.storageLimit)} without paying a dime! 🎉</p>
|
||||||
|
)}
|
||||||
{activePlan.price !== 0 &&
|
{activePlan.price !== 0 &&
|
||||||
(user.subscriptionCancelAtPeriodEnd ? (
|
(user.subscriptionCancelAtPeriodEnd ? (
|
||||||
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
|
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="first-letter:uppercase">{dayjs(user.subscribedUntil).fromNow(true)} until the next payment</p>
|
<p className="first-letter:uppercase">{dayjs(user.subscribedUntil).fromNow(true)} until the next payment</p>
|
||||||
))}
|
))}
|
||||||
<LatestPayment user={user} />
|
|
||||||
|
{user.subscriptionStatus && <LatestPayment user={user} />}
|
||||||
<SuggestedPlan plans={plans} activePlan={activePlan} />
|
<SuggestedPlan plans={plans} activePlan={activePlan} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ const SuggestedPlan = ({ plans, activePlan }) => {
|
||||||
<div className="mt-7">
|
<div className="mt-7">
|
||||||
<p className="font-sans font-semibold text-xs uppercase text-primary">Discover {nextPlan.name}</p>
|
<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>
|
<p className="pt-1 text-xs sm:text-base">{nextPlan.description}</p>
|
||||||
<Button $primary as={Link} to={`/upgrade?selectedPlan=${nextPlan.id}`} className="mt-6">
|
<Button $primary as={Link} to="/payments" className="mt-6">
|
||||||
Upgrade
|
Upgrade
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import fileSize from "pretty-bytes";
|
|
||||||
import { Link } from "gatsby";
|
import { Link } from "gatsby";
|
||||||
|
import cn from "classnames";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
import { useUser } from "../../contexts/user";
|
||||||
|
@ -9,6 +9,7 @@ import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
|
||||||
import { GraphBar } from "./GraphBar";
|
import { GraphBar } from "./GraphBar";
|
||||||
import { UsageGraph } from "./UsageGraph";
|
import { UsageGraph } from "./UsageGraph";
|
||||||
|
import humanBytes from "../../lib/humanBytes";
|
||||||
|
|
||||||
const useUsageData = () => {
|
const useUsageData = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
@ -44,7 +45,7 @@ const useUsageData = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const size = (bytes) => {
|
const size = (bytes) => {
|
||||||
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 });
|
const text = humanBytes(bytes ?? 0, { precision: 0 });
|
||||||
const [value, unit] = text.split(" ");
|
const [value, unit] = text.split(" ");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -62,7 +63,9 @@ const ErrorMessage = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function CurrentUsage() {
|
export default function CurrentUsage() {
|
||||||
|
const { activePlan, plans } = useActivePlan();
|
||||||
const { usage, error, loading } = useUsageData();
|
const { usage, error, loading } = useUsageData();
|
||||||
|
const nextPlan = useMemo(() => plans.find(({ tier }) => tier > activePlan?.tier), [plans, activePlan]);
|
||||||
const storageUsage = size(usage.storageUsed);
|
const storageUsage = size(usage.storageUsed);
|
||||||
const storageLimit = size(usage.storageLimit);
|
const storageLimit = size(usage.storageLimit);
|
||||||
const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]);
|
const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]);
|
||||||
|
@ -89,19 +92,21 @@ export default function CurrentUsage() {
|
||||||
<span>{storageLimit.text}</span>
|
<span>{storageLimit.text}</span>
|
||||||
</div>
|
</div>
|
||||||
<UsageGraph>
|
<UsageGraph>
|
||||||
<GraphBar value={usage.storageUsed} limit={usage.storageLimit} label={storageUsage} />
|
<GraphBar value={usage.storageUsed} limit={usage.storageLimit} label={storageUsage} className="normal-case" />
|
||||||
<GraphBar value={usage.filesUsed} limit={usage.filesLimit} label={filesUsedLabel} />
|
<GraphBar value={usage.filesUsed} limit={usage.filesLimit} label={filesUsedLabel} />
|
||||||
</UsageGraph>
|
</UsageGraph>
|
||||||
<div className="flex place-content-between">
|
<div className="flex place-content-between">
|
||||||
<span>Files</span>
|
<span>Files</span>
|
||||||
<span className="inline-flex place-content-between w-[37%]">
|
<span className="inline-flex place-content-between w-[37%]">
|
||||||
<Link
|
<Link
|
||||||
to="/upgrade"
|
to="/payments"
|
||||||
className="text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline"
|
className={cn(
|
||||||
|
"text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline",
|
||||||
|
{ invisible: !nextPlan }
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
UPGRADE
|
UPGRADE
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
{/* TODO: proper URL */}
|
|
||||||
<span>{usage.filesLimit}</span>
|
<span>{usage.filesLimit}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,18 +12,20 @@ const BarTip = styled.span.attrs({
|
||||||
})``;
|
})``;
|
||||||
|
|
||||||
const BarLabel = styled.span.attrs({
|
const BarLabel = styled.span.attrs({
|
||||||
className: "bg-white rounded border-2 border-palette-200 px-3 whitespace-nowrap absolute shadow",
|
className: "usage-label bg-white rounded border-2 border-palette-200 px-3 whitespace-nowrap absolute shadow",
|
||||||
})`
|
})`
|
||||||
right: max(0%, ${({ $percentage }) => 100 - $percentage}%);
|
${({ $percentage }) => `
|
||||||
top: -0.5rem;
|
left: max(0%, ${$percentage}%);
|
||||||
transform: translateX(50%);
|
top: -0.5rem;
|
||||||
|
transform: translateX(-${$percentage}%);
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GraphBar = ({ value, limit, label }) => {
|
export const GraphBar = ({ value, limit, label, className }) => {
|
||||||
const percentage = typeof limit !== "number" || limit === 0 ? 0 : (value / limit) * 100;
|
const percentage = typeof limit !== "number" || limit === 0 ? 0 : (value / limit) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center">
|
<div className={`relative flex items-center ${className}`}>
|
||||||
<Bar $percentage={percentage}>
|
<Bar $percentage={percentage}>
|
||||||
<BarTip />
|
<BarTip />
|
||||||
</Bar>
|
</Bar>
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import usageGraphBg from "../../../static/images/usage-graph-bg.svg";
|
||||||
|
|
||||||
export const UsageGraph = styled.div.attrs({
|
export const UsageGraph = styled.div.attrs({
|
||||||
className: "w-full my-3 grid grid-flow-row grid-rows-2",
|
className: "w-full my-3 grid grid-flow-row grid-rows-2",
|
||||||
})`
|
})`
|
||||||
height: 146px;
|
height: 146px;
|
||||||
background: url(/images/usage-graph-bg.svg) no-repeat;
|
background: url(${usageGraphBg}) no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -7,13 +7,10 @@ import { ChevronDownIcon } from "../Icons";
|
||||||
|
|
||||||
const dropDown = keyframes`
|
const dropDown = keyframes`
|
||||||
0% {
|
0% {
|
||||||
transform: scaleY(0);
|
transform: rotateX(-90deg);
|
||||||
}
|
|
||||||
80% {
|
|
||||||
transform: scaleY(1.1);
|
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: scaleY(1);
|
transform: rotateX(0deg);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -35,10 +32,11 @@ const Flyout = styled.div.attrs(({ open }) => ({
|
||||||
bg-white shadow-md shadow-palette-200/50
|
bg-white shadow-md shadow-palette-200/50
|
||||||
${open ? "visible" : "invisible"}`,
|
${open ? "visible" : "invisible"}`,
|
||||||
}))`
|
}))`
|
||||||
|
transform-origin: top center;
|
||||||
animation: ${({ open }) =>
|
animation: ${({ open }) =>
|
||||||
open
|
open
|
||||||
? css`
|
? css`
|
||||||
${dropDown} 0.1s ease-in-out
|
${dropDown} .15s ease-in-out forwards;
|
||||||
`
|
`
|
||||||
: "none"};
|
: "none"};
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,29 +1,40 @@
|
||||||
import * as React from "react";
|
import { useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useMedia } from "react-use";
|
import { useMedia } from "react-use";
|
||||||
|
|
||||||
import theme from "../../lib/theme";
|
import theme from "../../lib/theme";
|
||||||
|
|
||||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
import { Select, SelectOption } from "../Select";
|
|
||||||
import { Switch } from "../Switch";
|
|
||||||
import { TextInputIcon } from "../TextInputIcon/TextInputIcon";
|
|
||||||
import { SearchIcon } from "../Icons";
|
|
||||||
|
|
||||||
import FileTable from "./FileTable";
|
import FileTable from "./FileTable";
|
||||||
import useFormattedFilesData from "./useFormattedFilesData";
|
import useFormattedFilesData from "./useFormattedFilesData";
|
||||||
|
import { MobileFileList } from "./MobileFileList";
|
||||||
|
import { Pagination } from "./Pagination";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
const FileList = ({ type }) => {
|
const FileList = ({ type }) => {
|
||||||
const isMediumScreenOrLarger = useMedia(`(min-width: ${theme.screens.md})`);
|
const isMediumScreenOrLarger = useMedia(`(min-width: ${theme.screens.md})`);
|
||||||
const { data, error } = useSWR(`user/${type}?pageSize=10`);
|
const [offset, setOffset] = useState(0);
|
||||||
|
const baseUrl = `user/${type}?pageSize=${PAGE_SIZE}`;
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
mutate: refreshList,
|
||||||
|
} = useSWR(`${baseUrl}&offset=${offset}`, {
|
||||||
|
revalidateOnMount: true,
|
||||||
|
});
|
||||||
const items = useFormattedFilesData(data?.items || []);
|
const items = useFormattedFilesData(data?.items || []);
|
||||||
|
const count = data?.count || 0;
|
||||||
|
|
||||||
const setFilter = (name, value) => console.log("filter", name, "set to", value);
|
// Next page preloading
|
||||||
|
const hasMoreRecords = data ? data.offset + data.pageSize < data.count : false;
|
||||||
|
const nextPageOffset = hasMoreRecords ? data.offset + data.pageSize : offset;
|
||||||
|
useSWR(`${baseUrl}&offset=${nextPageOffset}`);
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
||||||
{/* TODO: proper error message */}
|
|
||||||
{!data && !error && <ContainerLoadingIndicator />}
|
{!data && !error && <ContainerLoadingIndicator />}
|
||||||
{!data && error && <p>An error occurred while loading this data.</p>}
|
{!data && error && <p>An error occurred while loading this data.</p>}
|
||||||
{data && <p>No {type} found.</p>}
|
{data && <p>No {type} found.</p>}
|
||||||
|
@ -32,42 +43,14 @@ const FileList = ({ type }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 pt-4">
|
<>
|
||||||
<div className="flex flex-col gap-4 lg:flex-row justify-between items-center">
|
{isMediumScreenOrLarger ? (
|
||||||
<TextInputIcon
|
<FileTable onUpdated={refreshList} items={items} />
|
||||||
className="w-full lg:w-[280px] xl:w-[420px]"
|
) : (
|
||||||
placeholder="Search"
|
<MobileFileList items={items} onUpdated={refreshList} />
|
||||||
icon={<SearchIcon size={20} />}
|
)}
|
||||||
onChange={console.log.bind(console)}
|
<Pagination count={count} offset={offset} setOffset={setOffset} pageSize={PAGE_SIZE} />
|
||||||
/>
|
</>
|
||||||
<div className="flex flex-row items-center uppercase font-light text-sm gap-4">
|
|
||||||
<Switch onChange={(value) => setFilter("showSmallFiles", value)} className="mr-8">
|
|
||||||
<span className="underline decoration-dashed underline-offset-2 decoration-2 decoration-gray-300">
|
|
||||||
Show small files
|
|
||||||
</span>
|
|
||||||
</Switch>
|
|
||||||
<div>
|
|
||||||
<span className="pr-2">File type:</span>
|
|
||||||
<Select onChange={(value) => setFilter("type", value)}>
|
|
||||||
<SelectOption value="all" label="All" />
|
|
||||||
<SelectOption value="mp4" label=".mp4" />
|
|
||||||
<SelectOption value="pdf" label=".pdf" />
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="pr-2">Sort:</span>
|
|
||||||
<Select onChange={(value) => setFilter("type", value)}>
|
|
||||||
<SelectOption value="size-desc" label="Biggest size" />
|
|
||||||
<SelectOption value="size-asc" label="Smallest size" />
|
|
||||||
<SelectOption value="date-desc" label="Latest" />
|
|
||||||
<SelectOption value="date-asc" label="Oldest" />
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* TODO: mobile view (it's not tabular) */}
|
|
||||||
{isMediumScreenOrLarger ? <FileTable items={items} /> : "Mobile view"}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,110 +2,78 @@ import { CogIcon, ShareIcon } from "../Icons";
|
||||||
import { PopoverMenu } from "../PopoverMenu/PopoverMenu";
|
import { PopoverMenu } from "../PopoverMenu/PopoverMenu";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeadCell, TableRow } from "../Table";
|
import { Table, TableBody, TableCell, TableHead, TableHeadCell, TableRow } from "../Table";
|
||||||
import { CopyButton } from "../CopyButton";
|
import { CopyButton } from "../CopyButton";
|
||||||
|
import { useSkylinkOptions } from "./useSkylinkOptions";
|
||||||
|
import { useSkylinkSharing } from "./useSkylinkSharing";
|
||||||
|
|
||||||
const buildShareMenu = (item) => {
|
const SkylinkOptionsMenu = ({ skylink, onUpdated }) => {
|
||||||
return [
|
const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated });
|
||||||
{
|
|
||||||
label: "Facebook",
|
|
||||||
callback: () => {
|
|
||||||
console.info("share to Facebook", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Twitter",
|
|
||||||
callback: () => {
|
|
||||||
console.info("share to Twitter", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Discord",
|
|
||||||
callback: () => {
|
|
||||||
console.info("share to Discord", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildOptionsMenu = (item) => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: "Preview",
|
|
||||||
callback: () => {
|
|
||||||
console.info("preview", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Download",
|
|
||||||
callback: () => {
|
|
||||||
console.info("download", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Unpin",
|
|
||||||
callback: () => {
|
|
||||||
console.info("unpin", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Report",
|
|
||||||
callback: () => {
|
|
||||||
console.info("report", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function FileTable({ items }) {
|
|
||||||
return (
|
return (
|
||||||
<Table style={{ tableLayout: "fixed" }}>
|
<PopoverMenu inProgress={inProgress} options={options} openClassName="text-primary">
|
||||||
<TableHead>
|
<button aria-label="Manage this skylink">
|
||||||
<TableRow noHoverEffect>
|
<CogIcon />
|
||||||
<TableHeadCell className="w-[240px] xl:w-[360px]">Name</TableHeadCell>
|
</button>
|
||||||
<TableHeadCell className="w-[80px]">Type</TableHeadCell>
|
</PopoverMenu>
|
||||||
<TableHeadCell className="w-[80px]" align="right">
|
);
|
||||||
Size
|
};
|
||||||
</TableHeadCell>
|
|
||||||
<TableHeadCell className="w-[180px]">Uploaded</TableHeadCell>
|
|
||||||
<TableHeadCell className="hidden lg:table-cell">Skylink</TableHeadCell>
|
|
||||||
<TableHeadCell className="w-[100px]">Activity</TableHeadCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{items.map((item) => {
|
|
||||||
const { id, name, type, size, date, skylink } = item;
|
|
||||||
|
|
||||||
return (
|
const SkylinkSharingMenu = ({ skylink }) => {
|
||||||
<TableRow key={id}>
|
const { options } = useSkylinkSharing(skylink);
|
||||||
<TableCell className="w-[240px] xl:w-[360px]">{name}</TableCell>
|
|
||||||
<TableCell className="w-[80px]">{type}</TableCell>
|
return (
|
||||||
<TableCell className="w-[80px]" align="right">
|
<PopoverMenu options={options} openClassName="text-primary">
|
||||||
{size}
|
<button aria-label="Share this skylink">
|
||||||
</TableCell>
|
<ShareIcon size={22} />
|
||||||
<TableCell className="w-[180px]">{date}</TableCell>
|
</button>
|
||||||
<TableCell className="hidden lg:table-cell pr-6 !overflow-visible">
|
</PopoverMenu>
|
||||||
<div className="flex items-center">
|
);
|
||||||
<CopyButton value={skylink} className="mr-2" />
|
};
|
||||||
<span className="w-full inline-block truncate">{skylink}</span>
|
|
||||||
</div>
|
export default function FileTable({ items, onUpdated }) {
|
||||||
</TableCell>
|
return (
|
||||||
<TableCell className="w-[100px] !overflow-visible">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex text-palette-600 gap-4">
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<PopoverMenu options={buildShareMenu(item)} openClassName="text-primary">
|
<TableHead>
|
||||||
<button>
|
<TableRow noHoverEffect>
|
||||||
<ShareIcon size={22} />
|
<TableHeadCell className="w-[180px] xl:w-[360px]">Name</TableHeadCell>
|
||||||
</button>
|
<TableHeadCell className="w-[80px]">Type</TableHeadCell>
|
||||||
</PopoverMenu>
|
<TableHeadCell className="w-[100px]" align="right">
|
||||||
<PopoverMenu options={buildOptionsMenu(item)} openClassName="text-primary">
|
Size
|
||||||
<button>
|
</TableHeadCell>
|
||||||
<CogIcon />
|
<TableHeadCell className="w-[160px]">Uploaded</TableHeadCell>
|
||||||
</button>
|
<TableHeadCell className="hidden lg:table-cell">Skylink</TableHeadCell>
|
||||||
</PopoverMenu>
|
<TableHeadCell className="w-[90px]">Activity</TableHeadCell>
|
||||||
</div>
|
</TableRow>
|
||||||
</TableCell>
|
</TableHead>
|
||||||
</TableRow>
|
<TableBody>
|
||||||
);
|
{items.map((item) => {
|
||||||
})}
|
const { id, name, type, size, date, skylink } = item;
|
||||||
</TableBody>
|
|
||||||
</Table>
|
return (
|
||||||
|
<TableRow key={id}>
|
||||||
|
<TableCell className="w-[180px] xl:w-[360px]">{name}</TableCell>
|
||||||
|
<TableCell className="w-[80px]">{type}</TableCell>
|
||||||
|
<TableCell className="w-[100px]" align="right">
|
||||||
|
{size}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="w-[160px]">{date}</TableCell>
|
||||||
|
<TableCell className="hidden lg:table-cell pr-6 !overflow-visible">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CopyButton value={skylink} className="mr-2" aria-label="Copy skylink" />
|
||||||
|
<span className="w-full inline-block truncate">{skylink}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="w-[90px] !overflow-visible">
|
||||||
|
<div className="flex text-palette-600 gap-4">
|
||||||
|
<SkylinkOptionsMenu skylink={skylink} onUpdated={onUpdated} />
|
||||||
|
<SkylinkSharingMenu skylink={skylink} />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
|
import { ChevronDownIcon } from "../Icons";
|
||||||
|
import { useSkylinkSharing } from "./useSkylinkSharing";
|
||||||
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
import { useSkylinkOptions } from "./useSkylinkOptions";
|
||||||
|
|
||||||
|
const SharingMenu = ({ skylink }) => {
|
||||||
|
const { options } = useSkylinkSharing(skylink);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 bg-white px-4 py-6 w-1/2">
|
||||||
|
{options.map(({ label, callback }, index) => (
|
||||||
|
<button key={index} className="uppercase text-left" onClick={callback}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OptionsMenu = ({ skylink, onUpdated }) => {
|
||||||
|
const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative px-4 py-6 w-1/2", { "bg-primary/10": !inProgress })}>
|
||||||
|
<div className={cn("flex flex-col gap-4", { "opacity-0": inProgress })}>
|
||||||
|
{options.map(({ label, callback }, index) => (
|
||||||
|
<button key={index} className="uppercase text-left" onClick={callback}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{inProgress && (
|
||||||
|
<ContainerLoadingIndicator className="absolute inset-0 !p-0 z-50 bg-primary/10 !text-palette-200" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListItem = ({ item, onUpdated }) => {
|
||||||
|
const { name, type, size, date, skylink } = item;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const toggle = () => setOpen((open) => !open);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("p-4 flex flex-col bg-palette-100", { "bg-opacity-50": !open })}>
|
||||||
|
<div className="flex items-center gap-4 justify-between">
|
||||||
|
<div className="info flex flex-col gap-2 truncate">
|
||||||
|
<div className="truncate">{name}</div>
|
||||||
|
<div className="flex divide-x divide-palette-200 text-xs">
|
||||||
|
<span className="px-1">{type}</span>
|
||||||
|
<span className="px-1">{size}</span>
|
||||||
|
<span className="px-1">{date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={toggle}>
|
||||||
|
<ChevronDownIcon className={cn("transition-[transform]", { "-scale-100": open })} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex transition-[max-height_padding] overflow-hidden text-xs text-left font-sans tracking-normal",
|
||||||
|
{ "pt-4 max-h-[150px]": open, "pt-0 max-h-0": !open }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SharingMenu skylink={skylink} />
|
||||||
|
<OptionsMenu skylink={skylink} onUpdated={onUpdated} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileFileList = ({ items, onUpdated }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<ListItem key={item.id} item={item} onUpdated={onUpdated} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
export const Pagination = ({ count, offset, setOffset, pageSize }) => {
|
||||||
|
const start = count ? offset + 1 : 0;
|
||||||
|
const end = offset + pageSize > count ? count : offset + pageSize;
|
||||||
|
|
||||||
|
const showPaginationButtons = offset > 0 || count > end;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="px-4 py-3 flex items-center justify-between" aria-label="Pagination">
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
Showing {start} to {end} of {count} results
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{showPaginationButtons && (
|
||||||
|
<div className="flex-1 flex justify-between sm:justify-end space-x-3">
|
||||||
|
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)} className="!border-0">
|
||||||
|
Previous page
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={offset + pageSize >= count}
|
||||||
|
onClick={() => setOffset(offset + pageSize)}
|
||||||
|
className="!border-0"
|
||||||
|
>
|
||||||
|
Next page
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import prettyBytes from "pretty-bytes";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { DATE_FORMAT } from "../../lib/config";
|
||||||
|
import humanBytes from "../../lib/humanBytes";
|
||||||
|
|
||||||
const parseFileName = (fileName) => {
|
const parseFileName = (fileName) => {
|
||||||
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
|
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
|
||||||
|
@ -10,12 +11,12 @@ const parseFileName = (fileName) => {
|
||||||
|
|
||||||
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
|
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
|
||||||
const [name, type] = parseFileName(rawFileName);
|
const [name, type] = parseFileName(rawFileName);
|
||||||
const date = dayjs(uploadedOn || downloadedOn).format("MM/DD/YYYY; HH:MM");
|
const date = dayjs(uploadedOn || downloadedOn).format(DATE_FORMAT);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
date,
|
date,
|
||||||
size: prettyBytes(size),
|
size: humanBytes(size, { precision: 2 }),
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import accountsService from "../../services/accountsService";
|
||||||
|
import skynetClient from "../../services/skynetClient";
|
||||||
|
|
||||||
|
export const useSkylinkOptions = ({ skylink, onUpdated }) => {
|
||||||
|
const [inProgress, setInProgress] = useState(false);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: "Preview",
|
||||||
|
callback: async () => window.open(await skynetClient.getSkylinkUrl(skylink)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Download",
|
||||||
|
callback: () => skynetClient.downloadFile(skylink),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Unpin",
|
||||||
|
callback: async () => {
|
||||||
|
setInProgress(true);
|
||||||
|
await accountsService.delete(`user/uploads/${skylink}`);
|
||||||
|
await onUpdated(); // No need to setInProgress(false), since at this point this hook should already be unmounted
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[skylink, onUpdated]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
inProgress,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import copy from "copy-text-to-clipboard";
|
||||||
|
|
||||||
|
import skynetClient from "../../services/skynetClient";
|
||||||
|
|
||||||
|
const COPY_LINK_LABEL = "Copy link";
|
||||||
|
|
||||||
|
export const useSkylinkSharing = (skylink) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [copyLabel, setCopyLabel] = useState(COPY_LINK_LABEL);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (copied) {
|
||||||
|
setCopyLabel("Copied!");
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => setCopied(false), 1500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
} else {
|
||||||
|
setCopyLabel(COPY_LINK_LABEL);
|
||||||
|
}
|
||||||
|
}, [copied]);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: copyLabel,
|
||||||
|
callback: async () => {
|
||||||
|
setCopied(true);
|
||||||
|
copy(await skynetClient.getSkylinkUrl(skylink));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[skylink, copyLabel]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,8 +1,19 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { PageContainer } from "../PageContainer";
|
import { PageContainer } from "../PageContainer";
|
||||||
|
|
||||||
|
const FooterLink = styled.a.attrs({
|
||||||
|
className: "text-palette-400 underline decoration-dotted decoration-offset-4 decoration-1",
|
||||||
|
rel: "noreferrer",
|
||||||
|
target: "_blank",
|
||||||
|
})``;
|
||||||
|
|
||||||
export const Footer = () => (
|
export const Footer = () => (
|
||||||
<PageContainer className="font-content text-palette-300 py-4">
|
<PageContainer className="font-content text-palette-300 py-4">
|
||||||
<p>© Skynet Labs Inc. All rights reserved.</p>
|
<p>
|
||||||
|
Made by <FooterLink href="https://skynetlabs.com">Skynet Labs</FooterLink>. Open-sourced{" "}
|
||||||
|
<FooterLink href="https://github.com/SkynetLabs/skynet-webportal">on Github</FooterLink>.
|
||||||
|
</p>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { Field } from "formik";
|
||||||
|
|
||||||
|
export const TextField = ({ id, label, name, error, touched, className, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full gap-1">
|
||||||
|
{label && (
|
||||||
|
<label className="font-sans uppercase text-palette-300 text-xs" htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<Field
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
className={cn("w-full py-2 px-4 bg-palette-100 rounded-sm placeholder:text-palette-200 outline-1", className, {
|
||||||
|
"focus:outline outline-palette-200": !error || !touched,
|
||||||
|
"outline outline-error": touched && error,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{touched && error && (
|
||||||
|
<div className="text-error">
|
||||||
|
<small>{error}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Besides noted properties, it accepts all props accepted by:
|
||||||
|
* - a regular <input> element
|
||||||
|
* - Formik's <Field> component
|
||||||
|
*/
|
||||||
|
TextField.propTypes = {
|
||||||
|
/**
|
||||||
|
* ID for the field. Used to couple <label> and <input> elements
|
||||||
|
*/
|
||||||
|
id: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Label for the field
|
||||||
|
*/
|
||||||
|
label: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Name of the field
|
||||||
|
*/
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* Validation error message
|
||||||
|
*/
|
||||||
|
error: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Indicates wether or not the user touched the field already.
|
||||||
|
*/
|
||||||
|
touched: PropTypes.bool,
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./TextField";
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Link } from "gatsby";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export default styled(Link).attrs({
|
||||||
|
className: "text-primary underline-offset-2 decoration-1 decoration-dotted hover:text-primary-light hover:underline",
|
||||||
|
})``;
|
|
@ -1,6 +1,8 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { withIconProps } from "../withIconProps";
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
export const CircledCheckmarkIcon = withIconProps(({ size, ...props }) => (
|
export const CheckmarkIcon = withIconProps(({ size, circled, ...props }) => (
|
||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
|
@ -9,10 +11,15 @@ export const CircledCheckmarkIcon = withIconProps(({ size, ...props }) => (
|
||||||
shapeRendering="geometricPrecision"
|
shapeRendering="geometricPrecision"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<circle cx="16" cy="16" r="15" fill="transparent" stroke="currentColor" strokeWidth="2" />
|
{circled && <circle cx="16" cy="16" r="15" fill="transparent" stroke="currentColor" strokeWidth="2" />}
|
||||||
<polygon
|
<polygon
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
points="22.45 11.19 23.86 12.61 14.44 22.03 9.69 17.28 11.1 15.86 14.44 19.2 22.45 11.19"
|
points="22.45 11.19 23.86 12.61 14.44 22.03 9.69 17.28 11.1 15.86 14.44 19.2 22.45 11.19"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
CheckmarkIcon.propTypes = {
|
||||||
|
...CheckmarkIcon.propTypes,
|
||||||
|
circled: PropTypes.bool,
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
|
export const ImportantNoteIcon = withIconProps(({ size, ...props }) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<g fill="none" fillRule="evenodd">
|
||||||
|
<g fill="currentColor" fillRule="nonzero">
|
||||||
|
<path d="M16.028 6c5.523 0 10 4.477 10 10s-4.477 10-10 10h-9c-.405.017-.78-.212-.95-.58-.156-.372-.074-.802.21-1.09l2-2C6.82 20.549 6.02 18.31 6.028 16c0-5.523 4.477-10 10-10zm3.05 2.607c-3.526-1.458-7.592-.222-9.71 2.953-2.119 3.174-1.7 7.403 1 10.1.189.185.296.436.3.7 0 .267-.109.523-.3.71l-.93.93h6.59c3.817-.003 7.1-2.701 7.841-6.445.742-3.744-1.264-7.49-4.79-8.948zM16.028 18c.552 0 1 .448 1 1s-.448 1-1 1-1-.448-1-1 .448-1 1-1zM17 12v5h-2v-5h2z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
));
|
|
@ -4,7 +4,7 @@ export * from "./icons/LockClosedIcon";
|
||||||
export * from "./icons/SkynetLogoIcon";
|
export * from "./icons/SkynetLogoIcon";
|
||||||
export * from "./icons/ArrowRightIcon";
|
export * from "./icons/ArrowRightIcon";
|
||||||
export * from "./icons/InfoIcon";
|
export * from "./icons/InfoIcon";
|
||||||
export * from "./icons/CircledCheckmarkIcon";
|
export * from "./icons/CheckmarkIcon";
|
||||||
export * from "./icons/CircledErrorIcon";
|
export * from "./icons/CircledErrorIcon";
|
||||||
export * from "./icons/CircledProgressIcon";
|
export * from "./icons/CircledProgressIcon";
|
||||||
export * from "./icons/CircledArrowUpIcon";
|
export * from "./icons/CircledArrowUpIcon";
|
||||||
|
@ -14,3 +14,4 @@ export * from "./icons/CopyIcon";
|
||||||
export * from "./icons/ShareIcon";
|
export * from "./icons/ShareIcon";
|
||||||
export * from "./icons/SimpleUploadIcon";
|
export * from "./icons/SimpleUploadIcon";
|
||||||
export * from "./icons/TrashIcon";
|
export * from "./icons/TrashIcon";
|
||||||
|
export * from "./icons/ImportantNoteIcon";
|
||||||
|
|
|
@ -3,12 +3,13 @@ import useSWR from "swr";
|
||||||
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
||||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
import useFormattedFilesData from "../FileList/useFormattedFilesData";
|
||||||
|
|
||||||
import useFormattedActivityData from "./useFormattedActivityData";
|
import { ViewAllLink } from "./ViewAllLink";
|
||||||
|
|
||||||
export default function ActivityTable({ type }) {
|
export default function ActivityTable({ type }) {
|
||||||
const { data, error } = useSWR(`user/${type}?pageSize=3`);
|
const { data, error } = useSWR(`user/${type}?pageSize=3`);
|
||||||
const items = useFormattedActivityData(data?.items || []);
|
const items = useFormattedFilesData(data?.items || []);
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return (
|
return (
|
||||||
|
@ -22,20 +23,23 @@ export default function ActivityTable({ type }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table style={{ tableLayout: "fixed" }}>
|
<>
|
||||||
<TableBody>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
{items.map(({ id, name, type, size, date, skylink }) => (
|
<TableBody>
|
||||||
<TableRow key={id}>
|
{items.map(({ id, name, type, size, date, skylink }) => (
|
||||||
<TableCell>{name}</TableCell>
|
<TableRow key={id}>
|
||||||
<TableCell className="w-[80px]">{type}</TableCell>
|
<TableCell>{name}</TableCell>
|
||||||
<TableCell className="w-[80px]" align="right">
|
<TableCell className="w-[80px]">{type}</TableCell>
|
||||||
{size}
|
<TableCell className="w-[80px]" align="right">
|
||||||
</TableCell>
|
{size}
|
||||||
<TableCell className="w-[180px]">{date}</TableCell>
|
</TableCell>
|
||||||
<TableCell>{skylink}</TableCell>
|
<TableCell className="w-[180px]">{date}</TableCell>
|
||||||
</TableRow>
|
<TableCell>{skylink}</TableCell>
|
||||||
))}
|
</TableRow>
|
||||||
</TableBody>
|
))}
|
||||||
</Table>
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<ViewAllLink to={`/files?tab=${type}`} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,13 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link } from "gatsby";
|
|
||||||
|
|
||||||
import { Panel } from "../Panel";
|
import { Panel } from "../Panel";
|
||||||
import { Tab, TabPanel, Tabs } from "../Tabs";
|
|
||||||
import { ArrowRightIcon } from "../Icons";
|
|
||||||
|
|
||||||
import ActivityTable from "./ActivityTable";
|
import ActivityTable from "./ActivityTable";
|
||||||
|
|
||||||
const ViewAllLink = (props) => (
|
|
||||||
<Link className="inline-flex mt-6 items-center gap-3 ease-in-out hover:brightness-90" {...props}>
|
|
||||||
<span className="bg-primary rounded-full w-[32px] h-[32px] inline-flex justify-center items-center">
|
|
||||||
<ArrowRightIcon />
|
|
||||||
</span>
|
|
||||||
<span className="font-sans text-xs uppercase text-palette-400">View all</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function LatestActivity() {
|
export default function LatestActivity() {
|
||||||
return (
|
return (
|
||||||
<Panel title="Latest activity">
|
<Panel title="Latest uploads">
|
||||||
<Tabs>
|
<ActivityTable type="uploads" />
|
||||||
<Tab id="uploads" title="Uploads" />
|
|
||||||
<Tab id="downloads" title="Downloads" />
|
|
||||||
<TabPanel tabId="uploads" className="pt-4">
|
|
||||||
<ActivityTable type="uploads" />
|
|
||||||
<ViewAllLink to="/files?tab=uploads" />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel tabId="downloads" className="pt-4">
|
|
||||||
<ActivityTable type="downloads" />
|
|
||||||
<ViewAllLink to="/files?tab=downloads" />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Link } from "gatsby";
|
||||||
|
|
||||||
|
import { ArrowRightIcon } from "../Icons";
|
||||||
|
|
||||||
|
export const ViewAllLink = (props) => (
|
||||||
|
<Link className="inline-flex mt-6 items-center gap-3 ease-in-out hover:brightness-90" {...props}>
|
||||||
|
<span className="bg-primary rounded-full w-[32px] h-[32px] inline-flex justify-center items-center">
|
||||||
|
<ArrowRightIcon />
|
||||||
|
</span>
|
||||||
|
<span className="font-sans text-xs uppercase text-palette-400">View all</span>
|
||||||
|
</Link>
|
||||||
|
);
|
|
@ -1,26 +0,0 @@
|
||||||
import { useMemo } from "react";
|
|
||||||
import prettyBytes from "pretty-bytes";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
const parseFileName = (fileName) => {
|
|
||||||
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
|
|
||||||
|
|
||||||
return [fileName.substr(0, lastDotIndex), fileName.substr(lastDotIndex)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
|
|
||||||
const [name, type] = parseFileName(rawFileName);
|
|
||||||
const date = dayjs(uploadedOn || downloadedOn).format("MM/DD/YYYY; HH:MM");
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rest,
|
|
||||||
date,
|
|
||||||
size: prettyBytes(size),
|
|
||||||
type,
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const useFormattedActivityData = (items) => useMemo(() => items.map(formatItem), [items]);
|
|
||||||
|
|
||||||
export default useFormattedActivityData;
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ContainerLoadingIndicator } from "./ContainerLoadingIndicator";
|
||||||
|
|
||||||
|
export const FullScreenLoadingIndicator = () => (
|
||||||
|
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
|
||||||
|
<ContainerLoadingIndicator className="!text-palette-200/50" />
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -1 +1,2 @@
|
||||||
export * from "./ContainerLoadingIndicator";
|
export * from "./ContainerLoadingIndicator";
|
||||||
|
export * from "./FullScreenLoadingIndicator";
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { graphql, useStaticQuery } from "gatsby";
|
||||||
|
|
||||||
|
import favicon from "../../../static/favicon.ico";
|
||||||
|
import favicon16 from "../../../static/favicon-16x16.png";
|
||||||
|
import favicon32 from "../../../static/favicon-32x32.png";
|
||||||
|
import appleIcon144 from "../../../static/apple-touch-icon-144x144.png";
|
||||||
|
import appleIcon152 from "../../../static/apple-touch-icon-152x152.png";
|
||||||
|
import msTileIcon from "../../../static/mstile-144x144.png";
|
||||||
|
|
||||||
|
export const Metadata = ({ children }) => {
|
||||||
|
const { site } = useStaticQuery(
|
||||||
|
graphql`
|
||||||
|
query Q {
|
||||||
|
site {
|
||||||
|
siteMetadata {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const { title } = site.siteMetadata;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Helmet htmlAttributes={{ lang: "en" }} titleTemplate={`%s | ${title}`} defaultTitle={title}>
|
||||||
|
<meta name="application-name" content="Skynet Account Dashboard" />
|
||||||
|
<link rel="icon" type="image/x-icon" href={favicon} />
|
||||||
|
<link rel="icon" type="image/png" href={favicon16} sizes="16x16" />
|
||||||
|
<link rel="icon" type="image/png" href={favicon32} sizes="32x32" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="144x144" href={appleIcon144} />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="152x152" href={appleIcon152} />
|
||||||
|
<meta name="msapplication-TileColor" content="#FFFFFF" />
|
||||||
|
<meta name="msapplication-TileImage" content={msTileIcon} />
|
||||||
|
|
||||||
|
<meta name="description" content="Manage your Skynet uploads, account subscription, settings and API keys" />
|
||||||
|
<link rel="preconnect" href={`https://${process.env.GATSBY_PORTAL_DOMAIN}/`} />
|
||||||
|
{children}
|
||||||
|
</Helmet>
|
||||||
|
);
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue