Merge branch 'master' into sevey/composite-actions

This commit is contained in:
Matthew Sevey 2022-04-14 14:35:00 -04:00 committed by GitHub
commit ac15135099
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2140 additions and 950 deletions

View File

@ -6,48 +6,37 @@ name: Nginx Lua Unit Tests
on: on:
push: push:
branches: branches:
- "master" - master
paths:
- ".github/workflows/nginx-lua-unit-tests.yml"
- "docker/nginx/libs/**.lua"
pull_request: pull_request:
paths:
- ".github/workflows/nginx-lua-unit-tests.yml"
- "docker/nginx/libs/**.lua"
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 luarocks install hasher
source env/bin/activate
luarocks install busted luarocks install busted
luarocks install luacov luarocks install luacov
luarocks install hasher
luarocks install luacheck luarocks install luacheck
- name: Lint code - name: Lint Code With Luacheck
run: | run: luacheck docker/nginx/libs --std ngx_lua+busted
source env/bin/activate
luacheck docker/nginx/libs --std ngx_lua+busted
- name: Unit Tests - name: Run Tests With Busted
run: | # ran from root repo directory; produces luacov.stats.out file
source env/bin/activate run: docker/nginx/testing/rbusted --lpath='docker/nginx/libs/?.lua;docker/nginx/libs/?/?.lua' --verbose --coverage --pattern=spec docker/nginx/libs
busted --verbose --coverage --pattern=spec --directory=docker/nginx/libs .
cd docker/nginx/libs && luacov
- uses: codecov/codecov-action@v2 - 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: with:
directory: docker/nginx/libs root_dir: ${GITHUB_WORKSPACE}
files: ./luacov.report.out
flags: nginx-lua flags: nginx-lua

4
.gitignore vendored
View File

@ -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

View File

@ -5,8 +5,8 @@
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://docs.siasky.net/webportal-management/overview.
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

View File

@ -92,7 +92,7 @@ services:
- ./docker/data/dashboard-v2/public:/usr/app/public - ./docker/data/dashboard-v2/public:/usr/app/public
networks: networks:
shared: shared:
ipv4_address: 10.10.10.90 ipv4_address: 10.10.10.86
expose: expose:
- 9000 - 9000
depends_on: depends_on:

View File

@ -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
}

View File

@ -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())
}

View File

@ -1,55 +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
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
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
local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", error_response)
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
}

View File

@ -1,33 +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
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
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
}

View File

@ -1,55 +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
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
-- 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
local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", error_response)
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
}

View File

@ -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,26 @@ 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())
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;
@ -294,12 +307,26 @@ 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())
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 +338,21 @@ 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())
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 +399,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 +432,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 {

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,91 @@
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)
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 ~= 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)
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_upload_timer, skylink, auth_headers)
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

View File

@ -0,0 +1,550 @@
-- 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" }
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)
assert.stub(ngx.timer.at).was_called_with(
0,
skynet_tracker.track_upload_timer,
valid_skylink,
valid_auth_headers
)
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)
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)
skynet_tracker.track_upload(valid_skylink, 403, valid_auth_headers)
skynet_tracker.track_upload(valid_skylink, 490, valid_auth_headers)
skynet_tracker.track_upload(valid_skylink, 500, valid_auth_headers)
skynet_tracker.track_upload(valid_skylink, 502, valid_auth_headers)
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_upload(valid_skylink, valid_status_code, {})
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_upload(valid_skylink, valid_status_code, valid_auth_headers)
assert.stub(ngx.timer.at).was_called_with(
0,
skynet_tracker.track_upload_timer,
valid_skylink,
valid_auth_headers
)
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
)
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
)
local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
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_upload_timer(
false,
valid_skylink,
valid_auth_headers
)
local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
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/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
)
local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
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/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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()
@ -77,3 +79,137 @@ describe("extract_cookie_value", function()
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)

View File

@ -0,0 +1,6 @@
exclude = {
"/usr/local/openresty", -- internal openresty libraries
"rbusted", -- busted executable
"basexx", -- external library https://github.com/aiq/basexx
}
includeuntestedfiles = true

View File

@ -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

View File

@ -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)`

8
docker/nginx/testing/rbusted Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env resty
setmetatable(_G, nil)
pcall(require, "luarocks.loader")
-- Busted command-line runner
require "busted.runner"({ standalone = false })

View File

@ -11,15 +11,20 @@ This is a Gatsby application. To run it locally, all you need is:
## Accessing remote APIs ## Accessing remote APIs
To be able to log in on a local environment with your production credentials, you'll need to make the browser believe you're actually on the same domain, otherwise the browser will block the session cookie. 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.
To do the trick, edit your `/etc/hosts` file and add a record like this: Example:
``` ```env
127.0.0.1 local.skynetpro.net GATSBY_PORTAL_DOMAIN=skynetfree.net # Use skynetfree.net APIs
GATSBY_HOST=local.skynetfree.net # Address of your local build
``` ```
then run `yarn develop:secure` -- it will run `gatsby develop` with `--https --host=local.skynetpro.net -p=443` options. > It's recommended to keep the 2LD the same, so any cookies dispatched by the API work without issues.
If you're on macOS, you may need to `sudo` the command to successfully bind to port `443`.
> **NOTE:** This should become easier once we have a docker image for the new dashboard. 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.

View File

@ -1,4 +1,5 @@
import * as React from "react"; import * as React from "react";
import { SWRConfig } from "swr";
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,6 +7,7 @@ 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 { MODAL_ROOT_ID } from "./src/components/Modal";
import { PortalSettingsProvider } from "./src/contexts/portal-settings"; import { PortalSettingsProvider } from "./src/contexts/portal-settings";
@ -13,10 +15,12 @@ export function wrapPageElement({ element, props }) {
const Layout = element.type.Layout ?? React.Fragment; const Layout = element.type.Layout ?? React.Fragment;
return ( return (
<PortalSettingsProvider> <PortalSettingsProvider>
<SWRConfig value={swrConfig}>
<Layout {...props}> <Layout {...props}>
{element} {element}
<div id={MODAL_ROOT_ID} /> <div id={MODAL_ROOT_ID} />
</Layout> </Layout>
</SWRConfig>
</PortalSettingsProvider> </PortalSettingsProvider>
); );
} }

View File

@ -1,9 +1,15 @@
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: "Skynet Account", title: `Account Dashboard`,
siteUrl: `https://account.${process.env.GATSBY_PORTAL_DOMAIN}/`, siteUrl: `https://account.${GATSBY_PORTAL_DOMAIN}`,
}, },
trailingSlash: "never", trailingSlash: "never",
plugins: [ plugins: [
@ -24,13 +30,27 @@ module.exports = {
}, },
], ],
developMiddleware: (app) => { developMiddleware: (app) => {
// Proxy Accounts service API requests:
app.use( app.use(
"/api/", "/api/",
createProxyMiddleware({ createProxyMiddleware({
target: "https://account.skynetpro.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": "",
},
})
);
}, },
}; };

View File

@ -1,4 +1,5 @@
import * as React from "react"; import * as React from "react";
import { SWRConfig } from "swr";
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,6 +7,7 @@ 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 { MODAL_ROOT_ID } from "./src/components/Modal";
import { PortalSettingsProvider } from "./src/contexts/portal-settings"; import { PortalSettingsProvider } from "./src/contexts/portal-settings";
@ -13,10 +15,12 @@ export function wrapPageElement({ element, props }) {
const Layout = element.type.Layout ?? React.Fragment; const Layout = element.type.Layout ?? React.Fragment;
return ( return (
<PortalSettingsProvider> <PortalSettingsProvider>
<SWRConfig value={swrConfig}>
<Layout {...props}> <Layout {...props}>
{element} {element}
<div id={MODAL_ROOT_ID} /> <div id={MODAL_ROOT_ID} />
</Layout> </Layout>
</SWRConfig>
</PortalSettingsProvider> </PortalSettingsProvider>
); );
} }

View File

@ -9,7 +9,7 @@
], ],
"scripts": { "scripts": {
"develop": "gatsby develop", "develop": "gatsby develop",
"develop:secure": "gatsby develop --https --host=local.skynetpro.net -p=443", "develop:secure": "dotenv -e .env.development -- gatsby develop --https -p=443",
"start": "gatsby develop", "start": "gatsby develop",
"build": "gatsby build", "build": "gatsby build",
"serve": "gatsby serve", "serve": "gatsby serve",
@ -60,6 +60,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",

View File

@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
import { Alert } from "../Alert"; import { Alert } from "../Alert";
import { Button } from "../Button"; import { Button } from "../Button";
import { AddSkylinkToAPIKeyForm } from "../forms/AddSkylinkToAPIKeyForm"; import { AddSkylinkToSponsorKeyForm } from "../forms/AddSkylinkToSponsorKeyForm";
import { CogIcon, TrashIcon } from "../Icons"; import { CogIcon, TrashIcon } from "../Icons";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
@ -13,7 +13,7 @@ import { useAPIKeyRemoval } from "./useAPIKeyRemoval";
export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => { export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
const { id, name, createdAt, skylinks } = apiKey; const { id, name, createdAt, skylinks } = apiKey;
const isPublic = apiKey.public === "true"; const isSponsorKey = apiKey.public === "true";
const [error, setError] = useState(null); const [error, setError] = useState(null);
const onSkylinkListEdited = useCallback(() => { const onSkylinkListEdited = useCallback(() => {
@ -53,9 +53,9 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
}, [abortEdit]); }, [abortEdit]);
const skylinksNumber = skylinks?.length ?? 0; const skylinksNumber = skylinks?.length ?? 0;
const isNotConfigured = isPublic && skylinksNumber === 0; const isNotConfigured = isSponsorKey && skylinksNumber === 0;
const skylinksPhrasePrefix = skylinksNumber === 0 ? "No" : skylinksNumber; const skylinksPhrasePrefix = skylinksNumber === 0 ? "No" : skylinksNumber;
const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} configured`; const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} sponsored`;
return ( return (
<li <li
@ -66,6 +66,7 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
<span className="col-span-2 sm:col-span-1 flex items-center"> <span className="col-span-2 sm:col-span-1 flex items-center">
<span className="flex flex-col"> <span className="flex flex-col">
<span className={cn("truncate", { "text-palette-300": !name })}>{name || "unnamed key"}</span> <span className={cn("truncate", { "text-palette-300": !name })}>{name || "unnamed key"}</span>
{isSponsorKey && (
<button <button
onClick={promptEdit} onClick={promptEdit}
className={cn("text-xs hover:underline decoration-dotted", { className={cn("text-xs hover:underline decoration-dotted", {
@ -75,12 +76,13 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
> >
{skylinksPhrase} {skylinksPhrase}
</button> </button>
)}
</span> </span>
</span> </span>
<span className="col-span-2 my-4 border-t border-t-palette-200/50 sm:hidden" /> <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="text-palette-400">{dayjs(createdAt).format("MMM DD, YYYY")}</span>
<span className="flex items-center justify-end"> <span className="flex items-center justify-end">
{isPublic && ( {isSponsorKey && (
<button <button
title="Add or remove skylinks" title="Add or remove skylinks"
aria-label="Add or remove skylinks" aria-label="Add or remove skylinks"
@ -119,7 +121,7 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
)} )}
{editInitiated && ( {editInitiated && (
<Modal onClose={closeEditModal} className="flex flex-col gap-4 text-center sm:px-8 sm:py-6"> <Modal onClose={closeEditModal} className="flex flex-col gap-4 text-center sm:px-8 sm:py-6">
<h4>Covered skylinks</h4> <h4>Sponsored skylinks</h4>
{skylinks?.length > 0 ? ( {skylinks?.length > 0 ? (
<ul className="text-xs flex flex-col gap-2"> <ul className="text-xs flex flex-col gap-2">
{skylinks.map((skylink) => ( {skylinks.map((skylink) => (
@ -143,7 +145,7 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{error && <Alert $variant="error">{error}</Alert>} {error && <Alert $variant="error">{error}</Alert>}
<AddSkylinkToAPIKeyForm addSkylink={addSkylink} /> <AddSkylinkToSponsorKeyForm addSkylink={addSkylink} />
</div> </div>
<div className="flex gap-4 justify-center mt-4"> <div className="flex gap-4 justify-center mt-4">
<Button onClick={closeEditModal}>Close</Button> <Button onClick={closeEditModal}>Close</Button>

View File

@ -1,7 +1,7 @@
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"; const AVATAR_PLACEHOLDER = "/images/avatar-placeholder.svg";
@ -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>
); );
}; };

View File

@ -1,5 +1,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import prettyBytes from "pretty-bytes";
import { useUser } from "../../contexts/user"; import { useUser } from "../../contexts/user";
import useActivePlan from "../../hooks/useActivePlan"; import useActivePlan from "../../hooks/useActivePlan";
@ -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>{prettyBytes(activePlan.limits.storageLimit, { binary: true })} 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>

View File

@ -44,7 +44,7 @@ const useUsageData = () => {
}; };
const size = (bytes) => { const size = (bytes) => {
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 }); const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0, binary: true });
const [value, unit] = text.split(" "); const [value, unit] = text.split(" ");
return { return {

View File

@ -12,11 +12,13 @@ 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 }) => `
left: max(0%, ${$percentage}%);
top: -0.5rem; top: -0.5rem;
transform: translateX(50%); transform: translateX(-${$percentage}%);
`}
`; `;
export const GraphBar = ({ value, limit, label }) => { export const GraphBar = ({ value, limit, label }) => {

View File

@ -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"};
`; `;

View File

@ -1,6 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { DATE_FORMAT } from "../../lib/config";
const parseFileName = (fileName) => { const parseFileName = (fileName) => {
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity; const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
@ -10,7 +11,7 @@ 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,

View File

@ -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>
); );

View File

@ -2,5 +2,5 @@ import { Link } from "gatsby";
import styled from "styled-components"; import styled from "styled-components";
export default styled(Link).attrs({ export default styled(Link).attrs({
className: "text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline", className: "text-primary underline-offset-2 decoration-1 decoration-dotted hover:text-primary-light hover:underline",
})``; })``;

View File

@ -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,6 +23,7 @@ export default function ActivityTable({ type }) {
} }
return ( return (
<>
<Table style={{ tableLayout: "fixed" }}> <Table style={{ tableLayout: "fixed" }}>
<TableBody> <TableBody>
{items.map(({ id, name, type, size, date, skylink }) => ( {items.map(({ id, name, type, size, date, skylink }) => (
@ -37,5 +39,7 @@ export default function ActivityTable({ type }) {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
<ViewAllLink to={`/files?tab=${type}`} />
</>
); );
} }

View File

@ -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>
<Tab id="uploads" title="Uploads" />
<Tab id="downloads" title="Downloads" />
<TabPanel tabId="uploads" className="pt-4">
<ActivityTable type="uploads" /> <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>
); );
} }

View File

@ -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>
);

View File

@ -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;

View File

@ -97,7 +97,7 @@ export const NavBar = () => {
as="button" as="button"
onClick={onLogout} onClick={onLogout}
activeClassName="text-primary" activeClassName="text-primary"
className="cursor-pointer" className="cursor-pointer w-full"
icon={LockClosedIcon} icon={LockClosedIcon}
label="Log out" label="Log out"
/> />

View File

@ -0,0 +1 @@
export * from "./Tooltip";

View File

@ -118,7 +118,7 @@ const Uploader = ({ mode }) => {
</div> </div>
{uploads.length > 0 && ( {uploads.length > 0 && (
<div className="flex flex-col space-y-4 py-10"> <div className="flex flex-col space-y-4 pt-6 pb-10">
{uploads.map((upload) => ( {uploads.map((upload) => (
<UploaderItem key={upload.id} onUploadStateChange={onUploadStateChange} upload={upload} /> <UploaderItem key={upload.id} onUploadStateChange={onUploadStateChange} upload={upload} />
))} ))}

View File

@ -109,7 +109,6 @@ export default function UploaderItem({ onUploadStateChange, upload }) {
{upload.status === "uploading" && ( {upload.status === "uploading" && (
<span className="uppercase tabular-nums">{Math.floor(upload.progress * 100)}%</span> <span className="uppercase tabular-nums">{Math.floor(upload.progress * 100)}%</span>
)} )}
{upload.status === "processing" && <span className="uppercase text-palette-300">Wait</span>}
{upload.status === "complete" && ( {upload.status === "complete" && (
<button <button
className="uppercase hover:text-primary transition-colors duration-200" className="uppercase hover:text-primary transition-colors duration-200"

View File

@ -34,8 +34,9 @@ export const AccountRemovalForm = ({ abort, onSuccess }) => {
<Form className="flex flex-col gap-4"> <Form className="flex flex-col gap-4">
<div> <div>
<h4>Delete account</h4> <h4>Delete account</h4>
<p>This will completely delete your account.</p>
<p> <p>
This will completely delete your account. <strong>This process can't be undone.</strong> <strong>This process cannot be undone.</strong>
</p> </p>
</div> </div>

View File

@ -2,6 +2,7 @@ import * as Yup from "yup";
import { forwardRef, useImperativeHandle, useState } from "react"; import { forwardRef, useImperativeHandle, useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { Formik, Form } from "formik"; import { Formik, Form } from "formik";
import cn from "classnames";
import accountsService from "../../services/accountsService"; import accountsService from "../../services/accountsService";
@ -9,7 +10,6 @@ import { Alert } from "../Alert";
import { Button } from "../Button"; import { Button } from "../Button";
import { CopyButton } from "../CopyButton"; import { CopyButton } from "../CopyButton";
import { TextField } from "../Form/TextField"; import { TextField } from "../Form/TextField";
import { CircledProgressIcon, PlusIcon } from "../Icons";
const newAPIKeySchema = Yup.object().shape({ const newAPIKeySchema = Yup.object().shape({
name: Yup.string(), name: Yup.string(),
@ -22,7 +22,7 @@ const State = {
}; };
export const APIKeyType = { export const APIKeyType = {
Public: "public", Sponsor: "sponsor",
General: "general", General: "general",
}; };
@ -37,10 +37,10 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
return ( return (
<div ref={ref} className="flex flex-col gap-4"> <div ref={ref} className="flex flex-col gap-4">
{state === State.Success && ( {state === State.Success && (
<Alert $variant="success" className="text-center"> <Alert $variant="success">
<strong>Success!</strong> <strong>Success!</strong>
<p>Please copy your new API key below. We'll never show it again!</p> <p>Please copy your new API key below. We'll never show it again!</p>
<div className="flex items-center gap-2 mt-4 justify-center"> <div className="flex items-center gap-2 mt-4">
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate"> <code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
{generatedKey} {generatedKey}
</code> </code>
@ -62,8 +62,8 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
.post("user/apikeys", { .post("user/apikeys", {
json: { json: {
name, name,
public: type === APIKeyType.Public ? "true" : "false", public: type === APIKeyType.Sponsor ? "true" : "false",
skylinks: type === APIKeyType.Public ? [] : null, skylinks: type === APIKeyType.Sponsor ? [] : null,
}, },
}) })
.json(); .json();
@ -78,26 +78,20 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
}} }}
> >
{({ errors, touched, isSubmitting }) => ( {({ errors, touched, isSubmitting }) => (
<Form className="grid grid-cols-[1fr_min-content] w-full gap-y-2 gap-x-4 items-start"> <Form className="flex flex-col gap-4">
<div className="flex items-center">
<TextField <TextField
type="text" type="text"
id="name" id="name"
name="name" name="name"
label="New API Key Name" label="New API Key Label"
placeholder="my_applications_statistics" placeholder="my_applications_statistics"
error={errors.name} error={errors.name}
touched={touched.name} touched={touched.name}
/> />
</div> <div className="flex justify-center">
<div className="flex mt-5 justify-center"> <Button type="submit" disabled={isSubmitting} className={cn({ "cursor-wait": isSubmitting })}>
{isSubmitting ? ( {isSubmitting ? "Generating your API key..." : "Generate your API key"}
<CircledProgressIcon size={38} className="text-palette-300 animate-[spin_3s_linear_infinite]" />
) : (
<Button type="submit" className="px-2.5" aria-label="Create general API key">
<PlusIcon size={14} />
</Button> </Button>
)}
</div> </div>
</Form> </Form>
)} )}
@ -110,5 +104,5 @@ AddAPIKeyForm.displayName = "AddAPIKeyForm";
AddAPIKeyForm.propTypes = { AddAPIKeyForm.propTypes = {
onSuccess: PropTypes.func.isRequired, onSuccess: PropTypes.func.isRequired,
type: PropTypes.oneOf([APIKeyType.Public, APIKeyType.General]).isRequired, type: PropTypes.oneOf([APIKeyType.Sponsor, APIKeyType.General]).isRequired,
}; };

View File

@ -19,7 +19,7 @@ const newSkylinkSchema = Yup.object().shape({
}), }),
}); });
export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => ( export const AddSkylinkToSponsorKeyForm = ({ addSkylink }) => (
<Formik <Formik
initialValues={{ initialValues={{
skylink: "", skylink: "",
@ -58,6 +58,6 @@ export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => (
</Formik> </Formik>
); );
AddSkylinkToAPIKeyForm.propTypes = { AddSkylinkToSponsorKeyForm.propTypes = {
addSkylink: PropTypes.func.isRequired, addSkylink: PropTypes.func.isRequired,
}; };

View File

@ -25,7 +25,7 @@ const skylinkValidator = (optional) => (value) => {
} }
}; };
const newPublicAPIKeySchema = Yup.object().shape({ const newSponsorKeySchema = Yup.object().shape({
name: Yup.string(), name: Yup.string(),
skylinks: Yup.array().of(Yup.string().test("skylink", "Provide a valid Skylink", skylinkValidator(false))), skylinks: Yup.array().of(Yup.string().test("skylink", "Provide a valid Skylink", skylinkValidator(false))),
nextSkylink: Yup.string().when("skylinks", { nextSkylink: Yup.string().when("skylinks", {
@ -41,7 +41,7 @@ const State = {
Failure: "FAILURE", Failure: "FAILURE",
}; };
export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => { export const AddSponsorKeyForm = forwardRef(({ onSuccess }, ref) => {
const [state, setState] = useState(State.Pure); const [state, setState] = useState(State.Pure);
const [generatedKey, setGeneratedKey] = useState(null); const [generatedKey, setGeneratedKey] = useState(null);
@ -52,10 +52,10 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
return ( return (
<div ref={ref} className="flex flex-col gap-4"> <div ref={ref} className="flex flex-col gap-4">
{state === State.Success && ( {state === State.Success && (
<Alert $variant="success" className="text-center"> <Alert $variant="success">
<strong>Success!</strong> <strong>Success!</strong>
<p>Please copy your new API key below. We'll never show it again!</p> <p>Please copy your new API key below. We'll never show it again!</p>
<div className="flex items-center gap-2 mt-4 justify-center"> <div className="flex items-center gap-2 mt-4">
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate"> <code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
{generatedKey} {generatedKey}
</code> </code>
@ -72,7 +72,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
skylinks: [], skylinks: [],
nextSkylink: "", nextSkylink: "",
}} }}
validationSchema={newPublicAPIKeySchema} validationSchema={newSponsorKeySchema}
onSubmit={async ({ name, skylinks, nextSkylink }, { resetForm }) => { onSubmit={async ({ name, skylinks, nextSkylink }, { resetForm }) => {
try { try {
const { key } = await accountsService const { key } = await accountsService
@ -101,14 +101,14 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
type="text" type="text"
id="name" id="name"
name="name" name="name"
label="Public API Key Name" label="Sponsor API Key Name"
placeholder="my_applications_statistics" placeholder="my_applications_statistics"
error={errors.name} error={errors.name}
touched={touched.name} touched={touched.name}
/> />
</div> </div>
<div> <div>
<h6 className="text-palette-300 mb-2">Skylinks accessible with the new key</h6> <h6 className="text-palette-300 mb-2">Skylinks sponsored by the new key</h6>
<FieldArray <FieldArray
name="skylinks" name="skylinks"
render={({ push, remove }) => { render={({ push, remove }) => {
@ -182,7 +182,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
className={cn("px-2.5", { "cursor-wait": isSubmitting })} className={cn("px-2.5", { "cursor-wait": isSubmitting })}
disabled={!isValid || isSubmitting} disabled={!isValid || isSubmitting}
> >
{isSubmitting ? "Generating" : "Generate"} your public key {isSubmitting ? "Generating your sponsor key..." : "Generate your sponsor key"}
</Button> </Button>
</div> </div>
</Form> </Form>
@ -192,8 +192,8 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
); );
}); });
AddPublicAPIKeyForm.displayName = "AddAPIKeyForm"; AddSponsorKeyForm.displayName = "AddSponsorKeyForm";
AddPublicAPIKeyForm.propTypes = { AddSponsorKeyForm.propTypes = {
onSuccess: PropTypes.func.isRequired, onSuccess: PropTypes.func.isRequired,
}; };

View File

@ -82,7 +82,7 @@ export const LoginForm = ({ onSuccess }) => {
</div> </div>
<p className="text-sm text-center mt-8"> <p className="text-sm text-center mt-8">
Don't have an account? <HighlightedLink to="/auth/signup">Sign up</HighlightedLink> Don't have an account? <HighlightedLink to="/auth/registration">Sign up</HighlightedLink>
</p> </p>
</Form> </Form>
)} )}

View File

@ -32,14 +32,16 @@ export const SignUpForm = ({ onSuccess, onFailure }) => (
validationSchema={registrationSchema} validationSchema={registrationSchema}
onSubmit={async ({ email, password }, { setErrors }) => { onSubmit={async ({ email, password }, { setErrors }) => {
try { try {
await accountsService.post("user", { const user = await accountsService
.post("user", {
json: { json: {
email, email,
password, password,
}, },
}); })
.json();
onSuccess(); onSuccess(user);
} catch (err) { } catch (err) {
let isFormErrorSet = false; let isFormErrorSet = false;

View File

@ -16,7 +16,7 @@ const fetcher = async (path) => {
}; };
export const PortalSettingsProvider = ({ children }) => { export const PortalSettingsProvider = ({ children }) => {
const { data, error } = useSWRImmutable("/__internal/do/not/use/accounts", fetcher); const { data, error } = useSWRImmutable("__internal/do/not/use/accounts", fetcher);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState(defaultSettings); const [settings, setSettings] = useState(defaultSettings);

View File

@ -1,17 +1,41 @@
import { navigate } from "gatsby";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable";
import { UnauthorizedError } from "../../lib/swrConfig";
import { FullScreenLoadingIndicator } from "../../components/LoadingIndicator";
import { UserContext } from "./UserContext"; import { UserContext } from "./UserContext";
export const UserProvider = ({ children }) => { export const UserProvider = ({ children, allowGuests = false, allowAuthenticated = true }) => {
const { data: user, error, mutate } = useSWRImmutable("user"); const { data: user, error, mutate } = useSWRImmutable("user");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
if (user || error) { const guard = async () => {
if (user) {
if (!allowAuthenticated) {
navigate("/");
} else {
setLoading(false);
}
} else if (error) {
if (error instanceof UnauthorizedError && !allowGuests) {
await navigate(`/auth/login?return_to=${encodeURIComponent(window.location.href)}`);
} else {
setLoading(false);
}
} else if (user === null) {
setLoading(false); setLoading(false);
} }
}, [user, error]); };
return <UserContext.Provider value={{ user, error, loading, mutate }}>{children}</UserContext.Provider>; guard();
}, [user, error, allowGuests, allowAuthenticated]);
return (
<UserContext.Provider value={{ user, error, loading, mutate }}>
{loading && <FullScreenLoadingIndicator />}
{!loading && children}
</UserContext.Provider>
);
}; };

View File

@ -1,9 +1,7 @@
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { SWRConfig } from "swr";
import { UserProvider } from "../contexts/user"; import { UserProvider } from "../contexts/user";
import { guestsOnly, allUsers } from "../lib/swrConfig";
const Layout = styled.div.attrs({ const Layout = styled.div.attrs({
className: "min-h-screen w-screen bg-black flex", className: "min-h-screen w-screen bg-black flex",
@ -22,12 +20,11 @@ const Content = styled.div.attrs({
})``; })``;
const AuthLayout = const AuthLayout =
(swrConfig) => (userProviderProps) =>
({ children }) => { ({ children }) =>
return ( (
<> <>
<SWRConfig value={swrConfig}> <UserProvider {...userProviderProps}>
<UserProvider>
<Layout> <Layout>
<SloganContainer className="pl-20 pr-20 lg:pr-30 xl:pr-40"> <SloganContainer className="pl-20 pr-20 lg:pr-30 xl:pr-40">
<div className=""> <div className="">
@ -36,15 +33,26 @@ const AuthLayout =
</h1> </h1>
</div> </div>
</SloganContainer> </SloganContainer>
<Content>{children}</Content> <Content>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" className="-ml-2" />
</div>
{children}
</div>
</Content>
</Layout> </Layout>
</UserProvider> </UserProvider>
</SWRConfig>
</> </>
); );
};
// Some pages (e.g. email confirmation) need to be accessible to both logged-in and guest users. // Some pages (e.g. email confirmation) need to be accessible to both logged-in and guest users.
export const AllUsersAuthLayout = AuthLayout(allUsers); export const AllUsersAuthLayout = AuthLayout({
allowGuests: true,
allowAuthenticated: true,
});
export default AuthLayout(guestsOnly); export default AuthLayout({
allowGuests: true,
allowAuthenticated: false,
});

View File

@ -1,8 +1,5 @@
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { SWRConfig } from "swr";
import { authenticatedOnly } from "../lib/swrConfig";
import { PageContainer } from "../components/PageContainer"; import { PageContainer } from "../components/PageContainer";
import { NavBar } from "../components/NavBar"; import { NavBar } from "../components/NavBar";
@ -30,22 +27,16 @@ const Layout = ({ children }) => {
); );
}; };
const DashboardLayout = ({ children }) => { const DashboardLayout = ({ children }) => (
return (
<> <>
<SWRConfig value={authenticatedOnly}>
<UserProvider> <UserProvider>
<Layout> <Layout>
<NavBar /> <NavBar />
<PageContainer> <PageContainer className="mt-2 md:mt-14">{children}</PageContainer>
<main className="mt-14">{children}</main>
</PageContainer>
<Footer /> <Footer />
</Layout> </Layout>
</UserProvider> </UserProvider>
</SWRConfig>
</> </>
); );
};
export default DashboardLayout; export default DashboardLayout;

View File

@ -1,43 +1,12 @@
import * as React from "react"; import * as React from "react";
import { Link } from "gatsby"; import { Link } from "gatsby";
import styled from "styled-components"; import styled from "styled-components";
import { SWRConfig } from "swr";
import { authenticatedOnly } from "../lib/swrConfig"; import DashboardLayout from "./DashboardLayout";
import { PageContainer } from "../components/PageContainer";
import { NavBar } from "../components/NavBar";
import { Footer } from "../components/Footer";
import { UserProvider, useUser } from "../contexts/user";
import { ContainerLoadingIndicator } from "../components/LoadingIndicator";
const Wrapper = styled.div.attrs({
className: "min-h-screen overflow-hidden",
})`
background-image: url(/images/dashboard-bg.svg);
background-position: center -280px;
background-repeat: no-repeat;
`;
const Layout = ({ children }) => {
const { user } = useUser();
// Prevent from flashing the dashboard screen to unauthenticated users.
return (
<Wrapper>
{!user && (
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
<ContainerLoadingIndicator className="!text-palette-200/50" />
</div>
)}
{user && <>{children}</>}
</Wrapper>
);
};
const Sidebar = () => ( const Sidebar = () => (
<aside className="w-full lg:w-48 bg-white text-sm font-sans font-light text-palette-600 shrink-0"> <aside className="w-full lg:w-48 text-sm font-sans font-light text-palette-600 shrink-0">
<nav> <nav className="bg-white">
<SidebarLink activeClassName="!border-l-primary" to="/settings"> <SidebarLink activeClassName="!border-l-primary" to="/settings">
Account Account
</SidebarLink> </SidebarLink>
@ -47,15 +16,15 @@ const Sidebar = () => (
<SidebarLink activeClassName="!border-l-primary" to="/settings/export"> <SidebarLink activeClassName="!border-l-primary" to="/settings/export">
Export Export
</SidebarLink> </SidebarLink>
<SidebarLink activeClassName="!border-l-primary" to="/settings/api-keys"> <SidebarLink activeClassName="!border-l-primary" to="/settings/developer-settings">
API Keys Developer settings
</SidebarLink> </SidebarLink>
</nav> </nav>
</aside> </aside>
); );
const SidebarLink = styled(Link).attrs({ const SidebarLink = styled(Link).attrs({
className: `h-12 py-3 px-6 h-full w-full flex className: `h-12 py-3 px-6 w-full flex
border-l-2 border-l-palette-200 border-l-2 border-l-palette-200
border-b border-b-palette-100 last:border-b-transparent`, border-b border-b-palette-100 last:border-b-transparent`,
})``; })``;
@ -67,21 +36,13 @@ const Content = styled.main.attrs({
`; `;
const UserSettingsLayout = ({ children }) => ( const UserSettingsLayout = ({ children }) => (
<SWRConfig value={authenticatedOnly}> <DashboardLayout>
<UserProvider>
<Layout>
<NavBar />
<PageContainer className="mt-2 md:mt-14">
<h6 className="hidden md:block mb-2 text-palette-400">Settings</h6> <h6 className="hidden md:block mb-2 text-palette-400">Settings</h6>
<div className="flex flex-col lg:flex-row"> <div className="flex flex-col lg:flex-row">
<Sidebar /> <Sidebar />
<Content className="lg:w-settings-lg xl:w-settings-xl">{children}</Content> <Content className="lg:w-settings-lg xl:w-settings-xl">{children}</Content>
</div> </div>
</PageContainer> </DashboardLayout>
<Footer />
</Layout>
</UserProvider>
</SWRConfig>
); );
export default UserSettingsLayout; export default UserSettingsLayout;

View File

@ -0,0 +1 @@
export const DATE_FORMAT = "MMM D, YYYY HH:MM";

View File

@ -1,39 +1,22 @@
import { navigate } from "gatsby";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
// TODO: portal-aware URL export class UnauthorizedError extends Error {}
const baseUrl = process.env.NODE_ENV !== "production" ? "/api" : "https://account.skynetpro.net/api";
const redirectUnauthenticated = (key) => const config = {
fetch(`${baseUrl}/${key}`).then((response) => { fetcher: (key) =>
if (response.status === StatusCodes.UNAUTHORIZED) { fetch(`/api/${key}`).then(async (response) => {
navigate(`/auth/login?return_to=${encodeURIComponent(window.location.href)}`);
return null;
}
return response.json();
});
const redirectAuthenticated = (key) =>
fetch(`${baseUrl}/${key}`).then(async (response) => {
if (response.ok) { if (response.ok) {
await navigate("/");
return response.json(); return response.json();
} }
// If there was an error, let's throw so useSWR's "error" property is populated instead "data".
const data = await response.json(); const data = await response.json();
throw new Error(data?.message || `Error occured when trying to fetch: ${key}`);
});
export const allUsers = { if (response.status === StatusCodes.UNAUTHORIZED) {
fetcher: (key) => fetch(`${baseUrl}/${key}`).then((response) => response.json()), throw new UnauthorizedError(data?.message || "Unauthorized");
}
throw new Error(data?.message || `Error occurred when trying to fetch: ${key}`);
}),
}; };
export const authenticatedOnly = { export default config;
fetcher: redirectUnauthenticated,
};
export const guestsOnly = {
fetcher: redirectAuthenticated,
};

View File

@ -22,16 +22,11 @@ const LoginPage = ({ location }) => {
<Metadata> <Metadata>
<title>Sign In</title> <title>Sign In</title>
</Metadata> </Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
<LoginForm <LoginForm
onSuccess={async () => { onSuccess={async () => {
await refreshUserState(); await refreshUserState();
}} }}
/> />
</div>
</> </>
); );
}; };

View File

@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useCallback, useState } from "react";
import { navigate } from "gatsby";
import bytes from "pretty-bytes"; import bytes from "pretty-bytes";
import AuthLayout from "../../layouts/AuthLayout"; import AuthLayout from "../../layouts/AuthLayout";
@ -10,6 +9,7 @@ import { SignUpForm } from "../../components/forms/SignUpForm";
import { usePortalSettings } from "../../contexts/portal-settings"; import { usePortalSettings } from "../../contexts/portal-settings";
import { PlansProvider, usePlans } from "../../contexts/plans"; import { PlansProvider, usePlans } from "../../contexts/plans";
import { Metadata } from "../../components/Metadata"; import { Metadata } from "../../components/Metadata";
import { useUser } from "../../contexts/user";
const FreePortalHeader = () => { const FreePortalHeader = () => {
const { plans } = usePlans(); const { plans } = usePlans();
@ -47,25 +47,21 @@ const State = {
const SignUpPage = () => { const SignUpPage = () => {
const [state, setState] = useState(State.Pure); const [state, setState] = useState(State.Pure);
const { settings } = usePortalSettings(); const { settings } = usePortalSettings();
const { mutate: refreshUserState } = useUser();
useEffect(() => { const onUserCreated = useCallback(
if (state === State.Success) { (newUser) => {
const timer = setTimeout(() => navigate(settings.isSubscriptionRequired ? "/upgrade" : "/"), 3000); refreshUserState(newUser);
},
return () => clearTimeout(timer); [refreshUserState]
} );
}, [state, settings.isSubscriptionRequired]);
return ( return (
<PlansProvider> <PlansProvider>
<Metadata> <Metadata>
<title>Sign Up</title> <title>Sign Up</title>
</Metadata> </Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen"> <div className="flex flex-col">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{!settings.areAccountsEnabled && <Alert $variant="info">Accounts are not enabled on this portal.</Alert>} {!settings.areAccountsEnabled && <Alert $variant="info">Accounts are not enabled on this portal.</Alert>}
{settings.areAccountsEnabled && ( {settings.areAccountsEnabled && (
@ -73,15 +69,13 @@ const SignUpPage = () => {
{settings.isSubscriptionRequired ? <PaidPortalHeader /> : <FreePortalHeader />} {settings.isSubscriptionRequired ? <PaidPortalHeader /> : <FreePortalHeader />}
{state !== State.Success && ( {state !== State.Success && (
<SignUpForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} /> <>
)} <SignUpForm onSuccess={onUserCreated} onFailure={() => setState(State.Failure)} />
{state === State.Success && ( <p className="text-sm text-center mt-8">
<div className="text-center"> Already have an account? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
<p className="text-primary font-semibold">Please check your inbox and confirm your email address.</p> </p>
<p>You will be redirected to your dashboard shortly.</p> </>
<HighlightedLink to="/">Click here to go there now.</HighlightedLink>
</div>
)} )}
{state === State.Failure && ( {state === State.Failure && (
@ -89,10 +83,6 @@ const SignUpPage = () => {
)} )}
</> </>
)} )}
<p className="text-sm text-center mt-8">
Already have an account? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
</div> </div>
</PlansProvider> </PlansProvider>
); );

View File

@ -20,10 +20,6 @@ const ResetPasswordPage = () => {
<Metadata> <Metadata>
<title>Reset Password</title> <title>Reset Password</title>
</Metadata> </Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && ( {state !== State.Success && (
<RecoveryForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} /> <RecoveryForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} />
)} )}
@ -41,10 +37,9 @@ const ResetPasswordPage = () => {
Suddenly remembered your password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink> Suddenly remembered your password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p> </p>
<p> <p>
Don't actually have an account? <HighlightedLink to="/auth/signup">Create one!</HighlightedLink> Don't actually have an account? <HighlightedLink to="/auth/registration">Create one!</HighlightedLink>
</p> </p>
</div> </div>
</div>
</> </>
); );
}; };

View File

@ -1,32 +1,19 @@
import * as React from "react"; import * as React from "react";
import { useSearchParam } from "react-use";
import DashboardLayout from "../layouts/DashboardLayout"; import DashboardLayout from "../layouts/DashboardLayout";
import { Panel } from "../components/Panel"; import { Panel } from "../components/Panel";
import { Tab, TabPanel, Tabs } from "../components/Tabs";
import { Metadata } from "../components/Metadata"; import { Metadata } from "../components/Metadata";
import FileList from "../components/FileList/FileList"; import FileList from "../components/FileList/FileList";
const FilesPage = () => { const FilesPage = () => {
const defaultTab = useSearchParam("tab");
return ( return (
<> <>
<Metadata> <Metadata>
<title>My Files</title> <title>Files</title>
</Metadata> </Metadata>
<Panel title="Files"> <Panel title="Files">
<Tabs defaultTab={defaultTab || "uploads"}>
<Tab id="uploads" title="Uploads" />
<Tab id="downloads" title="Downloads" />
<TabPanel tabId="uploads" className="pt-4">
<FileList type="uploads" /> <FileList type="uploads" />
</TabPanel>
<TabPanel tabId="downloads" className="pt-4">
<FileList type="downloads" />
</TabPanel>
</Tabs>
</Panel> </Panel>
</> </>
); );

View File

@ -6,13 +6,14 @@ import UserSettingsLayout from "../../layouts/UserSettingsLayout";
import { AddAPIKeyForm, APIKeyType } from "../../components/forms/AddAPIKeyForm"; import { AddAPIKeyForm, APIKeyType } from "../../components/forms/AddAPIKeyForm";
import { APIKeyList } from "../../components/APIKeyList/APIKeyList"; import { APIKeyList } from "../../components/APIKeyList/APIKeyList";
import { Alert } from "../../components/Alert"; import { Alert } from "../../components/Alert";
import { AddPublicAPIKeyForm } from "../../components/forms/AddPublicAPIKeyForm"; import { AddSponsorKeyForm } from "../../components/forms/AddSponsorKeyForm";
import { Metadata } from "../../components/Metadata"; import { Metadata } from "../../components/Metadata";
import HighlightedLink from "../../components/HighlightedLink";
const APIKeysPage = () => { const DeveloperSettingsPage = () => {
const { data: apiKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys"); const { data: allKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys");
const generalKeys = apiKeys.filter(({ public: isPublic }) => isPublic === "false"); const apiKeys = allKeys.filter(({ public: isPublic }) => isPublic === "false");
const publicKeys = apiKeys.filter(({ public: isPublic }) => isPublic === "true"); const sponsorKeys = allKeys.filter(({ public: isPublic }) => isPublic === "true");
const publicFormRef = useRef(); const publicFormRef = useRef();
const generalFormRef = useRef(); const generalFormRef = useRef();
@ -31,53 +32,57 @@ const APIKeysPage = () => {
return ( return (
<> <>
<Metadata> <Metadata>
<title>API Keys</title> <title>Developer settings</title>
</Metadata> </Metadata>
<div className="flex flex-col xl:flex-row"> <div className="flex flex-col xl:flex-row">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]"> <div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px] leading-relaxed">
<div> <div>
<h4>API Keys</h4> <h4>Developer settings</h4>
<p className="leading-relaxed">There are two types of API keys that you can generate for your account.</p> <p>API keys allow developers and applications to extend the functionality of your portal account.</p>
<p>Make sure to use the appropriate type.</p> <p>Skynet uses two types of API keys, explained below.</p>
</div> </div>
<hr /> <hr />
<section className="flex flex-col gap-2"> <section className="flex flex-col gap-2">
<h5>Public keys</h5> <h5>Sponsor keys</h5>
<p className="text-palette-500"> <div className="text-palette-500"></div>
Public keys provide read access to a selected list of skylinks. You can share them publicly. <p>
Sponsor keys allow users without an account on this portal to download skylinks covered by the API key.
</p> </p>
<p>
Learn more about sponsoring content with Sponsor API Keys{" "}
<HighlightedLink as="a" href="#">
here
</HighlightedLink>
.
</p>{" "}
{/* TODO: missing documentation link */}
<div className="mt-4"> <div className="mt-4">
<AddPublicAPIKeyForm ref={publicFormRef} onSuccess={refreshState} /> <AddSponsorKeyForm ref={publicFormRef} onSuccess={refreshState} />
</div> </div>
{error ? ( {error ? (
<Alert $variant="error" className="mt-4"> <Alert $variant="error" className="mt-4">
An error occurred while loading your API keys. Please try again later. An error occurred while loading your sponsor keys. Please try again later.
</Alert> </Alert>
) : ( ) : (
<div className="mt-4"> <div className="mt-4">
{publicKeys?.length > 0 ? ( {sponsorKeys?.length > 0 ? (
<APIKeyList title="Your public keys" keys={publicKeys} reloadKeys={() => refreshState(true)} /> <APIKeyList title="Your public keys" keys={sponsorKeys} reloadKeys={() => refreshState(true)} />
) : ( ) : (
<Alert $variant="info">No public API keys found.</Alert> <Alert $variant="info">No sponsor keys found.</Alert>
)} )}
</div> </div>
)} )}
</section> </section>
<hr /> <hr />
<section className="flex flex-col gap-2"> <section className="flex flex-col gap-2">
<h5>General keys</h5> <h5>API keys</h5>
<p className="text-palette-500"> <p className="text-palette-500">
These keys provide full access to <b>Accounts</b> service and are equivalent to using a JWT token. These keys allow uploading and downloading skyfiles, as well as reading and writing to the registry.
</p> </p>
<p className="underline">
This type of API keys needs to be kept secret and should never be shared with anyone.
</p>
<div className="mt-4"> <div className="mt-4">
<AddAPIKeyForm ref={generalFormRef} onSuccess={refreshState} type={APIKeyType.General} /> <AddAPIKeyForm ref={generalFormRef} onSuccess={refreshState} type={APIKeyType.General} />
</div> </div>
@ -88,10 +93,10 @@ const APIKeysPage = () => {
</Alert> </Alert>
) : ( ) : (
<div className="mt-4"> <div className="mt-4">
{generalKeys?.length > 0 ? ( {apiKeys?.length > 0 ? (
<APIKeyList title="Your general keys" keys={generalKeys} reloadKeys={() => refreshState(true)} /> <APIKeyList title="Your API keys" keys={apiKeys} reloadKeys={() => refreshState(true)} />
) : ( ) : (
<Alert $variant="info">No general API keys found.</Alert> <Alert $variant="info">No API keys found.</Alert>
)} )}
</div> </div>
)} )}
@ -105,6 +110,6 @@ const APIKeysPage = () => {
); );
}; };
APIKeysPage.Layout = UserSettingsLayout; DeveloperSettingsPage.Layout = UserSettingsLayout;
export default APIKeysPage; export default DeveloperSettingsPage;

View File

@ -38,8 +38,8 @@ const ExportPage = () => {
<section> <section>
<h4>Export</h4> <h4>Export</h4>
<p> <p>
Et quidem exercitus quid ex eo delectu rerum, quem modo ista sis aequitate. Probabo, inquit, modo dixi, Select the items you want to export. You can use this data to migrate your account to another Skynet
constituto. portal.
</p> </p>
</section> </section>
<hr /> <hr />

View File

@ -8,6 +8,10 @@ import { Modal } from "../../components/Modal/Modal";
import { AccountRemovalForm } from "../../components/forms/AccountRemovalForm"; import { AccountRemovalForm } from "../../components/forms/AccountRemovalForm";
import { Alert } from "../../components/Alert"; import { Alert } from "../../components/Alert";
import { Metadata } from "../../components/Metadata"; import { Metadata } from "../../components/Metadata";
import HighlightedLink from "../../components/HighlightedLink";
import { AvatarUploader } from "../../components/AvatarUploader/AvatarUploader";
import { useMedia } from "react-use";
import theme from "../../lib/theme";
const State = { const State = {
Pure: "PURE", Pure: "PURE",
@ -19,10 +23,16 @@ const AccountPage = () => {
const { user, mutate: reloadUser } = useUser(); const { user, mutate: reloadUser } = useUser();
const [state, setState] = useState(State.Pure); const [state, setState] = useState(State.Pure);
const [removalInitiated, setRemovalInitiated] = useState(false); const [removalInitiated, setRemovalInitiated] = useState(false);
const isLargeScreen = useMedia(`(min-width: ${theme.screens.xl})`);
const prompt = () => setRemovalInitiated(true); const prompt = () => setRemovalInitiated(true);
const abort = () => setRemovalInitiated(false); const abort = () => setRemovalInitiated(false);
const onAccountRemoved = useCallback(async () => {
await reloadUser(null);
await navigate("/auth/login");
}, [reloadUser]);
const onSettingsUpdated = useCallback( const onSettingsUpdated = useCallback(
async (updatedState) => { async (updatedState) => {
try { try {
@ -45,14 +55,7 @@ const AccountPage = () => {
</Metadata> </Metadata>
<div className="flex flex-col xl:flex-row"> <div className="flex flex-col xl:flex-row">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]"> <div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]">
<section>
<h4>Account</h4> <h4>Account</h4>
<p>
Tum dicere exorsus est laborum et quasi involuta aperiri, altera prompta et expedita. Primum igitur,
inquit, modo ista sis aequitate.
</p>
</section>
<hr />
<section className="flex flex-col gap-8"> <section className="flex flex-col gap-8">
{state === State.Failure && ( {state === State.Failure && (
<Alert $variant="error">There was an error processing your request. Please try again later.</Alert> <Alert $variant="error">There was an error processing your request. Please try again later.</Alert>
@ -63,7 +66,23 @@ const AccountPage = () => {
<hr /> <hr />
<section> <section>
<h6 className="text-palette-400">Delete account</h6> <h6 className="text-palette-400">Delete account</h6>
<p>This will completely delete your account. This process can't be undone.</p> <div className="my-4">
<p>
This action will delete your account and <strong>cannot be undone</strong>.
</p>
<p>
Your uploaded files will remain accessible while any portal continues to{" "}
<HighlightedLink
as="a"
href="https://support.skynetlabs.com/key-concepts/faqs#what-is-pinning"
target="_blank"
rel="noreferrer"
>
pin
</HighlightedLink>{" "}
them to Skynet.
</p>
</div>
<button <button
type="button" type="button"
onClick={prompt} onClick={prompt}
@ -73,9 +92,12 @@ const AccountPage = () => {
</button> </button>
</section> </section>
</div> </div>
<div className="flex w-full justify-start xl:justify-end">
{isLargeScreen && <AvatarUploader className="flex flex-col gap-4" />}
</div>
{removalInitiated && ( {removalInitiated && (
<Modal onClose={abort} className="text-center"> <Modal onClose={abort} className="text-center">
<AccountRemovalForm abort={abort} onSuccess={() => navigate("/auth/login")} /> <AccountRemovalForm abort={abort} onSuccess={onAccountRemoved} />
</Modal> </Modal>
)} )}
</div> </div>

View File

@ -18,18 +18,13 @@ const NotificationsPage = () => {
<section> <section>
{/* TODO: saves on change */} {/* TODO: saves on change */}
<Switch onChange={console.info.bind(console)} labelClassName="!items-start flex-col md:flex-row"> <Switch onChange={console.info.bind(console)} labelClassName="!items-start flex-col md:flex-row">
I agreee to get the latest news, updates and special offers delivered to my email inbox. I agree to receive emails of the latest news, updates and offers.
</Switch> </Switch>
</section> </section>
<hr /> <hr />
<section> <section>
<h6 className="text-palette-300">Statistics</h6> <h6 className="text-palette-300">Statistics</h6>
{/* TODO: proper content :) */} <p>Check below to be notified by email when your usage approaches your plan's limits.</p>
<p>
Si sine causa, nollem me tamen laudandis maioribus meis corrupisti nec in malis. Si sine causa, mox
videro.
</p>
<ul className="mt-7 flex flex-col gap-2"> <ul className="mt-7 flex flex-col gap-2">
<li> <li>
{/* TODO: saves on change */} {/* TODO: saves on change */}
@ -37,7 +32,7 @@ const NotificationsPage = () => {
</li> </li>
<li> <li>
{/* TODO: saves on change */} {/* TODO: saves on change */}
<Switch onChange={console.info.bind(console)}>File limit</Switch> <Switch onChange={console.info.bind(console)}>Files limit</Switch>
</li> </li>
</ul> </ul>
</section> </section>

View File

@ -57,10 +57,6 @@ const EmailConfirmationPage = ({ location }) => {
<Metadata> <Metadata>
<title>Confirm E-mail Address</title> <title>Confirm E-mail Address</title>
</Metadata> </Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
<div className="text-center"> <div className="text-center">
{state === State.Pure && <p>Please wait while we verify your account...</p>} {state === State.Pure && <p>Please wait while we verify your account...</p>}
@ -74,7 +70,6 @@ const EmailConfirmationPage = ({ location }) => {
{state === State.Failure && <p className="text-error">Something went wrong, please try again later.</p>} {state === State.Failure && <p className="text-error">Something went wrong, please try again later.</p>}
</div> </div>
</div>
</> </>
); );
}; };

View File

@ -24,10 +24,6 @@ const RecoverPage = ({ location }) => {
<Metadata> <Metadata>
<title>Recover Your Account</title> <title>Recover Your Account</title>
</Metadata> </Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && ( {state !== State.Success && (
<ResetPasswordForm <ResetPasswordForm
token={token} token={token}
@ -52,7 +48,6 @@ const RecoverPage = ({ location }) => {
<p className="text-sm text-center mt-8"> <p className="text-sm text-center mt-8">
Suddenly remembered your old password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink> Suddenly remembered your old password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p> </p>
</div>
</> </>
); );
}; };

View File

@ -1,3 +1,3 @@
import { SkynetClient } from "skynet-js"; import { SkynetClient } from "skynet-js";
export default new SkynetClient("https://skynetpro.net"); // TODO: proper API url export default new SkynetClient(`https://${process.env.GATSBY_PORTAL_DOMAIN}`);

View File

@ -6735,11 +6735,31 @@ dot-prop@^5.2.0:
dependencies: dependencies:
is-obj "^2.0.0" is-obj "^2.0.0"
dotenv-cli@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-5.1.0.tgz#0d2942b089082da0157f9b26bd6c5c4dd51ef48e"
integrity sha512-NoEZAlKo9WVrG0b3i9mBxdD6INdDuGqdgR74t68t8084QcI077/1MnPerRW1odl+9uULhcdnQp2U0pYVppKHOA==
dependencies:
cross-spawn "^7.0.3"
dotenv "^16.0.0"
dotenv-expand "^8.0.1"
minimist "^1.2.5"
dotenv-expand@^5.1.0: dotenv-expand@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
dotenv-expand@^8.0.1:
version "8.0.3"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e"
integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==
dotenv@^16.0.0:
version "16.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411"
integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==
dotenv@^8.0.0, dotenv@^8.6.0: dotenv@^8.0.0, dotenv@^8.6.0:
version "8.6.0" version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
@ -11242,9 +11262,9 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment@^2.29.1: moment@^2.29.1:
version "2.29.1" version "2.29.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
move-concurrently@^1.0.1: move-concurrently@^1.0.1:
version "1.0.1" version "1.0.1"

View File

@ -10,8 +10,8 @@
"dependencies": { "dependencies": {
"@fontsource/sora": "4.5.5", "@fontsource/sora": "4.5.5",
"@fontsource/source-sans-pro": "4.5.6", "@fontsource/source-sans-pro": "4.5.6",
"@stripe/react-stripe-js": "1.7.0", "@stripe/react-stripe-js": "1.7.1",
"@stripe/stripe-js": "1.26.0", "@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.11.0", "dayjs": "1.11.0",
@ -27,15 +27,15 @@
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-toastify": "8.2.0", "react-toastify": "8.2.0",
"skynet-js": "3.0.2", "skynet-js": "3.0.2",
"stripe": "8.215.0", "stripe": "8.216.0",
"swr": "1.2.2", "swr": "1.3.0",
"yup": "0.32.11" "yup": "0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "0.5.0", "@tailwindcss/forms": "0.5.0",
"@tailwindcss/typography": "0.5.2", "@tailwindcss/typography": "0.5.2",
"autoprefixer": "10.4.4", "autoprefixer": "10.4.4",
"eslint": "8.12.0", "eslint": "8.13.0",
"eslint-config-next": "12.1.4", "eslint-config-next": "12.1.4",
"postcss": "8.4.12", "postcss": "8.4.12",
"prettier": "2.6.2", "prettier": "2.6.2",

View File

@ -3,7 +3,14 @@ import { useFormik, getIn, setIn } from "formik";
import classnames from "classnames"; import classnames from "classnames";
import SelfServiceMessages from "./SelfServiceMessages"; import SelfServiceMessages from "./SelfServiceMessages";
export default function SelfServiceForm({ fieldsConfig, onSubmit, title, validationSchema = null, button = "Submit" }) { export default function SelfServiceForm({
fieldsConfig,
onSubmit,
title,
onError,
validationSchema = null,
button = "Submit",
}) {
const [messages, setMessages] = React.useState([]); const [messages, setMessages] = React.useState([]);
const fields = fieldsConfig.sort((a, b) => (a.position < b.position ? -1 : 1)); const fields = fieldsConfig.sort((a, b) => (a.position < b.position ? -1 : 1));
const formik = useFormik({ const formik = useFormik({
@ -21,6 +28,9 @@ export default function SelfServiceForm({ fieldsConfig, onSubmit, title, validat
const data = await error.response.json(); const data = await error.response.json();
if (data.message) { if (data.message) {
if (typeof onError === "function") {
onError(data.message);
}
setMessages((messages) => [...messages, { type: "error", text: data.message }]); setMessages((messages) => [...messages, { type: "error", text: data.message }]);
} }
} else { } else {

View File

@ -23,6 +23,8 @@ export default function Recovery() {
useAnonRoute(); // ensure user is not logged in useAnonRoute(); // ensure user is not logged in
const [success, setSuccess] = React.useState(false); const [success, setSuccess] = React.useState(false);
const [skynetFreeInviteVisible, setSkynetFreeInviteVisible] = React.useState(false);
const isSiaskyNet = typeof window !== "undefined" && window.location.hostname === "account.siasky.net";
const onSubmit = async (values) => { const onSubmit = async (values) => {
await accountsApi.post("user/recover/request", { await accountsApi.post("user/recover/request", {
@ -64,6 +66,16 @@ export default function Recovery() {
</Link>{" "} </Link>{" "}
for a new account for a new account
</p> </p>
{skynetFreeInviteVisible && isSiaskyNet && (
<div className="font-content rounded border border-blue-200 mt-6 p-4 bg-blue-100">
<p>
All Siasky.net accounts have been moved to{" "}
<a className="text-primary" href="https://skynetfree.net">
SkynetFree.net
</a>
</p>
</div>
)}
</div> </div>
{!success && ( {!success && (
@ -72,6 +84,9 @@ export default function Recovery() {
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={onSubmit} onSubmit={onSubmit}
button="Send recovery link" button="Send recovery link"
onError={(errorMessage) =>
setSkynetFreeInviteVisible(errorMessage === "registrations are currently disabled")
}
/> />
)} )}

View File

@ -175,17 +175,17 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.8.tgz#be3e914e84eacf16dbebd311c0d0b44aa1174c64" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.8.tgz#be3e914e84eacf16dbebd311c0d0b44aa1174c64"
integrity sha512-ZK5v4bJwgXldAUA8r3q9YKfCwOqoHTK/ZqRjSeRXQrBXWouoPnS4MQtgC4AXGiiBuUu5wxrRgTlv0ktmM4P1Aw== integrity sha512-ZK5v4bJwgXldAUA8r3q9YKfCwOqoHTK/ZqRjSeRXQrBXWouoPnS4MQtgC4AXGiiBuUu5wxrRgTlv0ktmM4P1Aw==
"@stripe/react-stripe-js@1.7.0": "@stripe/react-stripe-js@1.7.1":
version "1.7.0" version "1.7.1"
resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.7.0.tgz#83c993a09a903703205d556617f9729784a896c3" resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.7.1.tgz#6e1db8f4a0eaf2193b153173d4aa7c38b681310d"
integrity sha512-L20v8Jq0TDZFL2+y+uXD751t6q9SalSFkSYZpmZ2VWrGZGK7HAGfRQ804dzYSSr5fGenW6iz6y7U0YKfC/TK3g== integrity sha512-GiUPoMo0xVvmpRD6JR9JAhAZ0W3ZpnYZNi0KE+91+tzrSFVpChKZbeSsJ5InlZhHFk9NckJCt1wOYBTqNsvt3A==
dependencies: dependencies:
prop-types "^15.7.2" prop-types "^15.7.2"
"@stripe/stripe-js@1.26.0": "@stripe/stripe-js@1.27.0":
version "1.26.0" version "1.27.0"
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.26.0.tgz#45670924753c01e18d0544ea1f1067b474aaa96f" resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.27.0.tgz#ab0c82fa89fd40260de4414f69868b769e810550"
integrity sha512-4R1vC75yKaCVFARW3bhelf9+dKt4NP4iZY/sIjGK7AAMBVvZ47eG74NvsAIUdUnhOXSWFMjdFWqv+etk5BDW4g== integrity sha512-SEiybUBu+tlsFKuzdFFydxxjkbrdzHo0tz/naYC5Dt9or/Ux2gcKJBPYQ4RmqQCNHFxgyNj6UYsclywwhe2inQ==
"@tailwindcss/forms@0.5.0": "@tailwindcss/forms@0.5.0":
version "0.5.0" version "0.5.0"
@ -923,10 +923,10 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@8.12.0: eslint@8.13.0:
version "8.12.0" version "8.13.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.12.0.tgz#c7a5bd1cfa09079aae64c9076c07eada66a46e8e" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.13.0.tgz#6fcea43b6811e655410f5626cfcf328016badcd7"
integrity sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q== integrity sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==
dependencies: dependencies:
"@eslint/eslintrc" "^1.2.1" "@eslint/eslintrc" "^1.2.1"
"@humanwhocodes/config-array" "^0.9.2" "@humanwhocodes/config-array" "^0.9.2"
@ -1644,14 +1644,7 @@ mini-svg-data-uri@^1.2.3:
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.3.3.tgz#91d2c09f45e056e5e1043340b8b37ba7b50f4fac" resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.3.3.tgz#91d2c09f45e056e5e1043340b8b37ba7b50f4fac"
integrity sha512-+fA2oRcR1dJI/7ITmeQJDrYWks0wodlOz0pAEhKYJ2IVc1z0AnwJUsKY2fzFmPAM3Jo9J0rBx8JAA9QQSJ5PuA== integrity sha512-+fA2oRcR1dJI/7ITmeQJDrYWks0wodlOz0pAEhKYJ2IVc1z0AnwJUsKY2fzFmPAM3Jo9J0rBx8JAA9QQSJ5PuA==
minimatch@^3.0.4: minimatch@^3.0.4, minimatch@^3.1.2:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@ -1965,16 +1958,7 @@ pretty-bytes@6.0.0:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.0.0.tgz#928be2ad1f51a2e336add8ba764739f9776a8140" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.0.0.tgz#928be2ad1f51a2e336add8ba764739f9776a8140"
integrity sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg== integrity sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==
prop-types@^15.7.2: prop-types@^15.7.2, prop-types@^15.8.1:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"
prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -2036,7 +2020,7 @@ react-fast-compare@^2.0.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -2264,10 +2248,10 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
stripe@8.215.0: stripe@8.216.0:
version "8.215.0" version "8.216.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.215.0.tgz#bb464e256fb83da9ea2f514711fd0f6f7ae7dc9a" resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.216.0.tgz#23c047498526d13a238c3aca7b4dc8cbbd522e46"
integrity sha512-M+7iTZ9bzTkU1Ms+Zsuh0mTQfEzOjMoqyEaVBpuUmdbWTvshavzpAihsOkfabEu+sNY0vdbQxxHZ4kI3W8pKHQ== integrity sha512-LY8cNGizEnklIa4T82l6mZW0HS4cfzo1hNuhT+ZR9PBkmYcSUbg3ilUBVF0FCd4RP+NA44VEVfoSTTZ1Gg5+rQ==
dependencies: dependencies:
"@types/node" ">=8.1.0" "@types/node" ">=8.1.0"
qs "^6.10.3" qs "^6.10.3"
@ -2296,10 +2280,10 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swr@1.2.2: swr@1.3.0:
version "1.2.2" version "1.3.0"
resolved "https://registry.yarnpkg.com/swr/-/swr-1.2.2.tgz#6cae09928d30593a7980d80f85823e57468fac5d" resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
integrity sha512-ky0BskS/V47GpW8d6RU7CPsr6J8cr7mQD6+do5eky3bM0IyJaoi3vO8UhvrzJaObuTlGhPl2szodeB2dUd76Xw== integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
tailwindcss@3.0.23: tailwindcss@3.0.23:
version "3.0.23" version "3.0.23"

View File

@ -8,12 +8,14 @@ const port = Number(process.env.DNSLINK_API_PORT) || 3100;
const server = express(); const server = express();
const dnslinkNamespace = "skynet-ns"; const dnslinkNamespace = "skynet-ns";
const sponsorNamespace = "skynet-sponsor-key";
const dnslinkRegExp = new RegExp(`^dnslink=/${dnslinkNamespace}/.+$`); const dnslinkRegExp = new RegExp(`^dnslink=/${dnslinkNamespace}/.+$`);
const sponsorRegExp = new RegExp(`^${sponsorNamespace}=[a-zA-Z0-9]+$`);
const dnslinkSkylinkRegExp = new RegExp(`^dnslink=/${dnslinkNamespace}/([a-zA-Z0-9_-]{46}|[a-z0-9]{55})`); const dnslinkSkylinkRegExp = new RegExp(`^dnslink=/${dnslinkNamespace}/([a-zA-Z0-9_-]{46}|[a-z0-9]{55})`);
const hint = `valid example: dnslink=/${dnslinkNamespace}/3ACpC9Umme41zlWUgMQh1fw0sNwgWwyfDDhRQ9Sppz9hjQ`; const hint = `valid example: dnslink=/${dnslinkNamespace}/3ACpC9Umme41zlWUgMQh1fw0sNwgWwyfDDhRQ9Sppz9hjQ`;
server.get("/dnslink/:name", async (req, res) => { server.get("/dnslink/:name", async (req, res) => {
const success = (skylink) => res.send(skylink); const success = (response) => res.json(response);
const failure = (message) => res.status(400).send(message); const failure = (message) => res.status(400).send(message);
if (!isValidDomain(req.params.name)) { if (!isValidDomain(req.params.name)) {
@ -22,7 +24,7 @@ server.get("/dnslink/:name", async (req, res) => {
const lookup = `_dnslink.${req.params.name}`; const lookup = `_dnslink.${req.params.name}`;
dns.resolveTxt(lookup, (error, records) => { dns.resolveTxt(lookup, (error, addresses) => {
if (error) { if (error) {
if (error.code === "ENOTFOUND") { if (error.code === "ENOTFOUND") {
return failure(`ENOTFOUND: ${lookup} TXT record doesn't exist`); return failure(`ENOTFOUND: ${lookup} TXT record doesn't exist`);
@ -35,11 +37,12 @@ server.get("/dnslink/:name", async (req, res) => {
return failure(`Failed to fetch ${lookup} TXT record: ${error.message}`); return failure(`Failed to fetch ${lookup} TXT record: ${error.message}`);
} }
if (records.length === 0) { if (addresses.length === 0) {
return failure(`No TXT record found for ${lookup}`); return failure(`No TXT record found for ${lookup}`);
} }
const dnslinks = records.flat().filter((record) => dnslinkRegExp.test(record)); const records = addresses.flat();
const dnslinks = records.filter((record) => dnslinkRegExp.test(record));
if (dnslinks.length === 0) { if (dnslinks.length === 0) {
return failure(`TXT records for ${lookup} found but none of them contained valid skynet dnslink - ${hint}`); return failure(`TXT records for ${lookup} found but none of them contained valid skynet dnslink - ${hint}`);
@ -58,9 +61,25 @@ server.get("/dnslink/:name", async (req, res) => {
const skylink = matchSkylink[1]; const skylink = matchSkylink[1];
// check if _dnslink records contain skynet-sponsor-key entries
const sponsors = records.filter((record) => sponsorRegExp.test(record));
if (sponsors.length > 1) {
return failure(`Multiple TXT records with valid sponsor key found for ${lookup}, only one allowed`);
}
if (sponsors.length === 1) {
// extract just the key part from the record
const sponsor = sponsors[0].substring(sponsors[0].indexOf("=") + 1);
console.log(`${req.params.name} => ${skylink} | sponsor: ${sponsor}`);
return success({ skylink, sponsor });
}
console.log(`${req.params.name} => ${skylink}`); console.log(`${req.params.name} => ${skylink}`);
return success(skylink); return success({ skylink });
}); });
}); });

View File

@ -8,14 +8,14 @@
"express": "^4.17.3", "express": "^4.17.3",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"graceful-fs": "^4.2.9", "graceful-fs": "^4.2.10",
"hasha": "^5.2.2", "hasha": "^5.2.2",
"http-status-codes": "^2.2.0", "http-status-codes": "^2.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lowdb": "^1.0.0", "lowdb": "^1.0.0",
"skynet-js": "^4.0.19-beta", "skynet-js": "^4.0.19-beta",
"write-file-atomic": "^4.0.1", "write-file-atomic": "^4.0.1",
"yargs": "^17.4.0" "yargs": "^17.4.1"
}, },
"devDependencies": { "devDependencies": {
"jest": "^27.5.1", "jest": "^27.5.1",

View File

@ -1516,10 +1516,10 @@ got@^11.8.2:
p-cancelable "^2.0.0" p-cancelable "^2.0.0"
responselike "^2.0.0" responselike "^2.0.0"
graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.2.9: graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.2.10, graceful-fs@^4.2.9:
version "4.2.9" version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
has-flag@^3.0.0: has-flag@^3.0.0:
version "3.0.0" version "3.0.0"
@ -3361,10 +3361,10 @@ yargs@^16.2.0:
y18n "^5.0.5" y18n "^5.0.5"
yargs-parser "^20.2.2" yargs-parser "^20.2.2"
yargs@^17.4.0: yargs@^17.4.1:
version "17.4.0" version "17.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.0.tgz#9fc9efc96bd3aa2c1240446af28499f0e7593d00" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284"
integrity sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA== integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==
dependencies: dependencies:
cliui "^7.0.2" cliui "^7.0.2"
escalade "^3.1.1" escalade "^3.1.1"

View File

@ -12,16 +12,16 @@
"classnames": "2.3.1", "classnames": "2.3.1",
"copy-text-to-clipboard": "3.0.1", "copy-text-to-clipboard": "3.0.1",
"crypto-browserify": "3.12.0", "crypto-browserify": "3.12.0",
"framer-motion": "6.2.8", "framer-motion": "6.2.10",
"gatsby": "4.11.1", "gatsby": "4.12.1",
"gatsby-background-image": "1.6.0", "gatsby-background-image": "1.6.0",
"gatsby-plugin-image": "2.11.1", "gatsby-plugin-image": "2.11.1",
"gatsby-plugin-manifest": "4.11.1", "gatsby-plugin-manifest": "4.11.1",
"gatsby-plugin-postcss": "5.10.0", "gatsby-plugin-postcss": "5.12.1",
"gatsby-plugin-react-helmet": "5.10.0", "gatsby-plugin-react-helmet": "5.10.0",
"gatsby-plugin-robots-txt": "1.7.0", "gatsby-plugin-robots-txt": "1.7.1",
"gatsby-plugin-sharp": "4.10.2", "gatsby-plugin-sharp": "4.12.1",
"gatsby-plugin-sitemap": "5.10.2", "gatsby-plugin-sitemap": "5.11.1",
"gatsby-plugin-svgr": "3.0.0-beta.0", "gatsby-plugin-svgr": "3.0.0-beta.0",
"gatsby-source-filesystem": "4.10.1", "gatsby-source-filesystem": "4.10.1",
"gatsby-transformer-sharp": "4.10.0", "gatsby-transformer-sharp": "4.10.0",
@ -37,7 +37,7 @@
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-dropzone": "12.0.4", "react-dropzone": "12.0.5",
"react-helmet": "6.1.0", "react-helmet": "6.1.0",
"react-use": "17.3.2", "react-use": "17.3.2",
"skynet-js": "4.0.26-beta", "skynet-js": "4.0.26-beta",
@ -49,8 +49,8 @@
"autoprefixer": "10.4.4", "autoprefixer": "10.4.4",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "9.5.2", "cypress": "9.5.2",
"prettier": "2.6.1", "prettier": "2.6.2",
"tailwindcss": "3.0.23" "tailwindcss": "3.0.24"
}, },
"keywords": [ "keywords": [
"gatsby" "gatsby"

View File

@ -2594,10 +2594,10 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/sharp@^0.29.5": "@types/sharp@^0.30.0":
version "0.29.5" version "0.30.2"
resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.5.tgz#9c7032d30d138ad16dde6326beaff2af757b91b3" resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.2.tgz#df5ff34140b3bad165482e6f3d26b08e42a0503a"
integrity sha512-3TC+S3H5RwnJmLYMHrcdfNjz/CaApKmujjY9b6PU/pE6n0qfooi99YqXGWoW8frU9EWYj/XTI35Pzxa+ThAZ5Q== integrity sha512-uLCBwjDg/BTcQit0dpNGvkIjvH3wsb8zpaJePCjvONBBSfaKHoxXBIuq1MT8DMQEfk2fKYnpC9QExCgFhkGkMQ==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
@ -3339,23 +3339,23 @@ babel-plugin-polyfill-regenerator@^0.3.0:
dependencies: dependencies:
"@babel/helper-define-polyfill-provider" "^0.3.1" "@babel/helper-define-polyfill-provider" "^0.3.1"
babel-plugin-remove-graphql-queries@^4.11.1: babel-plugin-remove-graphql-queries@^4.11.1, babel-plugin-remove-graphql-queries@^4.12.1:
version "4.11.1" version "4.12.1"
resolved "https://registry.yarnpkg.com/babel-plugin-remove-graphql-queries/-/babel-plugin-remove-graphql-queries-4.11.1.tgz#6f107865e8c1a83807c4b48b2262f5e0e0ba537e" resolved "https://registry.yarnpkg.com/babel-plugin-remove-graphql-queries/-/babel-plugin-remove-graphql-queries-4.12.1.tgz#08e7531ed3c61aaa3c2f083ddce8040844e611d4"
integrity sha512-Bqbeow4Xf+Vm4YhAucRGJjf9pNAXakSndYiLKfvef/W6mdtBh00SM8FMaX0U3rtR7ZUXV63RmIyOybVQ6SWCyg== integrity sha512-z4Z0VkDpmoIW3cihPYEb+HJMgwa+RF77LnpgAC6y6ozS76ci3ENqfIry/vvdD6auys5TG3xYZ0eHpdPobXzhfA==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
gatsby-core-utils "^3.11.1" gatsby-core-utils "^3.12.1"
babel-plugin-transform-react-remove-prop-types@^0.4.24: babel-plugin-transform-react-remove-prop-types@^0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a"
integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==
babel-preset-gatsby@^2.11.1: babel-preset-gatsby@^2.12.1:
version "2.11.1" version "2.12.1"
resolved "https://registry.yarnpkg.com/babel-preset-gatsby/-/babel-preset-gatsby-2.11.1.tgz#860d8d9903df38c314fa6f0cfdb197d02555c4e4" resolved "https://registry.yarnpkg.com/babel-preset-gatsby/-/babel-preset-gatsby-2.12.1.tgz#33d904dc54d5395e049fb346015eba1dbd62bfbf"
integrity sha512-NGUNAIb3hzD1Mt97q5T3gSSuVuaqnYFSm7AvgByDa3Mk2ohF5Ni86sCLVPRIntIzJvgU5OWY4Qz+6rrI1SwprQ== integrity sha512-ozpDqxxQa32gZVeXO07S0jLJvfewzMLAytP6QHJvVlHEcDnfo7sTo/r3ZNm+2SzeHP51eTDuTFo46WWQnY5kMw==
dependencies: dependencies:
"@babel/plugin-proposal-class-properties" "^7.14.0" "@babel/plugin-proposal-class-properties" "^7.14.0"
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5"
@ -3370,8 +3370,8 @@ babel-preset-gatsby@^2.11.1:
babel-plugin-dynamic-import-node "^2.3.3" babel-plugin-dynamic-import-node "^2.3.3"
babel-plugin-macros "^2.8.0" babel-plugin-macros "^2.8.0"
babel-plugin-transform-react-remove-prop-types "^0.4.24" babel-plugin-transform-react-remove-prop-types "^0.4.24"
gatsby-core-utils "^3.11.1" gatsby-core-utils "^3.12.1"
gatsby-legacy-polyfills "^2.11.0" gatsby-legacy-polyfills "^2.12.1"
backo2@^1.0.2, backo2@~1.0.2: backo2@^1.0.2, backo2@~1.0.2:
version "1.0.2" version "1.0.2"
@ -3566,7 +3566,7 @@ braces@^2.3.1:
split-string "^3.0.2" split-string "^3.0.2"
to-regex "^3.0.1" to-regex "^3.0.1"
braces@^3.0.1, braces@~3.0.2: braces@^3.0.2, braces@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@ -4340,10 +4340,10 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0" bn.js "^4.1.0"
elliptic "^6.5.3" elliptic "^6.5.3"
create-gatsby@^2.11.1: create-gatsby@^2.12.1:
version "2.11.1" version "2.12.1"
resolved "https://registry.yarnpkg.com/create-gatsby/-/create-gatsby-2.11.1.tgz#8d73cce07ff0006386795ca1b74a0bdbb023500b" resolved "https://registry.yarnpkg.com/create-gatsby/-/create-gatsby-2.12.1.tgz#da5ab2b6dde54d62ec853b699c5aceafb0f166a2"
integrity sha512-ltSLSsbQRoCXxKzgkxp5PBv60O1BL0IdeKKbgmwEcYxiDVw4pXPcFmIqMmvHfk9fqzbCyPzehIQHdlEpJGDYwQ== integrity sha512-dOsEy9feLJVFVzFFnA6xJL9OhfYcKewaGMqI9uUaUdifIehBjb5jdeWi+cNy49j2FQLMm38jfZ2SNSQjEK2yOw==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
@ -4710,10 +4710,10 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1: debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1:
version "4.3.3" version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies: dependencies:
ms "2.1.2" ms "2.1.2"
@ -5384,15 +5384,15 @@ eslint-plugin-jsx-a11y@^6.5.1:
language-tags "^1.0.5" language-tags "^1.0.5"
minimatch "^3.0.4" minimatch "^3.0.4"
eslint-plugin-react-hooks@^4.3.0: eslint-plugin-react-hooks@^4.4.0:
version "4.3.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz#71c39e528764c848d8253e1aa2c7024ed505f6c4"
integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== integrity sha512-U3RVIfdzJaeKDQKEJbz5p3NW8/L80PCATJAfuojwbaEL+gBjfGdhUcGde+WGUW46Q5sr/NgxevsIiDtNXrvZaQ==
eslint-plugin-react@^7.29.2: eslint-plugin-react@^7.29.4:
version "7.29.2" version "7.29.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.29.2.tgz#2d4da69d30d0a736efd30890dc6826f3e91f3f7c" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz#4717de5227f55f3801a5fd51a16a4fa22b5914d2"
integrity sha512-ypEBTKOy5liFQXZWMchJ3LN0JX1uPI6n7MN7OPHKacqXAxq5gYC30TdO7wqGYQyxD1OrzpobdHC3hDmlRWDg9w== integrity sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==
dependencies: dependencies:
array-includes "^3.1.4" array-includes "^3.1.4"
array.prototype.flatmap "^1.2.5" array.prototype.flatmap "^1.2.5"
@ -6068,10 +6068,10 @@ fragment-cache@^0.2.1:
dependencies: dependencies:
map-cache "^0.2.2" map-cache "^0.2.2"
framer-motion@6.2.8: framer-motion@6.2.10:
version "6.2.8" version "6.2.10"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.2.8.tgz#02abb529191af7e2df444185fe27e932215b715d" resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.2.10.tgz#7fb300d4c559f0991be5499f99d055a04851fcb3"
integrity sha512-4PtBWFJ6NqR350zYVt9AsFDtISTqsdqna79FvSYPfYDXuuqFmiKtZdkTnYPslnsOMedTW0pEvaQ7eqjD+sA+HA== integrity sha512-nfkpA5r3leVOYJH0YXV1cMOLNJuAoznR3Cswet5wCIDi7AZwS62N+u0EmGSNG1JHtglDo5erqyamc55M2XICvA==
dependencies: dependencies:
framesync "6.0.1" framesync "6.0.1"
hey-listen "^1.0.8" hey-listen "^1.0.8"
@ -6158,10 +6158,10 @@ gatsby-background-image@1.6.0:
short-uuid "^4.2.0" short-uuid "^4.2.0"
sort-media-queries "^0.2.2" sort-media-queries "^0.2.2"
gatsby-cli@^4.11.1: gatsby-cli@^4.12.1:
version "4.11.1" version "4.12.1"
resolved "https://registry.yarnpkg.com/gatsby-cli/-/gatsby-cli-4.11.1.tgz#a549a91cbd7e7bb9a98413cf604af09d10ef75c2" resolved "https://registry.yarnpkg.com/gatsby-cli/-/gatsby-cli-4.12.1.tgz#157d6dbe783102248af2c938816a58401afeb64b"
integrity sha512-RDOFIzKAyysa51x0mMoMtdVhyOX2UkBuEyelGqpuchl8b/ddka/cjEYHk3QRSq55+cBN0/1cTHt/A139ooAKUg== integrity sha512-vlSqri0p9HpLfACFtUCJhxQArzxSvdcUkrN4Jlw8RgeJYxcJyb8VPPDJHJT3rMGRKZFeBaAeqMbqx/eK4K5F1w==
dependencies: dependencies:
"@babel/code-frame" "^7.14.0" "@babel/code-frame" "^7.14.0"
"@babel/core" "^7.15.5" "@babel/core" "^7.15.5"
@ -6179,13 +6179,13 @@ gatsby-cli@^4.11.1:
common-tags "^1.8.2" common-tags "^1.8.2"
configstore "^5.0.1" configstore "^5.0.1"
convert-hrtime "^3.0.0" convert-hrtime "^3.0.0"
create-gatsby "^2.11.1" create-gatsby "^2.12.1"
envinfo "^7.8.1" envinfo "^7.8.1"
execa "^5.1.1" execa "^5.1.1"
fs-exists-cached "^1.0.0" fs-exists-cached "^1.0.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
gatsby-core-utils "^3.11.1" gatsby-core-utils "^3.12.1"
gatsby-telemetry "^3.11.1" gatsby-telemetry "^3.12.1"
hosted-git-info "^3.0.8" hosted-git-info "^3.0.8"
is-valid-path "^0.1.1" is-valid-path "^0.1.1"
joi "^17.4.2" joi "^17.4.2"
@ -6209,10 +6209,10 @@ gatsby-cli@^4.11.1:
yoga-layout-prebuilt "^1.10.0" yoga-layout-prebuilt "^1.10.0"
yurnalist "^2.1.0" yurnalist "^2.1.0"
gatsby-core-utils@^3.10.1, gatsby-core-utils@^3.11.1, gatsby-core-utils@^3.8.2: gatsby-core-utils@^3.10.1, gatsby-core-utils@^3.11.1, gatsby-core-utils@^3.12.1, gatsby-core-utils@^3.8.2:
version "3.11.1" version "3.12.1"
resolved "https://registry.yarnpkg.com/gatsby-core-utils/-/gatsby-core-utils-3.11.1.tgz#ea87c1d3aa45c26c9ea32b8e8b029afe6a56f8c7" resolved "https://registry.yarnpkg.com/gatsby-core-utils/-/gatsby-core-utils-3.12.1.tgz#590ec08de7168b086b7d49128732b9ada95b985f"
integrity sha512-Op9/uihtcsDLlZDfRsGJ1ya2mFx2YH9Zmx93bawElZ0YpIzKjCkNTp+I5i5UANxvs5I+Fljl0WHQRudMWg+fWA== integrity sha512-jBG1MfR6t2MZNIl8LQ3Cwc92F6uFNcEC091IK+qKVy9FNT0+WzcKQ6Olip6u1NSvCatfrg1FqrH0K78a6lmnLQ==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
ci-info "2.0.0" ci-info "2.0.0"
@ -6222,7 +6222,7 @@ gatsby-core-utils@^3.10.1, gatsby-core-utils@^3.11.1, gatsby-core-utils@^3.8.2:
fs-extra "^10.0.0" fs-extra "^10.0.0"
got "^11.8.3" got "^11.8.3"
import-from "^4.0.0" import-from "^4.0.0"
lmdb "^2.2.4" lmdb "^2.2.6"
lock "^1.1.0" lock "^1.1.0"
node-object-hash "^2.3.10" node-object-hash "^2.3.10"
proper-lockfile "^4.1.2" proper-lockfile "^4.1.2"
@ -6230,49 +6230,49 @@ gatsby-core-utils@^3.10.1, gatsby-core-utils@^3.11.1, gatsby-core-utils@^3.8.2:
tmp "^0.2.1" tmp "^0.2.1"
xdg-basedir "^4.0.0" xdg-basedir "^4.0.0"
gatsby-graphiql-explorer@^2.11.0: gatsby-graphiql-explorer@^2.12.1:
version "2.11.0" version "2.12.1"
resolved "https://registry.yarnpkg.com/gatsby-graphiql-explorer/-/gatsby-graphiql-explorer-2.11.0.tgz#7e886846482ad72bd49f515e7faa658a94342803" resolved "https://registry.yarnpkg.com/gatsby-graphiql-explorer/-/gatsby-graphiql-explorer-2.12.1.tgz#4fad5b9a3ccbcc4871f70222e8ac1ca880ff844d"
integrity sha512-nMNXlF/pleO/rH66t00SdXdKq3vV0/Su5EEQY7xg3yRc38ueC2UkZq10nrJiVoc05RO8Txo5o2gpoC2DP07lFg== integrity sha512-H5phTjIGUiUZxN3C0hogH66lB+qC9HO9O4m4RpHZ3JyxVIvPemGSNmgovhL7+LydS34UY5rbT0UBFwaxrHMZpQ==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
gatsby-legacy-polyfills@^2.11.0: gatsby-legacy-polyfills@^2.12.1:
version "2.11.0" version "2.12.1"
resolved "https://registry.yarnpkg.com/gatsby-legacy-polyfills/-/gatsby-legacy-polyfills-2.11.0.tgz#8b2afc4d97f44eb5767fe9b49f55ff675055ffd2" resolved "https://registry.yarnpkg.com/gatsby-legacy-polyfills/-/gatsby-legacy-polyfills-2.12.1.tgz#62ad6432b3c17f7f5640786ed00dba88da60ab22"
integrity sha512-ulkRNCitwFjwUM4f2ufljH0WjELm6QEIOGRryNRt9LKJEB9QGmdm+KUAWIv7xrFUqKq1Pn6is64wcfXDw21zSA== integrity sha512-x2Njk0GsBKsiVBDZHI7nVWDNBPQeonQsElzFEDoSJpW47j9H8PPJDeOUZ+u5q76rtxuQQo/VXl/eD817qRBxAA==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
core-js-compat "3.9.0" core-js-compat "3.9.0"
gatsby-link@^4.11.1: gatsby-link@^4.12.1:
version "4.11.1" version "4.12.1"
resolved "https://registry.yarnpkg.com/gatsby-link/-/gatsby-link-4.11.1.tgz#f8bfee4c7f3bf0ede255bddf87d0f13c64ed39f2" resolved "https://registry.yarnpkg.com/gatsby-link/-/gatsby-link-4.12.1.tgz#131448e9c51e0c9a65e2a342603d496e85cea2da"
integrity sha512-wOhdgsnzHr4iYWo3iKadw8jj5PmIu1wbi6LUftwQzFOFvkBaJvC/br1ju8W0nbwSjWG474hTZRon43xDQX9bIw== integrity sha512-ILWYNqyTlEt2bOVWgzwmbijwC+Ow4CZVbnWOyaQ/jvu5z3ZGL0z5tGGD+sjZAHc8anOMWn/JWhL0BKGVaxjMGQ==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
"@types/reach__router" "^1.3.10" "@types/reach__router" "^1.3.10"
gatsby-page-utils "^2.11.1" gatsby-page-utils "^2.12.1"
prop-types "^15.7.2" prop-types "^15.7.2"
gatsby-page-utils@^2.11.1: gatsby-page-utils@^2.12.1:
version "2.11.1" version "2.12.1"
resolved "https://registry.yarnpkg.com/gatsby-page-utils/-/gatsby-page-utils-2.11.1.tgz#dd10f99184b64528ae76f2b654b8ed1b23cb9c39" resolved "https://registry.yarnpkg.com/gatsby-page-utils/-/gatsby-page-utils-2.12.1.tgz#ee5dbd21b8e22003e5b0c7e4d86ed3b89773c0ac"
integrity sha512-K1Mbk4CKYZwpJcE4zk4JAff7ZBNFXI0fC8lZwLbDAzVcqYUaouqqqnoU7WeB8HHUqDQi05CXItx1bbZFDGIymw== integrity sha512-2NPfVHRoHYcqUZrGVAvHN28uqI/PTGE/DrpE79YR/blbnloEzwzpAGNbBjWitgcR0st5q5NrATJQ/Imu3M7ApA==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
bluebird "^3.7.2" bluebird "^3.7.2"
chokidar "^3.5.2" chokidar "^3.5.2"
fs-exists-cached "^1.0.0" fs-exists-cached "^1.0.0"
gatsby-core-utils "^3.11.1" gatsby-core-utils "^3.12.1"
glob "^7.2.0" glob "^7.2.0"
lodash "^4.17.21" lodash "^4.17.21"
micromatch "^4.0.4" micromatch "^4.0.5"
gatsby-parcel-config@^0.2.0: gatsby-parcel-config@^0.3.1:
version "0.2.0" version "0.3.1"
resolved "https://registry.yarnpkg.com/gatsby-parcel-config/-/gatsby-parcel-config-0.2.0.tgz#0b1795d17c825bd293c372fa0acfa987aa91111e" resolved "https://registry.yarnpkg.com/gatsby-parcel-config/-/gatsby-parcel-config-0.3.1.tgz#424c7b3d65b60e2e454cd9d61d4c5de752aad576"
integrity sha512-BbsSm5O0R7IvCRLNSk3lBpkU8RtSOn8s7Ifa7bHF63PzTG1SUpBjwMF6301tCbvdSXWrP7n9dsfaXS6ex/TElQ== integrity sha512-Wpz6DSKiWeqVyZUNmO7EHy0h9ISG+HfUD8v2g0kN4ZcZjJtSiWvGym1+6Swgjo9bQvy59qa7bO4hKGA9gHvMVg==
dependencies: dependencies:
"@gatsbyjs/parcel-namer-relative-to-cwd" "0.0.2" "@gatsbyjs/parcel-namer-relative-to-cwd" "0.0.2"
"@parcel/bundler-default" "^2.3.2" "@parcel/bundler-default" "^2.3.2"
@ -6322,27 +6322,27 @@ gatsby-plugin-manifest@4.11.1:
semver "^7.3.5" semver "^7.3.5"
sharp "^0.30.1" sharp "^0.30.1"
gatsby-plugin-page-creator@^4.11.1: gatsby-plugin-page-creator@^4.12.1:
version "4.11.1" version "4.12.1"
resolved "https://registry.yarnpkg.com/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-4.11.1.tgz#750f4b773684777cec6caa9266787427ed2630a6" resolved "https://registry.yarnpkg.com/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-4.12.1.tgz#36460f5308df8b04bb2b0649150cb87e3eb15c8f"
integrity sha512-6XET4qYqu2yVwUU6sO44wSR62zQZdq7BoMvN9OhKpUDBZYLfve9CwufkhZZnQvq+axNZZMUmKa/RqbBXiE6/yA== integrity sha512-McVYpXWgneo1+3+8KGrGATgNwAYtZXbFKL8Q18lwH+bt5f2NbYP23g+xGitxT62zvhhzs0AjuEJa7BoTEmFTMQ==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
"@babel/traverse" "^7.15.4" "@babel/traverse" "^7.15.4"
"@sindresorhus/slugify" "^1.1.2" "@sindresorhus/slugify" "^1.1.2"
chokidar "^3.5.2" chokidar "^3.5.2"
fs-exists-cached "^1.0.0" fs-exists-cached "^1.0.0"
gatsby-core-utils "^3.11.1" gatsby-core-utils "^3.12.1"
gatsby-page-utils "^2.11.1" gatsby-page-utils "^2.12.1"
gatsby-plugin-utils "^3.5.1" gatsby-plugin-utils "^3.6.1"
gatsby-telemetry "^3.11.1" gatsby-telemetry "^3.12.1"
globby "^11.0.4" globby "^11.1.0"
lodash "^4.17.21" lodash "^4.17.21"
gatsby-plugin-postcss@5.10.0: gatsby-plugin-postcss@5.12.1:
version "5.10.0" version "5.12.1"
resolved "https://registry.yarnpkg.com/gatsby-plugin-postcss/-/gatsby-plugin-postcss-5.10.0.tgz#e241f1671e66f7b660826f39fd26591aae652716" resolved "https://registry.yarnpkg.com/gatsby-plugin-postcss/-/gatsby-plugin-postcss-5.12.1.tgz#8be5cf7bbfbb27967cad2c5da8dbe1b8c7e576b8"
integrity sha512-s1zzysu1kKIqR+CfQeQsG0CCdj2S7tjc4BhCY2a3V4cl7ORJtMx1HGKDUzE9gV/EXRTmr9lhE9Gl+2v8fRouvA== integrity sha512-gNfMl/ZWCZpnCsy+IPzRPzqaPPStbUr/LG8eikGbyoZj1gSjom/a7Hi3z29sWx/vEd4dAHGRPS+n7bjf3iOnNw==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
postcss-loader "^4.3.0" postcss-loader "^4.3.0"
@ -6354,43 +6354,43 @@ gatsby-plugin-react-helmet@5.10.0:
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
gatsby-plugin-robots-txt@1.7.0: gatsby-plugin-robots-txt@1.7.1:
version "1.7.0" version "1.7.1"
resolved "https://registry.yarnpkg.com/gatsby-plugin-robots-txt/-/gatsby-plugin-robots-txt-1.7.0.tgz#58ac310c9fb7e58162d6e21802884b342837b451" resolved "https://registry.yarnpkg.com/gatsby-plugin-robots-txt/-/gatsby-plugin-robots-txt-1.7.1.tgz#f956729e34f6269cc314352e9ef1cf7b4c515a68"
integrity sha512-Y1D8FBeXNtECoCd0g0jIkhKpSvzFzeh2xpt1xTvGluRP6xmqJq7iB3DPEv7xqGlZAcfzaSxw/j5++Y+3WLva8A== integrity sha512-ZdZm8/4b7Whf+W5kf+DqjZwz/+DY+IB7xp227+m2f2rgGUsz8yVCz4RitiN5+EInGFZFry0v+IbrUKCXTpIZYg==
dependencies: dependencies:
"@babel/runtime" "^7.16.7" "@babel/runtime" "^7.16.7"
generate-robotstxt "^8.0.3" generate-robotstxt "^8.0.3"
gatsby-plugin-sharp@4.10.2: gatsby-plugin-sharp@4.12.1:
version "4.10.2" version "4.12.1"
resolved "https://registry.yarnpkg.com/gatsby-plugin-sharp/-/gatsby-plugin-sharp-4.10.2.tgz#253a49c452a7409ceece4e541e4770e61a306bcc" resolved "https://registry.yarnpkg.com/gatsby-plugin-sharp/-/gatsby-plugin-sharp-4.12.1.tgz#f97cb29074f4a07b469b9ddb1058a3574928d4eb"
integrity sha512-MWzPTYnu7HZ0kctHtkLbZOe6ZGUqSsNATO3lWlSBIFpeimxaPF5iHBiu1CX/ofz4pwt7VamtIzAV28VB6sjONw== integrity sha512-P6noUl5LyASwYtCRSo1rjchk/ytfJvSFTLwzgXr1TiQHgZh06SUIqR8v3UqT90EDERNd1GeEBsQjRfWkrV2nbg==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
async "^3.2.3" async "^3.2.3"
bluebird "^3.7.2" bluebird "^3.7.2"
debug "^4.3.3" debug "^4.3.4"
filenamify "^4.3.0" filenamify "^4.3.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
gatsby-core-utils "^3.10.1" gatsby-core-utils "^3.12.1"
gatsby-plugin-utils "^3.4.2" gatsby-plugin-utils "^3.6.1"
gatsby-telemetry "^3.10.1" gatsby-telemetry "^3.12.1"
got "^11.8.3" got "^11.8.3"
lodash "^4.17.21" lodash "^4.17.21"
mini-svg-data-uri "^1.4.3" mini-svg-data-uri "^1.4.4"
potrace "^2.1.8" potrace "^2.1.8"
probe-image-size "^7.0.0" probe-image-size "^7.2.3"
progress "^2.0.3" progress "^2.0.3"
semver "^7.3.5" semver "^7.3.5"
sharp "^0.30.1" sharp "^0.30.3"
svgo "1.3.2" svgo "1.3.2"
uuid "3.4.0" uuid "3.4.0"
gatsby-plugin-sitemap@5.10.2: gatsby-plugin-sitemap@5.11.1:
version "5.10.2" version "5.11.1"
resolved "https://registry.yarnpkg.com/gatsby-plugin-sitemap/-/gatsby-plugin-sitemap-5.10.2.tgz#208149b900b166c42aa88a5f5436f5c6bf6561e9" resolved "https://registry.yarnpkg.com/gatsby-plugin-sitemap/-/gatsby-plugin-sitemap-5.11.1.tgz#863397fe9dd5aab89bda8db09ef9b877c960150e"
integrity sha512-X6pVbytl/QfdfGrnXAEKPf5vc38WIbclmHYIfbgjXUYA9yckTxnfuYZqkS2YwCmbcUTHG1ugcmXMeBGVo77IBQ== integrity sha512-tt92KLUDS+eCrqSA5oYieDGjXLyUDXfYKEwLhYKXk7KlMMjporFJWVrc4Ba8WD04bUWVnzc2rqr19/zQI0ZIpQ==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
common-tags "^1.8.2" common-tags "^1.8.2"
@ -6402,10 +6402,10 @@ gatsby-plugin-svgr@3.0.0-beta.0:
resolved "https://registry.yarnpkg.com/gatsby-plugin-svgr/-/gatsby-plugin-svgr-3.0.0-beta.0.tgz#7e5315f51dae2663a447899322ea1487cef93dd6" resolved "https://registry.yarnpkg.com/gatsby-plugin-svgr/-/gatsby-plugin-svgr-3.0.0-beta.0.tgz#7e5315f51dae2663a447899322ea1487cef93dd6"
integrity sha512-oALTh6VwO6l3khgC/vGr706aqt38EkXwdr6iXVei/auOKGxpCLEuDCQVal1a4SpYXdjHjRsEyab6bxaHL2lzsA== integrity sha512-oALTh6VwO6l3khgC/vGr706aqt38EkXwdr6iXVei/auOKGxpCLEuDCQVal1a4SpYXdjHjRsEyab6bxaHL2lzsA==
gatsby-plugin-typescript@^4.11.1: gatsby-plugin-typescript@^4.12.1:
version "4.11.1" version "4.12.1"
resolved "https://registry.yarnpkg.com/gatsby-plugin-typescript/-/gatsby-plugin-typescript-4.11.1.tgz#dcd96ded685f8c4a73ae5524faab342f9c9e3c1d" resolved "https://registry.yarnpkg.com/gatsby-plugin-typescript/-/gatsby-plugin-typescript-4.12.1.tgz#8fe51b57f1fca5027f5dd558b73364a4c34a0a38"
integrity sha512-6ef2wRhPqcLPyekEAU3xcoqI59r+mDnCzn/O+8hRgwJyx/2dwvF8brusetXoqdTk4Vyhk44p8dog8+gCGATckw== integrity sha512-7ZzGTL+hNGGmiIk4j4QSZYyYsy4i9EW/zgK/IJwmpSBNzoagI/Pz64ntNWpxZstfgzkuIYZfvuvj3Ao9mKF5aw==
dependencies: dependencies:
"@babel/core" "^7.15.5" "@babel/core" "^7.15.5"
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5"
@ -6413,37 +6413,37 @@ gatsby-plugin-typescript@^4.11.1:
"@babel/plugin-proposal-optional-chaining" "^7.14.5" "@babel/plugin-proposal-optional-chaining" "^7.14.5"
"@babel/preset-typescript" "^7.15.0" "@babel/preset-typescript" "^7.15.0"
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
babel-plugin-remove-graphql-queries "^4.11.1" babel-plugin-remove-graphql-queries "^4.12.1"
gatsby-plugin-utils@^3.4.2, gatsby-plugin-utils@^3.5.1: gatsby-plugin-utils@^3.5.1, gatsby-plugin-utils@^3.6.1:
version "3.5.1" version "3.6.1"
resolved "https://registry.yarnpkg.com/gatsby-plugin-utils/-/gatsby-plugin-utils-3.5.1.tgz#9aed9deec0f4ee82bfc7390f735b9455ca4f8494" resolved "https://registry.yarnpkg.com/gatsby-plugin-utils/-/gatsby-plugin-utils-3.6.1.tgz#31d742e1aded08439ad42959880821e1fc9740cd"
integrity sha512-RZXUvwQjTnkukMfAGr+DCz/qZj7g6REljTmQS43MaovWO4Yf4YGvs+1Leays7J0XmqN2I3SIZGBgt4tgKCsNVQ== integrity sha512-Ebk98v4mxaDWjGFl6VBeNv1zjeJ7UCQ29UTabzY2BpztvUCBHfLVQdMmuaAgzPRn+A3SFVOGpcl++CF0IEl+7A==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
fs-extra "^10.0.0" fs-extra "^10.0.0"
gatsby-core-utils "^3.11.1" gatsby-core-utils "^3.12.1"
gatsby-sharp "^0.5.0" gatsby-sharp "^0.6.1"
graphql-compose "^9.0.7" graphql-compose "^9.0.7"
import-from "^4.0.0" import-from "^4.0.0"
joi "^17.4.2" joi "^17.4.2"
mime "^3.0.0" mime "^3.0.0"
gatsby-react-router-scroll@^5.11.0: gatsby-react-router-scroll@^5.12.1:
version "5.11.0" version "5.12.1"
resolved "https://registry.yarnpkg.com/gatsby-react-router-scroll/-/gatsby-react-router-scroll-5.11.0.tgz#866b89366146d8df3852ed699d12be1e9fce4acc" resolved "https://registry.yarnpkg.com/gatsby-react-router-scroll/-/gatsby-react-router-scroll-5.12.1.tgz#10fdf43c1179ae53e7726c6c8d894139e7862e8f"
integrity sha512-g/lyG0X73cpI9DdYvCv5rZiV8LqHjn6q1l8Vfm/jBS7wtv8XxNR4BxUqkbMeHRcvZcX5bXku6FFSFUAOd9c3QQ== integrity sha512-zZCTiicALh6eSsQAgIhSCmQm6Dl6fY6eaKmOXGMMbVtUKmiGxikh2MFN6S5J5JU9MV/piSheVqYkouyTDGXbuw==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
prop-types "^15.7.2" prop-types "^15.8.1"
gatsby-sharp@^0.5.0: gatsby-sharp@^0.6.1:
version "0.5.0" version "0.6.1"
resolved "https://registry.yarnpkg.com/gatsby-sharp/-/gatsby-sharp-0.5.0.tgz#879d3c462eefa917cb3a50c6ec891951d9740f56" resolved "https://registry.yarnpkg.com/gatsby-sharp/-/gatsby-sharp-0.6.1.tgz#48805690cb020111722cc25445b4b662446da6d0"
integrity sha512-9wZS0ADZsKTCsU66sxIP/tCHgFaREyoYm53tepgtp/YSVWNrurx9/0kGf8XsFFY9OecrqIRNuk1cWe7XKCpbQA== integrity sha512-KhBFE72QLlrAgeMWNoBV2LDp0nZ9ZOw1pY5wIohb/ktDFRUi9K5nwVCJvDJonfPn100mxtDqnZVckXirtcHVzQ==
dependencies: dependencies:
"@types/sharp" "^0.29.5" "@types/sharp" "^0.30.0"
sharp "^0.30.1" sharp "^0.30.3"
gatsby-source-filesystem@4.10.1: gatsby-source-filesystem@4.10.1:
version "4.10.1" version "4.10.1"
@ -6463,10 +6463,10 @@ gatsby-source-filesystem@4.10.1:
valid-url "^1.0.9" valid-url "^1.0.9"
xstate "^4.26.1" xstate "^4.26.1"
gatsby-telemetry@^3.10.1, gatsby-telemetry@^3.11.1: gatsby-telemetry@^3.12.1:
version "3.11.1" version "3.12.1"
resolved "https://registry.yarnpkg.com/gatsby-telemetry/-/gatsby-telemetry-3.11.1.tgz#87caed899143276056e9af20ab38c15ad9dcdf52" resolved "https://registry.yarnpkg.com/gatsby-telemetry/-/gatsby-telemetry-3.12.1.tgz#a3508a45d95f2c3457db7dbe2628560d00c43beb"
integrity sha512-TPNKTpuYFyULOuRvhpXUtj8h2E7bvrTYsRC/aKeHoWqEchwwbzPwBSJd+3ZFjsxLHIXAa5sTAlR2wd9SYBgOlA== integrity sha512-sAL2T9GdYpceGlFP6CymVDoy0UEhRvrJApv/mu7sU6F0gu8g8rOLvRxVYE3Y2D9RdfCzkuLIonzmscmVIduyOg==
dependencies: dependencies:
"@babel/code-frame" "^7.14.0" "@babel/code-frame" "^7.14.0"
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
@ -6476,7 +6476,7 @@ gatsby-telemetry@^3.10.1, gatsby-telemetry@^3.11.1:
boxen "^4.2.0" boxen "^4.2.0"
configstore "^5.0.1" configstore "^5.0.1"
fs-extra "^10.0.0" fs-extra "^10.0.0"
gatsby-core-utils "^3.11.1" gatsby-core-utils "^3.12.1"
git-up "^4.0.5" git-up "^4.0.5"
is-docker "^2.2.1" is-docker "^2.2.1"
lodash "^4.17.21" lodash "^4.17.21"
@ -6506,18 +6506,18 @@ gatsby-transformer-yaml@4.11.0:
lodash "^4.17.21" lodash "^4.17.21"
unist-util-select "^1.5.0" unist-util-select "^1.5.0"
gatsby-worker@^1.11.0: gatsby-worker@^1.12.1:
version "1.11.0" version "1.12.1"
resolved "https://registry.yarnpkg.com/gatsby-worker/-/gatsby-worker-1.11.0.tgz#bf8c3b9374390260b8335d7cfccbc332d0716727" resolved "https://registry.yarnpkg.com/gatsby-worker/-/gatsby-worker-1.12.1.tgz#553a0fd5ad796567fc3ab965490270ff7b0ae680"
integrity sha512-uJ5bNrifIrS20o0SYkmb379logfRKO35cqYxd2R0uNf9kWGaQOda0SZfm7Uw+Vdx7cO9Ra8p1ArijbHm7ZArCA== integrity sha512-9slhXsK1/N4nJK+Yia84PL/zvNqV/bqD820W4R2f5jh5gEnVYrY2TcnG6A+UDbY7orhS0CLf1mMW9WKd6u6CUA==
dependencies: dependencies:
"@babel/core" "^7.15.5" "@babel/core" "^7.15.5"
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
gatsby@4.11.1: gatsby@4.12.1:
version "4.11.1" version "4.12.1"
resolved "https://registry.yarnpkg.com/gatsby/-/gatsby-4.11.1.tgz#ffe7754c9c368fd746bdeca808572641a378addb" resolved "https://registry.yarnpkg.com/gatsby/-/gatsby-4.12.1.tgz#b9a72bca167662a05d7154e5e6091f31aa333efd"
integrity sha512-ffEXb/mvZtB0cQ8javEkhruubxjTbZSsN81IYGGY/ym4YB+Zm1a8K0NV7DsRGsPO9nx7Z/D/OBVxVmse1Nnxzw== integrity sha512-/QteQShPAW1dRmG9wrjHmdfQEQxh6WfOi9jnJXAxljAx8UlRt0JFntxMc9gWGUJD6fXYKmf13Jan9izuNDQxNQ==
dependencies: dependencies:
"@babel/code-frame" "^7.14.0" "@babel/code-frame" "^7.14.0"
"@babel/core" "^7.15.5" "@babel/core" "^7.15.5"
@ -6544,8 +6544,8 @@ gatsby@4.11.1:
babel-plugin-add-module-exports "^1.0.4" babel-plugin-add-module-exports "^1.0.4"
babel-plugin-dynamic-import-node "^2.3.3" babel-plugin-dynamic-import-node "^2.3.3"
babel-plugin-lodash "^3.3.4" babel-plugin-lodash "^3.3.4"
babel-plugin-remove-graphql-queries "^4.11.1" babel-plugin-remove-graphql-queries "^4.12.1"
babel-preset-gatsby "^2.11.1" babel-preset-gatsby "^2.12.1"
better-opn "^2.1.1" better-opn "^2.1.1"
bluebird "^3.7.2" bluebird "^3.7.2"
body-parser "^1.19.0" body-parser "^1.19.0"
@ -6574,8 +6574,8 @@ gatsby@4.11.1:
eslint-plugin-graphql "^4.0.0" eslint-plugin-graphql "^4.0.0"
eslint-plugin-import "^2.25.4" eslint-plugin-import "^2.25.4"
eslint-plugin-jsx-a11y "^6.5.1" eslint-plugin-jsx-a11y "^6.5.1"
eslint-plugin-react "^7.29.2" eslint-plugin-react "^7.29.4"
eslint-plugin-react-hooks "^4.3.0" eslint-plugin-react-hooks "^4.4.0"
eslint-webpack-plugin "^2.6.0" eslint-webpack-plugin "^2.6.0"
event-source-polyfill "^1.0.25" event-source-polyfill "^1.0.25"
execa "^5.1.1" execa "^5.1.1"
@ -6587,19 +6587,19 @@ gatsby@4.11.1:
find-cache-dir "^3.3.2" find-cache-dir "^3.3.2"
fs-exists-cached "1.0.0" fs-exists-cached "1.0.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
gatsby-cli "^4.11.1" gatsby-cli "^4.12.1"
gatsby-core-utils "^3.11.1" gatsby-core-utils "^3.12.1"
gatsby-graphiql-explorer "^2.11.0" gatsby-graphiql-explorer "^2.12.1"
gatsby-legacy-polyfills "^2.11.0" gatsby-legacy-polyfills "^2.12.1"
gatsby-link "^4.11.1" gatsby-link "^4.12.1"
gatsby-page-utils "^2.11.1" gatsby-page-utils "^2.12.1"
gatsby-parcel-config "^0.2.0" gatsby-parcel-config "^0.3.1"
gatsby-plugin-page-creator "^4.11.1" gatsby-plugin-page-creator "^4.12.1"
gatsby-plugin-typescript "^4.11.1" gatsby-plugin-typescript "^4.12.1"
gatsby-plugin-utils "^3.5.1" gatsby-plugin-utils "^3.6.1"
gatsby-react-router-scroll "^5.11.0" gatsby-react-router-scroll "^5.12.1"
gatsby-telemetry "^3.11.1" gatsby-telemetry "^3.12.1"
gatsby-worker "^1.11.0" gatsby-worker "^1.12.1"
glob "^7.2.0" glob "^7.2.0"
globby "^11.1.0" globby "^11.1.0"
got "^11.8.2" got "^11.8.2"
@ -6614,7 +6614,7 @@ gatsby@4.11.1:
joi "^17.4.2" joi "^17.4.2"
json-loader "^0.5.7" json-loader "^0.5.7"
latest-version "5.1.0" latest-version "5.1.0"
lmdb "^2.2.3" lmdb "~2.2.3"
lodash "^4.17.21" lodash "^4.17.21"
md5-file "^5.0.0" md5-file "^5.0.0"
meant "^1.0.3" meant "^1.0.3"
@ -6672,7 +6672,7 @@ gatsby@4.11.1:
xstate "^4.26.0" xstate "^4.26.0"
yaml-loader "^0.6.0" yaml-loader "^0.6.0"
optionalDependencies: optionalDependencies:
gatsby-sharp "^0.5.0" gatsby-sharp "^0.6.1"
gauge@~2.7.3: gauge@~2.7.3:
version "2.7.4" version "2.7.4"
@ -6894,7 +6894,7 @@ globby@11.0.3:
merge2 "^1.3.0" merge2 "^1.3.0"
slash "^3.0.0" slash "^3.0.0"
globby@^11.0.3, globby@^11.0.4, globby@^11.1.0: globby@^11.0.3, globby@^11.1.0:
version "11.1.0" version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
@ -8072,10 +8072,10 @@ levn@^0.4.1:
prelude-ls "^1.2.1" prelude-ls "^1.2.1"
type-check "~0.4.0" type-check "~0.4.0"
lilconfig@^2.0.3, lilconfig@^2.0.4: lilconfig@^2.0.3, lilconfig@^2.0.5:
version "2.0.4" version "2.0.5"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25"
integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==
lines-and-columns@^1.1.6: lines-and-columns@^1.1.6:
version "1.2.4" version "1.2.4"
@ -8096,6 +8096,36 @@ listr2@^3.8.3:
through "^2.3.8" through "^2.3.8"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
lmdb-darwin-arm64@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.3.2.tgz#edcd324e4693abfcd02e7c5ba44a7f0e209f7588"
integrity sha512-20lWWUPGKnSZRFY8FBm+vZEFx/5Deh0joz6cqJ8/0SuO/ejqRCppSsNqAxPqW87KUNR5rNfhaA2oRekMeb0cwQ==
lmdb-darwin-x64@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/lmdb-darwin-x64/-/lmdb-darwin-x64-2.3.2.tgz#6561f37d0461c3128b92fb80996c54e6243af939"
integrity sha512-BsBnOfgK1B11Dh4RgcgBTmkmsPv3mjBPKsA4W4E+18SW9K2aRi86CAMPXqjfY/OJDUe1pSrpVf1A83b8N/C9rg==
lmdb-linux-arm64@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/lmdb-linux-arm64/-/lmdb-linux-arm64-2.3.2.tgz#feb4a52a0030feb9520543c78919c30250a85107"
integrity sha512-DIibLZHpwwlIsP9cBRmw0xqDy6wZH+CDAnOTI+eihQ5PdWjTs+kaQs5O/x8l6/8fwCB0TPYKWTqfdUbvd/F7AA==
lmdb-linux-arm@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/lmdb-linux-arm/-/lmdb-linux-arm-2.3.2.tgz#d8ee33547cc0f671efcf63ca381306df51f972f5"
integrity sha512-ofxfxVQqMbaC2Ygjzk8k6xgS5Dg/3cANeLcEx14T35GoU5pQKlLAWjypptyLQEeOboEmEOpZmHMoD7sWu/zakQ==
lmdb-linux-x64@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/lmdb-linux-x64/-/lmdb-linux-x64-2.3.2.tgz#1c66a012199ab5235d3fcec8c3fdad573cf3eff4"
integrity sha512-HBUd013RRQ2KpiyBqqqSPSEwPpVUpTJZdTZGDVQFQZuxqyJumt4Wye3uh6ZgEiBtxzSelt4xvAeNjYPH0dcZSQ==
lmdb-win32-x64@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/lmdb-win32-x64/-/lmdb-win32-x64-2.3.2.tgz#e65f79bfbb9f09ebfa53d3b03959bc581ebda55a"
integrity sha512-/hir5oU+GYm7/B6QirrpyOmIuzCKiIbWoKIJI2ebXeJlrs6Jj7UY9caPBYVkCzd79QzJnB7hIlX/F6Jx6gcUmg==
lmdb@2.2.4: lmdb@2.2.4:
version "2.2.4" version "2.2.4"
resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.2.4.tgz#6494d5a1d1db152e0be759edcfa06893e4cbdb53" resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.2.4.tgz#6494d5a1d1db152e0be759edcfa06893e4cbdb53"
@ -8107,7 +8137,26 @@ lmdb@2.2.4:
ordered-binary "^1.2.4" ordered-binary "^1.2.4"
weak-lru-cache "^1.2.2" weak-lru-cache "^1.2.2"
lmdb@^2.0.2, lmdb@^2.2.3, lmdb@^2.2.4: lmdb@^2.0.2, lmdb@^2.2.6:
version "2.3.3"
resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.3.3.tgz#ee0c53a5f5c2509c566b891803b3322e5a59095f"
integrity sha512-CrooSvHOzd+jPXCXpiffu2+5m90Fe6L/cw90fg+4sCWNrw3W7/ad20CGuTkMVU7mAuwXEAJAfnUwvHN2pS9Rqg==
dependencies:
msgpackr "^1.5.4"
nan "^2.14.2"
node-addon-api "^4.3.0"
node-gyp-build-optional-packages "^4.3.2"
ordered-binary "^1.2.4"
weak-lru-cache "^1.2.2"
optionalDependencies:
lmdb-darwin-arm64 "2.3.2"
lmdb-darwin-x64 "2.3.2"
lmdb-linux-arm "2.3.2"
lmdb-linux-arm64 "2.3.2"
lmdb-linux-x64 "2.3.2"
lmdb-win32-x64 "2.3.2"
lmdb@~2.2.3:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.2.6.tgz#a52ef533812b8abcbe0033fc9d74d215e7dfc0a0" resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.2.6.tgz#a52ef533812b8abcbe0033fc9d74d215e7dfc0a0"
integrity sha512-UmQV0oZZcV3EN6rjcAjIiuWcc3MYZGWQ0GUYz46Ron5fuTa/dUow7WSQa6leFkvZIKVUdECBWVw96tckfEzUFQ== integrity sha512-UmQV0oZZcV3EN6rjcAjIiuWcc3MYZGWQ0GUYz46Ron5fuTa/dUow7WSQa6leFkvZIKVUdECBWVw96tckfEzUFQ==
@ -8579,13 +8628,13 @@ micromatch@^3.1.10:
snapdragon "^0.8.1" snapdragon "^0.8.1"
to-regex "^3.0.2" to-regex "^3.0.2"
micromatch@^4.0.4: micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.4" version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
dependencies: dependencies:
braces "^3.0.1" braces "^3.0.2"
picomatch "^2.2.3" picomatch "^2.3.1"
miller-rabin@^4.0.0: miller-rabin@^4.0.0:
version "4.0.1" version "4.0.1"
@ -8668,10 +8717,10 @@ mini-css-extract-plugin@1.6.2:
schema-utils "^3.0.0" schema-utils "^3.0.0"
webpack-sources "^1.1.0" webpack-sources "^1.1.0"
mini-svg-data-uri@^1.4.3: mini-svg-data-uri@^1.4.4:
version "1.4.3" version "1.4.4"
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.3.tgz#43177b2e93766ba338931a3e2a84a3dfd3a222b8" resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
integrity sha512-gSfqpMRC8IxghvMcxzzmMnWpXAChSA+vy4cia33RgerMS8Fex95akUyQZPbxJJmeBGiGmK7n/1OpUX8ksRjIdA== integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1" version "1.0.1"
@ -8737,9 +8786,9 @@ mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.1:
minimist "^1.2.5" minimist "^1.2.5"
moment@^2.29.1: moment@^2.29.1:
version "2.29.1" version "2.29.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
@ -8919,6 +8968,11 @@ node-fetch@^2.6.1, node-fetch@^2.6.6, node-fetch@^2.6.7:
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"
node-gyp-build-optional-packages@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-4.3.2.tgz#82de9bdf9b1ad042457533afb2f67469dc2264bb"
integrity sha512-P5Ep3ISdmwcCkZIaBaQamQtWAG0facC89phWZgi5Z3hBU//J6S48OIvyZWSPPf6yQMklLZiqoosWAZUj7N+esA==
node-gyp-build@^4.2.3, node-gyp-build@^4.3.0: node-gyp-build@^4.2.3, node-gyp-build@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
@ -9051,10 +9105,10 @@ object-copy@^0.1.0:
define-property "^0.2.5" define-property "^0.2.5"
kind-of "^3.0.3" kind-of "^3.0.3"
object-hash@^2.2.0: object-hash@^3.0.0:
version "2.2.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
object-inspect@^1.11.0, object-inspect@^1.9.0: object-inspect@^1.11.0, object-inspect@^1.9.0:
version "1.12.0" version "1.12.0"
@ -9509,7 +9563,7 @@ picocolors@^1.0.0:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@ -9639,12 +9693,12 @@ postcss-js@^4.0.0:
dependencies: dependencies:
camelcase-css "^2.0.1" camelcase-css "^2.0.1"
postcss-load-config@^3.1.0: postcss-load-config@^3.1.4:
version "3.1.3" version "3.1.4"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.3.tgz#21935b2c43b9a86e6581a576ca7ee1bde2bd1d23" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
integrity sha512-5EYgaM9auHGtO//ljHH+v/aC/TQ5LHXtL7bQajNAUBKUVKiYE8rYpFms7+V26D9FncaGe2zwCoPQsFKb5zF/Hw== integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
dependencies: dependencies:
lilconfig "^2.0.4" lilconfig "^2.0.5"
yaml "^1.10.2" yaml "^1.10.2"
postcss-loader@^4.3.0: postcss-loader@^4.3.0:
@ -9838,10 +9892,10 @@ postcss-reduce-transforms@^5.1.0:
dependencies: dependencies:
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9:
version "6.0.9" version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
dependencies: dependencies:
cssesc "^3.0.0" cssesc "^3.0.0"
util-deprecate "^1.0.2" util-deprecate "^1.0.2"
@ -9866,7 +9920,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.4.12, postcss@^8.2.15, postcss@^8.2.9, postcss@^8.3.11, postcss@^8.4.6: postcss@8.4.12, postcss@^8.2.15, postcss@^8.2.9, postcss@^8.3.11, postcss@^8.4.12:
version "8.4.12" version "8.4.12"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905"
integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==
@ -9911,10 +9965,10 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@2.6.1: prettier@2.6.2:
version "2.6.1" version "2.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A== integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
pretty-bytes@^5.4.1, pretty-bytes@^5.6.0: pretty-bytes@^5.4.1, pretty-bytes@^5.6.0:
version "5.6.0" version "5.6.0"
@ -9929,7 +9983,7 @@ pretty-error@^2.1.2:
lodash "^4.17.20" lodash "^4.17.20"
renderkid "^2.0.4" renderkid "^2.0.4"
probe-image-size@^7.0.0: probe-image-size@^7.0.0, probe-image-size@^7.2.3:
version "7.2.3" version "7.2.3"
resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.3.tgz#d49c64be540ec8edea538f6f585f65a9b3ab4309" resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.3.tgz#d49c64be540ec8edea538f6f585f65a9b3ab4309"
integrity sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w== integrity sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==
@ -10209,10 +10263,10 @@ react-dom@17.0.2:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.2" scheduler "^0.20.2"
react-dropzone@12.0.4: react-dropzone@12.0.5:
version "12.0.4" version "12.0.5"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-12.0.4.tgz#b88eeaa2c7118f7fd042404682b17a1d466f2fcf" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-12.0.5.tgz#f9b557484a8afd6267f670f96a770ddd3948838b"
integrity sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang== integrity sha512-zUjZigD0VJ91CSm9T1h7ErxFReBLaa9sjS2dUL0+inb0RROZpSJTNDHPY1rrBES5V2NXhF8v0kghmaHc81BMFg==
dependencies: dependencies:
attr-accept "^2.2.2" attr-accept "^2.2.2"
file-selector "^0.4.0" file-selector "^0.4.0"
@ -10857,10 +10911,10 @@ shallow-compare@^1.2.2:
resolved "https://registry.yarnpkg.com/shallow-compare/-/shallow-compare-1.2.2.tgz#fa4794627bf455a47c4f56881d8a6132d581ffdb" resolved "https://registry.yarnpkg.com/shallow-compare/-/shallow-compare-1.2.2.tgz#fa4794627bf455a47c4f56881d8a6132d581ffdb"
integrity sha512-LUMFi+RppPlrHzbqmFnINTrazo0lPNwhcgzuAXVVcfy/mqPDrQmHAyz5bvV0gDAuRFrk804V0HpQ6u9sZ0tBeg== integrity sha512-LUMFi+RppPlrHzbqmFnINTrazo0lPNwhcgzuAXVVcfy/mqPDrQmHAyz5bvV0gDAuRFrk804V0HpQ6u9sZ0tBeg==
sharp@^0.30.1: sharp@^0.30.1, sharp@^0.30.3:
version "0.30.2" version "0.30.3"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.2.tgz#95b309b2740424702dc19b62a62595dd34a458b1" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.3.tgz#315a1817423a4d1cde5119a21c99c234a7a6fb37"
integrity sha512-mrMeKI5ECTdYhslPlA2TbBtU3nZXMEBcQwI6qYXjPlu1LpW4HBZLFm6xshMI1HpIdEEJ3UcYp5AKifLT/fEHZQ== integrity sha512-rjpfJFK58ZOFSG8sxYSo3/JQb4ej095HjXp9X7gVu7gEn1aqSG8TCW29h/Rr31+PXrFADo1H/vKfw0uhMQWFtg==
dependencies: dependencies:
color "^4.2.1" color "^4.2.1"
detect-libc "^2.0.1" detect-libc "^2.0.1"
@ -11612,29 +11666,29 @@ table@^6.0.9:
string-width "^4.2.3" string-width "^4.2.3"
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
tailwindcss@3.0.23: tailwindcss@3.0.24:
version "3.0.23" version "3.0.24"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.23.tgz#c620521d53a289650872a66adfcb4129d2200d10" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.24.tgz#22e31e801a44a78a1d9a81ecc52e13b69d85704d"
integrity sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA== integrity sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==
dependencies: dependencies:
arg "^5.0.1" arg "^5.0.1"
chalk "^4.1.2"
chokidar "^3.5.3" chokidar "^3.5.3"
color-name "^1.1.4" color-name "^1.1.4"
cosmiconfig "^7.0.1"
detective "^5.2.0" detective "^5.2.0"
didyoumean "^1.2.2" didyoumean "^1.2.2"
dlv "^1.1.3" dlv "^1.1.3"
fast-glob "^3.2.11" fast-glob "^3.2.11"
glob-parent "^6.0.2" glob-parent "^6.0.2"
is-glob "^4.0.3" is-glob "^4.0.3"
lilconfig "^2.0.5"
normalize-path "^3.0.0" normalize-path "^3.0.0"
object-hash "^2.2.0" object-hash "^3.0.0"
postcss "^8.4.6" picocolors "^1.0.0"
postcss "^8.4.12"
postcss-js "^4.0.0" postcss-js "^4.0.0"
postcss-load-config "^3.1.0" postcss-load-config "^3.1.4"
postcss-nested "5.0.6" postcss-nested "5.0.6"
postcss-selector-parser "^6.0.9" postcss-selector-parser "^6.0.10"
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
quick-lru "^5.1.1" quick-lru "^5.1.1"
resolve "^1.22.0" resolve "^1.22.0"

View File

@ -2,16 +2,22 @@
set -e # exit on first error set -e # exit on first error
while getopts d:t: flag while getopts d:t:r: flag
do do
case "${flag}" in case "${flag}" in
d) delay=${OPTARG};; d) delay=${OPTARG};;
t) timeout=${OPTARG};; t) timeout=${OPTARG};;
r) reason=${OPTARG};;
esac esac
done done
delay=${delay:-0} # default to no delay delay=${delay:-0} # default to no delay
timeout=${timeout:-300} # default timeout is 300s timeout=${timeout:-300} # default timeout is 300s
if [[ -z $reason ]]; then
echo "Please provide a reason for disabling the portal (use '-r <reason>')."
exit 1
fi
countdown() { countdown() {
local secs=$1 local secs=$1
while [ $secs -gt 0 ]; do while [ $secs -gt 0 ]; do
@ -24,8 +30,8 @@ countdown() {
# delay disabling the portal # delay disabling the portal
countdown $delay countdown $delay
# stop healh-check so the server is taken our of load balancer # stop health-check so the server is taken our of load balancer
docker exec health-check cli/disable docker exec health-check cli disable $reason
# then wait 5 minutes for the load balancer to propagate the dns records # then wait 5 minutes for the load balancer to propagate the dns records
countdown $timeout countdown $timeout

View File

@ -3,4 +3,4 @@
set -e # exit on first error set -e # exit on first error
# start the health-checks service # start the health-checks service
docker exec health-check cli/enable docker exec health-check cli enable