Merge branch 'master' into improve-subscription-only-portal-communication
This commit is contained in:
commit
5ede788667
|
@ -64,8 +64,6 @@ services:
|
|||
logging: *default-logging
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-true}
|
||||
volumes:
|
||||
- ./docker/data/nginx/cache:/data/nginx/cache
|
||||
- ./docker/data/nginx/blocker:/data/nginx/blocker
|
||||
|
|
|
@ -2,4 +2,4 @@ more_set_headers 'Access-Control-Allow-Origin: $http_origin';
|
|||
more_set_headers 'Access-Control-Allow-Credentials: true';
|
||||
more_set_headers 'Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE';
|
||||
more_set_headers 'Access-Control-Allow-Headers: DNT,User-Agent,X-Requested-With,If-Modified-Since,If-None-Match,Cache-Control,Content-Type,Range,X-HTTP-Method-Override,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location,Skynet-API-Key';
|
||||
more_set_headers 'Access-Control-Expose-Headers: Content-Length,Content-Range,ETag,Skynet-File-Metadata,Skynet-Skylink,Skynet-Proof,Skynet-Portal-Api,Skynet-Server-Api,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location';
|
||||
more_set_headers 'Access-Control-Expose-Headers: Content-Length,Content-Range,ETag,Accept-Ranges,Skynet-File-Metadata,Skynet-Skylink,Skynet-Proof,Skynet-Portal-Api,Skynet-Server-Api,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location';
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
include /etc/nginx/conf.d/include/proxy-buffer;
|
||||
include /etc/nginx/conf.d/include/proxy-pass-internal;
|
||||
include /etc/nginx/conf.d/include/portal-access-check;
|
||||
|
||||
# variable definititions - we need to define a variable to be able to access it in lua by ngx.var.something
|
||||
set $skylink ''; # placeholder for the base64 skylink
|
||||
set $skylink_base32 ''; # placeholder for the base32 skylink
|
||||
set $skylink ''; # placeholder for the raw 46 bit skylink
|
||||
|
||||
# resolve handshake domain by requesting to /hnsres endpoint and assign correct values to $skylink and $rest
|
||||
rewrite_by_lua_block {
|
||||
|
@ -75,16 +73,10 @@ rewrite_by_lua_block {
|
|||
if ngx.var.path == "/" and skylink_rest ~= nil and skylink_rest ~= "" and skylink_rest ~= "/" then
|
||||
ngx.var.path = skylink_rest
|
||||
end
|
||||
|
||||
-- assign base32 skylink to be used in proxy_pass
|
||||
ngx.var.skylink_base32 = require("skynet.skylink").base32(ngx.var.skylink)
|
||||
}
|
||||
|
||||
# host header has to be adjusted to properly match server name
|
||||
proxy_set_header Host $skylink_base32.$skynet_portal_domain;
|
||||
|
||||
# pass the skylink request to subdomain skylink server
|
||||
proxy_pass $scheme://$server_addr$path$is_args$args;
|
||||
# we proxy to another nginx location rather than directly to siad because we do not want to deal with caching here
|
||||
proxy_pass https://127.0.0.1/$skylink$path$is_args$args;
|
||||
|
||||
# in case siad returns location header, we need to replace the skylink with the domain name
|
||||
header_filter_by_lua_block {
|
||||
|
|
|
@ -1,70 +1,16 @@
|
|||
include /etc/nginx/conf.d/include/cors;
|
||||
include /etc/nginx/conf.d/include/proxy-buffer;
|
||||
include /etc/nginx/conf.d/include/proxy-cache-downloads;
|
||||
include /etc/nginx/conf.d/include/track-download;
|
||||
|
||||
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
||||
|
||||
# ensure that skylink that we pass around is base64 encoded (transform base32 encoded ones)
|
||||
# this is important because we want only one format in cache keys and logs
|
||||
set_by_lua_block $skylink { return require("skynet.skylink").base64(ngx.var.skylink) }
|
||||
|
||||
# $skylink_v1 and $skylink_v2 variables default to the same value but in case the requested skylink was:
|
||||
# a) skylink v1 - it would not matter, no additional logic is executed
|
||||
# b) skylink v2 - in a lua block below we will resolve the skylink v2 into skylink v1 and update
|
||||
# $skylink_v1 variable so then the proxy request to skyd can be cached in nginx (proxy_cache_key
|
||||
# in proxy-cache-downloads includes $skylink_v1 as a part of the cache key)
|
||||
set $skylink_v1 $skylink;
|
||||
set $skylink_v2 $skylink;
|
||||
|
||||
# variable for Skynet-Proof header that we need to inject
|
||||
# into a response if the request was for skylink v2
|
||||
set $skynet_proof '';
|
||||
set_by_lua_block $skylink { return require("skynet.skylink").parse(ngx.var.skylink) }
|
||||
|
||||
# default download rate to unlimited
|
||||
set $limit_rate 0;
|
||||
|
||||
access_by_lua_block {
|
||||
-- the block below only makes sense if we are using nginx cache
|
||||
if not ngx.var.skyd_disk_cache_enabled then
|
||||
local httpc = require("resty.http").new()
|
||||
|
||||
-- detect whether requested skylink is v2
|
||||
local isBase32v2 = string.len(ngx.var.skylink) == 55 and string.sub(ngx.var.skylink, 0, 2) == "04"
|
||||
local isBase64v2 = string.len(ngx.var.skylink) == 46 and string.sub(ngx.var.skylink, 0, 2) == "AQ"
|
||||
|
||||
if isBase32v2 or isBase64v2 then
|
||||
-- 10.10.10.10 points to sia service (alias not available when using resty-http)
|
||||
local res, err = httpc:request_uri("http://10.10.10.10:9980/skynet/resolve/" .. ngx.var.skylink_v2, {
|
||||
headers = { ["User-Agent"] = "Sia-Agent" }
|
||||
})
|
||||
|
||||
-- print error and exit with 500 or exit with response if status is not 200
|
||||
if err or (res and res.status ~= ngx.HTTP_OK) then
|
||||
ngx.status = (err and ngx.HTTP_INTERNAL_SERVER_ERROR) or res.status
|
||||
ngx.header["content-type"] = "text/plain"
|
||||
ngx.say(err or res.body)
|
||||
return ngx.exit(ngx.status)
|
||||
end
|
||||
|
||||
local json = require('cjson')
|
||||
local resolve = json.decode(res.body)
|
||||
ngx.var.skylink_v1 = resolve.skylink
|
||||
ngx.var.skynet_proof = res.headers["Skynet-Proof"]
|
||||
end
|
||||
|
||||
-- check if skylink v1 is present on blocklist (compare hashes)
|
||||
if require("skynet.blocklist").is_blocked(ngx.var.skylink_v1) then
|
||||
return require("skynet.blocklist").exit_illegal()
|
||||
end
|
||||
|
||||
-- if skylink is found on nocache list then set internal nocache variable
|
||||
-- to tell nginx that it should not try and cache this file (too large)
|
||||
if ngx.shared.nocache:get(ngx.var.skylink_v1) then
|
||||
ngx.var.nocache = "1"
|
||||
end
|
||||
end
|
||||
|
||||
if require("skynet.account").accounts_enabled() then
|
||||
-- check if portal is in authenticated only mode
|
||||
if require("skynet.account").is_access_unauthorized() then
|
||||
|
@ -84,36 +30,10 @@ access_by_lua_block {
|
|||
end
|
||||
}
|
||||
|
||||
header_filter_by_lua_block {
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
||||
|
||||
-- the block below only makes sense if we are using nginx cache
|
||||
if not ngx.var.skyd_disk_cache_enabled then
|
||||
-- not empty skynet_proof means this is a skylink v2 request
|
||||
-- so we should replace the Skynet-Proof header with the one
|
||||
-- we got from /skynet/resolve/ endpoint, otherwise we would
|
||||
-- be serving cached empty v1 skylink Skynet-Proof header
|
||||
if ngx.var.skynet_proof and ngx.var.skynet_proof ~= "" then
|
||||
ngx.header["Skynet-Proof"] = ngx.var.skynet_proof
|
||||
end
|
||||
|
||||
-- add skylink to nocache list if it exceeds 1GB (1e+9 bytes) threshold
|
||||
-- (content length can be nil for already cached files - we can ignore them)
|
||||
if ngx.header["Content-Length"] and tonumber(ngx.header["Content-Length"]) > 1e+9 then
|
||||
ngx.shared.nocache:set(ngx.var.skylink_v1, ngx.header["Content-Length"])
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
limit_rate_after 512k;
|
||||
limit_rate $limit_rate;
|
||||
|
||||
proxy_read_timeout 600;
|
||||
proxy_set_header User-Agent: Sia-Agent;
|
||||
|
||||
# in case the requested skylink was v2 and we already resolved it to skylink v1, we are going to pass resolved
|
||||
# skylink v1 to skyd to save that extra skylink v2 lookup in skyd but in turn, in case skyd returns a redirect
|
||||
# we need to rewrite the skylink v1 to skylink v2 in the location header with proxy_redirect
|
||||
proxy_redirect $skylink_v1 $skylink_v2;
|
||||
proxy_pass http://sia:9980/skynet/skylink/$skylink_v1$path$is_args$args;
|
||||
proxy_pass http://sia:9980/skynet/skylink/$skylink$path$is_args$args;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
# if you are expecting large headers (ie. Skynet-Skyfile-Metadata), tune these values to your needs
|
||||
# read more: https://www.getpagespeed.com/server-setup/nginx/tuning-proxy_buffer_size-in-nginx
|
||||
proxy_buffer_size 4096k;
|
||||
proxy_buffers 64 256k;
|
||||
proxy_busy_buffers_size 4096k; # at least as high as proxy_buffer_size
|
|
@ -1,21 +0,0 @@
|
|||
proxy_cache skynet; # cache name
|
||||
proxy_cache_key $skylink_v1$path$arg_format$arg_attachment$arg_start$arg_end$http_range; # unique cache key
|
||||
proxy_cache_min_uses 3; # cache after 3 uses
|
||||
proxy_cache_valid 200 206 307 308 48h; # keep 200, 206, 307 and 308 responses valid for up to 2 days
|
||||
add_header X-Proxy-Cache $upstream_cache_status; # add response header to indicate cache hits and misses
|
||||
|
||||
# map skyd env variable value to "1" for true and "0" for false (expected by proxy_no_cache)
|
||||
set_by_lua_block $skyd_disk_cache_enabled {
|
||||
return os.getenv("SKYD_DISK_CACHE_ENABLED") == "true" and "1" or "0"
|
||||
}
|
||||
|
||||
# bypass - this will bypass cache hit on request (status BYPASS)
|
||||
# but still stores file in cache if cache conditions are met
|
||||
proxy_cache_bypass $cookie_nocache $arg_nocache $skyd_disk_cache_enabled;
|
||||
|
||||
# no cache - this will ignore cache on request (status MISS)
|
||||
# and does not store file in cache under no condition
|
||||
set_if_empty $nocache "0";
|
||||
|
||||
# disable cache when nocache is set or skyd cache is enabled
|
||||
proxy_no_cache $nocache $skyd_disk_cache_enabled;
|
|
@ -1,9 +0,0 @@
|
|||
server {
|
||||
# local server - do not expose this port externally
|
||||
listen 8000;
|
||||
|
||||
# secure traffic by limiting to only local networks
|
||||
include /etc/nginx/conf.d/include/local-network-only;
|
||||
|
||||
include /etc/nginx/conf.d/server/server.local;
|
||||
}
|
|
@ -47,7 +47,9 @@ location /skynet/portal/blocklist {
|
|||
|
||||
proxy_cache skynet;
|
||||
proxy_cache_valid 200 204 15m; # cache portal blocklist for 15 minutes
|
||||
proxy_pass http://blocker:4000/blocklist;
|
||||
|
||||
# 10.10.10.110 points to blocker service
|
||||
proxy_pass http://10.10.10.110:4000/blocklist;
|
||||
}
|
||||
|
||||
location /skynet/portals {
|
||||
|
@ -123,14 +125,15 @@ location /abuse/report {
|
|||
location /hns {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
rewrite_by_lua_block {
|
||||
local hns_domain = string.match(ngx.var.uri, "/hns/([^/?]+)")
|
||||
local path = string.match(ngx.var.uri, "/hns/[^/?]+(.*)")
|
||||
local args = ngx.var.args and ngx.var.is_args .. ngx.var.args or ""
|
||||
local hns_subdomain_url = ngx.var.scheme .. "://" .. hns_domain .. ".hns." .. ngx.var.skynet_portal_domain .. path .. args
|
||||
# match the request_uri and extract the hns domain and anything that is passed in the uri after it
|
||||
# example: /hns/something/foo/bar matches:
|
||||
# > hns_domain: something
|
||||
# > path: /foo/bar/
|
||||
set_by_lua_block $hns_domain { return string.match(ngx.var.uri, "/hns/([^/?]+)") }
|
||||
set_by_lua_block $path { return string.match(ngx.var.uri, "/hns/[^/?]+(.*)") }
|
||||
|
||||
return ngx.redirect(hns_subdomain_url, ngx.HTTP_MOVED_PERMANENTLY)
|
||||
}
|
||||
proxy_set_header Host $host;
|
||||
include /etc/nginx/conf.d/include/location-hns;
|
||||
}
|
||||
|
||||
location /hnsres {
|
||||
|
@ -176,13 +179,13 @@ location /skynet/registry/subscription {
|
|||
local httpc = require("resty.http").new()
|
||||
|
||||
-- fetch account limits and set download bandwidth and registry delays accordingly
|
||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits", {
|
||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits?unit=byte", {
|
||||
headers = { ["Cookie"] = "skynet-jwt=" .. ngx.var.skynet_jwt }
|
||||
})
|
||||
|
||||
-- fail gracefully in case /user/limits failed
|
||||
if err or (res and res.status ~= ngx.HTTP_OK) then
|
||||
ngx.log(ngx.ERR, "Failed accounts service request /user/limits: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
||||
ngx.log(ngx.ERR, "Failed accounts service request /user/limits?unit=byte: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
||||
elseif res and res.status == ngx.HTTP_OK then
|
||||
local json = require('cjson')
|
||||
local limits = json.decode(res.body)
|
||||
|
@ -337,14 +340,7 @@ location ~ "^/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
|
|||
set $skylink $2;
|
||||
set $path $3;
|
||||
|
||||
rewrite_by_lua_block {
|
||||
local skynet_skylink = require("skynet.skylink")
|
||||
local base32_skylink = skynet_skylink.base32(ngx.var.skylink)
|
||||
local args = ngx.var.args and ngx.var.is_args .. ngx.var.args or ""
|
||||
local base32_url = ngx.var.scheme .. "://" .. base32_skylink .. "." .. ngx.var.skynet_portal_domain .. ngx.var.path .. args
|
||||
|
||||
return ngx.redirect(base32_url, ngx.HTTP_MOVED_PERMANENTLY)
|
||||
}
|
||||
include /etc/nginx/conf.d/include/location-skylink;
|
||||
}
|
||||
|
||||
location ~ "^/file/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
|
||||
|
@ -358,7 +354,6 @@ location ~ "^/file/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
|
|||
|
||||
location /skynet/trustless/basesector {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
include /etc/nginx/conf.d/include/proxy-buffer;
|
||||
include /etc/nginx/conf.d/include/track-download;
|
||||
|
||||
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
||||
|
|
|
@ -37,9 +37,7 @@ location / {
|
|||
ngx.var.skylink = cache_value
|
||||
end
|
||||
|
||||
ngx.var.skylink = require("skynet.skylink").base64(ngx.var.skylink)
|
||||
ngx.var.skylink_v1 = ngx.var.skylink
|
||||
ngx.var.skylink_v2 = ngx.var.skylink
|
||||
ngx.var.skylink = require("skynet.skylink").parse(ngx.var.skylink)
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/include/location-skylink;
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
include /etc/nginx/conf.d/include/init-optional-variables;
|
||||
|
||||
location /skynet/blocklist {
|
||||
client_max_body_size 10m; # increase max body size to account for large lists
|
||||
client_body_buffer_size 10m; # force whole body to memory so we can read it
|
||||
|
||||
content_by_lua_block {
|
||||
local httpc = require("resty.http").new()
|
||||
|
||||
ngx.req.read_body() -- ensure the post body data is read before using get_body_data
|
||||
|
||||
-- proxy blocklist update request
|
||||
-- 10.10.10.10 points to sia service (alias not available when using resty-http)
|
||||
local res, err = httpc:request_uri("http://10.10.10.10:9980/skynet/blocklist", {
|
||||
method = "POST",
|
||||
body = ngx.req.get_body_data(),
|
||||
headers = {
|
||||
["Content-Type"] = "application/x-www-form-urlencoded",
|
||||
["Authorization"] = require("skynet.utils").authorization_header(),
|
||||
["User-Agent"] = "Sia-Agent",
|
||||
}
|
||||
})
|
||||
|
||||
-- print error and exit with 500 or exit with response if status is not 204
|
||||
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
|
||||
ngx.status = (err and ngx.HTTP_INTERNAL_SERVER_ERROR) or res.status
|
||||
ngx.header["content-type"] = "text/plain"
|
||||
ngx.say(err or res.body)
|
||||
return ngx.exit(ngx.status)
|
||||
end
|
||||
|
||||
require("skynet.blocklist").reload()
|
||||
|
||||
ngx.status = ngx.HTTP_NO_CONTENT
|
||||
return ngx.exit(ngx.status)
|
||||
}
|
||||
}
|
|
@ -45,13 +45,13 @@ function _M.get_account_limits()
|
|||
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/user/limits", {
|
||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits?unit=byte", {
|
||||
headers = { ["Cookie"] = "skynet-jwt=" .. ngx.var.skynet_jwt }
|
||||
})
|
||||
|
||||
-- fail gracefully in case /user/limits failed
|
||||
if err or (res and res.status ~= ngx.HTTP_OK) then
|
||||
ngx.log(ngx.ERR, "Failed accounts service request /user/limits: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
||||
ngx.log(ngx.ERR, "Failed accounts service request /user/limits?unit=byte: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
||||
ngx.var.account_limits = cjson.encode(anon_limits)
|
||||
elseif res and res.status == ngx.HTTP_OK then
|
||||
ngx.var.account_limits = res.body
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
local _M = {}
|
||||
|
||||
function _M.reload()
|
||||
local httpc = require("resty.http").new()
|
||||
|
||||
-- fetch blocklist records (all blocked skylink hashes)
|
||||
-- 10.10.10.10 points to sia service (alias not available when using resty-http)
|
||||
local res, err = httpc:request_uri("http://10.10.10.10:9980/skynet/blocklist", {
|
||||
headers = {
|
||||
["User-Agent"] = "Sia-Agent",
|
||||
}
|
||||
})
|
||||
|
||||
-- fail whole request in case this request failed, we want to make sure
|
||||
-- the blocklist is pre cached before serving first skylink
|
||||
if err or (res and res.status ~= ngx.HTTP_OK) then
|
||||
ngx.log(ngx.ERR, "Failed skyd service request /skynet/blocklist: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
|
||||
ngx.status = (err and ngx.HTTP_INTERNAL_SERVER_ERROR) or res.status
|
||||
ngx.header["content-type"] = "text/plain"
|
||||
ngx.say(err or res.body)
|
||||
return ngx.exit(ngx.status)
|
||||
elseif res and res.status == ngx.HTTP_OK then
|
||||
local json = require('cjson')
|
||||
local data = json.decode(res.body)
|
||||
|
||||
-- mark all existing entries as expired
|
||||
ngx.shared.blocklist:flush_all()
|
||||
|
||||
-- check if blocklist is table (it is null when empty)
|
||||
if type(data.blocklist) == "table" then
|
||||
-- set all cache entries one by one (resets expiration)
|
||||
for i, hash in ipairs(data.blocklist) do
|
||||
ngx.shared.blocklist:set(hash, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- ensure that init flag is persisted
|
||||
ngx.shared.blocklist:set("__init", true)
|
||||
|
||||
-- remove all leftover expired entries
|
||||
ngx.shared.blocklist:flush_expired()
|
||||
end
|
||||
end
|
||||
|
||||
function _M.is_blocked(skylink)
|
||||
-- make sure that blocklist has been preloaded
|
||||
if not ngx.shared.blocklist:get("__init") then _M.reload() end
|
||||
|
||||
-- hash skylink before comparing it with blocklist
|
||||
local hash = require("skynet.skylink").hash(skylink)
|
||||
|
||||
-- we need to use get_stale because we are expiring previous
|
||||
-- entries when the blocklist is reloading and we still want
|
||||
-- to block them until the reloading is finished
|
||||
return ngx.shared.blocklist:get_stale(hash) == true
|
||||
end
|
||||
|
||||
-- exit with 416 illegal content status code
|
||||
function _M.exit_illegal()
|
||||
ngx.status = ngx.HTTP_ILLEGAL
|
||||
ngx.header["content-type"] = "text/plain"
|
||||
ngx.say("Unavailable For Legal Reasons")
|
||||
return ngx.exit(ngx.status)
|
||||
end
|
||||
|
||||
return _M
|
|
@ -3,13 +3,10 @@ local _M = {}
|
|||
local basexx = require("basexx")
|
||||
local hasher = require("hasher")
|
||||
|
||||
-- use lowercase alphabet since our skylinks are part of urls
|
||||
local base32_alphabet = "0123456789abcdefghijklmnopqrstuv"
|
||||
|
||||
-- parse any skylink and return base64 version
|
||||
function _M.base64(skylink)
|
||||
function _M.parse(skylink)
|
||||
if string.len(skylink) == 55 then
|
||||
local decoded = basexx.from_basexx(string.lower(skylink), base32_alphabet, 5)
|
||||
local decoded = basexx.from_basexx(string.upper(skylink), "0123456789ABCDEFGHIJKLMNOPQRSTUV", 5)
|
||||
|
||||
return basexx.to_url64(decoded)
|
||||
end
|
||||
|
@ -17,21 +14,10 @@ function _M.base64(skylink)
|
|||
return skylink
|
||||
end
|
||||
|
||||
-- parse any skylink and return base32 version
|
||||
function _M.base32(skylink)
|
||||
if string.len(skylink) == 46 then
|
||||
local decoded = basexx.from_url64(skylink)
|
||||
|
||||
return basexx.to_basexx(decoded, base32_alphabet, 5)
|
||||
end
|
||||
|
||||
return skylink
|
||||
end
|
||||
|
||||
-- hash skylink into 32 bytes hash used in blocklist
|
||||
function _M.hash(skylink)
|
||||
-- ensure that the skylink is base64 encoded
|
||||
local base64Skylink = _M.base64(skylink)
|
||||
local base64Skylink = _M.parse(skylink)
|
||||
|
||||
-- decode skylink from base64 encoding
|
||||
local rawSkylink = basexx.from_url64(base64Skylink)
|
||||
|
|
|
@ -1,28 +1,15 @@
|
|||
local skynet_skylink = require("skynet.skylink")
|
||||
|
||||
describe("base64", function()
|
||||
describe("parse", function()
|
||||
local base32 = "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30"
|
||||
local base64 = "AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA"
|
||||
|
||||
it("should return unchanged base64 skylink", function()
|
||||
assert.is.same(skynet_skylink.base64(base64), base64)
|
||||
assert.is.same(skynet_skylink.parse(base64), base64)
|
||||
end)
|
||||
|
||||
it("should transform base32 skylink into base64", function()
|
||||
assert.is.same(skynet_skylink.base64(base32), base64)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("base32", function()
|
||||
local base32 = "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30"
|
||||
local base64 = "AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA"
|
||||
|
||||
it("should return unchanged base32 skylink", function()
|
||||
assert.is.same(skynet_skylink.base32(base32), base32)
|
||||
end)
|
||||
|
||||
it("should transform base64 skylink into base32", function()
|
||||
assert.is.same(skynet_skylink.base32(base64), base32)
|
||||
assert.is.same(skynet_skylink.parse(base32), base64)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ env SERVER_DOMAIN;
|
|||
env PORTAL_MODULES;
|
||||
env ACCOUNTS_LIMIT_ACCESS;
|
||||
env SIA_API_PASSWORD;
|
||||
env SKYD_DISK_CACHE_ENABLED;
|
||||
|
||||
events {
|
||||
worker_connections 8192;
|
||||
|
@ -75,20 +74,10 @@ http {
|
|||
# proxy cache definition
|
||||
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=skynet:10m max_size=50g min_free=100g inactive=48h use_temp_path=off;
|
||||
|
||||
# create a shared blocklist dictionary with size of 30 megabytes
|
||||
# estimated capacity of 1 megabyte dictionary is 3500 blocklist entries
|
||||
# that gives us capacity of around 100k entries in 30 megabyte dictionary
|
||||
lua_shared_dict blocklist 30m;
|
||||
|
||||
# create a shared dictionary to fill with skylinks that should not
|
||||
# be cached due to the large size or some other reasons
|
||||
lua_shared_dict nocache 10m;
|
||||
|
||||
# this runs before forking out nginx worker processes
|
||||
init_by_lua_block {
|
||||
require "cjson"
|
||||
require "resty.http"
|
||||
require "skynet.blocklist"
|
||||
require "skynet.skylink"
|
||||
require "skynet.utils"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useUser } from "../../contexts/user";
|
||||
import { SimpleUploadIcon } from "../Icons";
|
||||
|
||||
const AVATAR_PLACEHOLDER = "/images/avatar-placeholder.svg";
|
||||
|
||||
export const AvatarUploader = (props) => {
|
||||
const { user } = useUser();
|
||||
const [imageUrl, setImageUrl] = useState(AVATAR_PLACEHOLDER);
|
||||
|
||||
useEffect(() => {
|
||||
setImageUrl(user.avatarUrl ?? AVATAR_PLACEHOLDER);
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<div
|
||||
className={`flex justify-center items-center xl:w-[245px] xl:h-[245px] bg-contain bg-none xl:bg-[url(/images/avatar-bg.svg)]`}
|
||||
>
|
||||
<img src={imageUrl} className="w-[160px]" alt="" />
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
className="flex items-center gap-4 hover:underline decoration-1 decoration-dashed underline-offset-2 decoration-gray-400"
|
||||
type="button"
|
||||
onClick={console.info.bind(console)}
|
||||
>
|
||||
<SimpleUploadIcon size={20} className="shrink-0" /> Upload profile picture
|
||||
</button>
|
||||
{/* TODO: actual uploading */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from "./AvatarUploader";
|
|
@ -6,7 +6,7 @@ import styled from "styled-components";
|
|||
*/
|
||||
export const Button = styled.button.attrs(({ $primary }) => ({
|
||||
type: "button",
|
||||
className: `px-6 py-3 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 transition-[filter] hover:brightness-90
|
||||
className: `px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 transition-[filter] hover:brightness-90
|
||||
${$primary ? "bg-primary" : "bg-white border-2 border-black"}`,
|
||||
}))``;
|
||||
Button.propTypes = {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
import copy from "copy-text-to-clipboard";
|
||||
import styled from "styled-components";
|
||||
import { useClickAway } from "react-use";
|
||||
|
||||
import { CopyIcon } from "./Icons";
|
||||
|
||||
const Button = styled.button.attrs({
|
||||
className: "relative inline-flex items-center hover:text-primary",
|
||||
})``;
|
||||
|
||||
const TooltipContainer = styled.div.attrs(({ $visible }) => ({
|
||||
className: `absolute left-full top-1/2 z-10
|
||||
bg-white rounded border border-primary/30 shadow-md
|
||||
pointer-events-none transition-opacity duration-150 ease-in-out
|
||||
${$visible ? "opacity-100" : "opacity-0"}`,
|
||||
}))`
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
||||
const TooltipContent = styled.div.attrs({
|
||||
className: "bg-primary-light/10 text-palette-600 py-2 px-4 ",
|
||||
})``;
|
||||
|
||||
export const CopyButton = ({ value, className }) => {
|
||||
const containerRef = useRef();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [timer, setTimer] = useState(null);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
clearTimeout(timer);
|
||||
copy(value);
|
||||
setCopied(true);
|
||||
|
||||
setTimer(setTimeout(() => setCopied(false), 1500));
|
||||
}, [value, timer]);
|
||||
|
||||
useClickAway(containerRef, () => setCopied(false));
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`inline-flex relative overflow-visible pr-2 ${className ?? ""}`}>
|
||||
<Button onClick={handleCopy} className={copied ? "text-primary" : ""}>
|
||||
<CopyIcon size={16} />
|
||||
</Button>
|
||||
<TooltipContainer $visible={copied}>
|
||||
<TooltipContent>Copied to clipboard</TooltipContent>
|
||||
</TooltipContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -2,7 +2,8 @@ import dayjs from "dayjs";
|
|||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
import { useUser } from "../../contexts/user";
|
||||
import useSubscriptionPlans from "../../hooks/useSubscriptionPlans";
|
||||
import useActivePlan from "../../hooks/useActivePlan";
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
|
||||
import LatestPayment from "./LatestPayment";
|
||||
import SuggestedPlan from "./SuggestedPlan";
|
||||
|
@ -11,13 +12,10 @@ dayjs.extend(relativeTime);
|
|||
|
||||
const CurrentPlan = () => {
|
||||
const { user, error: userError } = useUser();
|
||||
const { activePlan, plans, error: plansError } = useSubscriptionPlans(user);
|
||||
const { plans, activePlan, error: plansError } = useActivePlan(user);
|
||||
|
||||
if (!user || !activePlan) {
|
||||
return (
|
||||
// TODO: a nicer loading indicator
|
||||
<div className="flex flex-col space-y-4 h-full justify-center items-center">Loading...</div>
|
||||
);
|
||||
return <ContainerLoadingIndicator />;
|
||||
}
|
||||
|
||||
if (userError || plansError) {
|
||||
|
|
|
@ -1,24 +1,50 @@
|
|||
import * as React from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import fileSize from "pretty-bytes";
|
||||
import { Link } from "gatsby";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useUser } from "../../contexts/user";
|
||||
import useActivePlan from "../../hooks/useActivePlan";
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
|
||||
import { GraphBar } from "./GraphBar";
|
||||
import { UsageGraph } from "./UsageGraph";
|
||||
|
||||
// TODO: get real data
|
||||
const useUsageData = () => ({
|
||||
files: {
|
||||
used: 19_521,
|
||||
limit: 20_000,
|
||||
},
|
||||
storage: {
|
||||
used: 23_000_000_000,
|
||||
limit: 1_000_000_000_000,
|
||||
},
|
||||
});
|
||||
const useUsageData = () => {
|
||||
const { user } = useUser();
|
||||
const { activePlan, error } = useActivePlan(user);
|
||||
const { data: stats, error: statsError } = useSWR("user/stats");
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [usage, setUsage] = useState({});
|
||||
|
||||
const hasError = error || statsError;
|
||||
const hasData = activePlan && stats;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasData || hasError) {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (hasData && !hasError) {
|
||||
setUsage({
|
||||
filesUsed: stats?.numUploads,
|
||||
filesLimit: activePlan?.limits?.maxNumberUploads,
|
||||
storageUsed: stats?.totalUploadsSize,
|
||||
storageLimit: activePlan?.limits?.storageLimit,
|
||||
});
|
||||
}
|
||||
}, [hasData, hasError, stats, activePlan]);
|
||||
|
||||
return {
|
||||
error: error || statsError,
|
||||
loading,
|
||||
usage,
|
||||
};
|
||||
};
|
||||
|
||||
const size = (bytes) => {
|
||||
const text = fileSize(bytes, { maximumFractionDigits: 1 });
|
||||
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 });
|
||||
const [value, unit] = text.split(" ");
|
||||
|
||||
return {
|
||||
|
@ -28,12 +54,26 @@ const size = (bytes) => {
|
|||
};
|
||||
};
|
||||
|
||||
export default function CurrentUsage() {
|
||||
const { files, storage } = useUsageData();
|
||||
const ErrorMessage = () => (
|
||||
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
|
||||
<p>We were not able to fetch the current usage data.</p>
|
||||
<p>We'll try again automatically.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const storageUsage = size(storage.used);
|
||||
const storageLimit = size(storage.limit);
|
||||
const filesUsedLabel = React.useMemo(() => ({ value: files.used, unit: "files" }), [files.used]);
|
||||
export default function CurrentUsage() {
|
||||
const { usage, error, loading } = useUsageData();
|
||||
const storageUsage = size(usage.storageUsed);
|
||||
const storageLimit = size(usage.storageLimit);
|
||||
const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]);
|
||||
|
||||
if (loading) {
|
||||
return <ContainerLoadingIndicator />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -41,7 +81,7 @@ export default function CurrentUsage() {
|
|||
{storageUsage.text} of {storageLimit.text}
|
||||
</h4>
|
||||
<p className="text-palette-400">
|
||||
{files.used} of {files.limit} files
|
||||
{usage.filesUsed} of {usage.filesLimit} files
|
||||
</p>
|
||||
<div className="relative mt-7 font-sans uppercase text-xs">
|
||||
<div className="flex place-content-between">
|
||||
|
@ -49,8 +89,8 @@ export default function CurrentUsage() {
|
|||
<span>{storageLimit.text}</span>
|
||||
</div>
|
||||
<UsageGraph>
|
||||
<GraphBar value={storage.used} limit={storage.limit} label={storageUsage} />
|
||||
<GraphBar value={files.used} limit={files.limit} label={filesUsedLabel} />
|
||||
<GraphBar value={usage.storageUsed} limit={usage.storageLimit} label={storageUsage} />
|
||||
<GraphBar value={usage.filesUsed} limit={usage.filesLimit} label={filesUsedLabel} />
|
||||
</UsageGraph>
|
||||
<div className="flex place-content-between">
|
||||
<span>Files</span>
|
||||
|
@ -62,7 +102,7 @@ export default function CurrentUsage() {
|
|||
UPGRADE
|
||||
</Link>{" "}
|
||||
{/* TODO: proper URL */}
|
||||
<span>{files.limit}</span>
|
||||
<span>{usage.filesLimit}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@ const TriggerIcon = styled(ChevronDownIcon).attrs({
|
|||
`;
|
||||
|
||||
const Flyout = styled.div.attrs(({ open }) => ({
|
||||
className: `absolute top-full right-0 p-0
|
||||
className: `absolute top-full right-0 p-0 z-10
|
||||
border rounded border-palette-100
|
||||
bg-white shadow-md shadow-palette-200/50
|
||||
${open ? "visible" : "invisible"}`,
|
||||
|
|
|
@ -3,18 +3,16 @@ import PropTypes from "prop-types";
|
|||
|
||||
const DropdownLink = styled.a.attrs({
|
||||
className: `m-0 border-t border-palette-200/50 h-[60px]
|
||||
whitespace-nowrap transition-colors text-current
|
||||
whitespace-nowrap transition-colors
|
||||
hover:bg-palette-100/50 flex items-center
|
||||
pr-8 pl-6 py-4 gap-4 first:border-0`,
|
||||
})``;
|
||||
|
||||
export const DropdownMenuLink = ({ active, icon: Icon, label, ...props }) => (
|
||||
<>
|
||||
<DropdownLink {...props}>
|
||||
{Icon ? <Icon className={active ? "text-primary" : "text-current"} /> : null}
|
||||
{label}
|
||||
{Icon ? <Icon /> : null}
|
||||
<span className="text-palette-500">{label}</span>
|
||||
</DropdownLink>
|
||||
</>
|
||||
);
|
||||
|
||||
DropdownMenuLink.propTypes = {
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import * as React from "react";
|
||||
import useSWR from "swr";
|
||||
import { useMedia } from "react-use";
|
||||
|
||||
import theme from "../../lib/theme";
|
||||
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
import { Select, SelectOption } from "../Select";
|
||||
import { Switch } from "../Switch";
|
||||
import { TextInputIcon } from "../TextInputIcon/TextInputIcon";
|
||||
import { SearchIcon } from "../Icons";
|
||||
|
||||
import FileTable from "./FileTable";
|
||||
import useFormattedFilesData from "./useFormattedFilesData";
|
||||
|
||||
const FileList = ({ type }) => {
|
||||
const isMediumScreenOrLarger = useMedia(`(min-width: ${theme.screens.md})`);
|
||||
const { data, error } = useSWR(`user/${type}?pageSize=10`);
|
||||
const items = useFormattedFilesData(data?.items || []);
|
||||
|
||||
const setFilter = (name, value) => console.log("filter", name, "set to", value);
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
||||
{/* TODO: proper error message */}
|
||||
{!data && !error && <ContainerLoadingIndicator />}
|
||||
{!data && error && <p>An error occurred while loading this data.</p>}
|
||||
{data && <p>No {type} found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 pt-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row justify-between items-center">
|
||||
<TextInputIcon
|
||||
className="w-full lg:w-[280px] xl:w-[420px]"
|
||||
placeholder="Search"
|
||||
icon={<SearchIcon size={20} />}
|
||||
onChange={console.log.bind(console)}
|
||||
/>
|
||||
<div className="flex flex-row items-center uppercase font-light text-sm gap-4">
|
||||
<Switch onChange={(value) => setFilter("showSmallFiles", value)} className="mr-8">
|
||||
<span className="underline decoration-dashed underline-offset-2 decoration-2 decoration-gray-300">
|
||||
Show small files
|
||||
</span>
|
||||
</Switch>
|
||||
<div>
|
||||
<span className="pr-2">File type:</span>
|
||||
<Select onChange={(value) => setFilter("type", value)}>
|
||||
<SelectOption value="all" label="All" />
|
||||
<SelectOption value="mp4" label=".mp4" />
|
||||
<SelectOption value="pdf" label=".pdf" />
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<span className="pr-2">Sort:</span>
|
||||
<Select onChange={(value) => setFilter("type", value)}>
|
||||
<SelectOption value="size-desc" label="Biggest size" />
|
||||
<SelectOption value="size-asc" label="Smallest size" />
|
||||
<SelectOption value="date-desc" label="Latest" />
|
||||
<SelectOption value="date-asc" label="Oldest" />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: mobile view (it's not tabular) */}
|
||||
{isMediumScreenOrLarger ? <FileTable items={items} /> : "Mobile view"}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileList;
|
|
@ -0,0 +1,111 @@
|
|||
import { CogIcon, ShareIcon } from "../Icons";
|
||||
import { PopoverMenu } from "../PopoverMenu/PopoverMenu";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeadCell, TableRow } from "../Table";
|
||||
import { CopyButton } from "../CopyButton";
|
||||
|
||||
const buildShareMenu = (item) => {
|
||||
return [
|
||||
{
|
||||
label: "Facebook",
|
||||
callback: () => {
|
||||
console.info("share to Facebook", item);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Twitter",
|
||||
callback: () => {
|
||||
console.info("share to Twitter", item);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Discord",
|
||||
callback: () => {
|
||||
console.info("share to Discord", item);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildOptionsMenu = (item) => {
|
||||
return [
|
||||
{
|
||||
label: "Preview",
|
||||
callback: () => {
|
||||
console.info("preview", item);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Download",
|
||||
callback: () => {
|
||||
console.info("download", item);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Unpin",
|
||||
callback: () => {
|
||||
console.info("unpin", item);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Report",
|
||||
callback: () => {
|
||||
console.info("report", item);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default function FileTable({ items }) {
|
||||
return (
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<TableHead>
|
||||
<TableRow noHoverEffect>
|
||||
<TableHeadCell className="w-[240px] xl:w-[360px]">Name</TableHeadCell>
|
||||
<TableHeadCell className="w-[80px]">Type</TableHeadCell>
|
||||
<TableHeadCell className="w-[80px]" align="right">
|
||||
Size
|
||||
</TableHeadCell>
|
||||
<TableHeadCell className="w-[180px]">Uploaded</TableHeadCell>
|
||||
<TableHeadCell className="hidden lg:table-cell">Skylink</TableHeadCell>
|
||||
<TableHeadCell className="w-[100px]">Activity</TableHeadCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
const { id, name, type, size, date, skylink } = item;
|
||||
|
||||
return (
|
||||
<TableRow key={id}>
|
||||
<TableCell className="w-[240px] xl:w-[360px]">{name}</TableCell>
|
||||
<TableCell className="w-[80px]">{type}</TableCell>
|
||||
<TableCell className="w-[80px]" align="right">
|
||||
{size}
|
||||
</TableCell>
|
||||
<TableCell className="w-[180px]">{date}</TableCell>
|
||||
<TableCell className="hidden lg:table-cell pr-6 !overflow-visible">
|
||||
<div className="flex items-center">
|
||||
<CopyButton value={skylink} className="mr-2" />
|
||||
<span className="w-full inline-block truncate">{skylink}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-[100px] !overflow-visible">
|
||||
<div className="flex text-palette-600 gap-4">
|
||||
<PopoverMenu options={buildShareMenu(item)} openClassName="text-primary">
|
||||
<button>
|
||||
<ShareIcon size={22} />
|
||||
</button>
|
||||
</PopoverMenu>
|
||||
<PopoverMenu options={buildOptionsMenu(item)} openClassName="text-primary">
|
||||
<button>
|
||||
<CogIcon />
|
||||
</button>
|
||||
</PopoverMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./FileList";
|
|
@ -0,0 +1,26 @@
|
|||
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 useFormattedFilesData = (items) => useMemo(() => items.map(formatItem), [items]);
|
||||
|
||||
export default useFormattedFilesData;
|
|
@ -0,0 +1,10 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const CopyIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M26.35,11.29A5.65,5.65,0,0,1,32,16.94v9.41A5.65,5.65,0,0,1,26.35,32H16.94a5.65,5.65,0,0,1-5.65-5.65V16.94a5.65,5.65,0,0,1,5.65-5.65Zm0,3.77H16.94a1.88,1.88,0,0,0-1.88,1.88v9.41a1.89,1.89,0,0,0,1.88,1.89h9.41a1.89,1.89,0,0,0,1.89-1.89V16.94A1.89,1.89,0,0,0,26.35,15.06ZM16.22,0A4.49,4.49,0,0,1,20.7,4.18V5.79A1.89,1.89,0,0,1,17,6V4.49a.73.73,0,0,0-.58-.71l-.14,0H4.49a.74.74,0,0,0-.71.58l0,.15V16.22a.73.73,0,0,0,.58.71H5.79A1.88,1.88,0,0,1,6,20.69l-.22,0H4.49A4.49,4.49,0,0,1,0,16.53v-12A4.48,4.48,0,0,1,4.18,0h12Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -0,0 +1,10 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const SearchIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9,0a9,9,0,0,1,7,14.62l3.68,3.67a1,1,0,0,1-1.32,1.5l-.1-.08L14.62,16A9,9,0,1,1,9,0ZM9,2a7,7,0,1,0,4.87,12l.07-.09.09-.07A7,7,0,0,0,9,2Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -0,0 +1,18 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const ShareIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 29.09 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
shapeRendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M24.73,0a5.82,5.82,0,1,1-4.14,9.91l-7.72,4.51a5.85,5.85,0,0,1,0,3.16l7.73,4.5a5.81,5.81,0,1,1-1.47,2.51l-7.72-4.5a5.82,5.82,0,1,1,0-8.22l0,0L19.13,7.4a5.82,5.82,0,0,1,4-7.18A5.69,5.69,0,0,1,24.73,0Zm0,23.27a2.93,2.93,0,0,0-2.43,1.3,1,1,0,0,1-.07.15l-.09.14-.05.09A2.91,2.91,0,1,0,26,23.54,2.86,2.86,0,0,0,24.73,23.27ZM7.27,13.09a2.91,2.91,0,1,0,2.51,4.37l0-.05A2.93,2.93,0,0,0,10.18,16a2.89,2.89,0,0,0-.4-1.46v0A2.9,2.9,0,0,0,7.27,13.09ZM24.73,2.91a2.92,2.92,0,0,0-2.55,4.32l0,0v0a2.91,2.91,0,1,0,2.5-4.4Z"
|
||||
transform="translate(-1.46 0)"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -0,0 +1,10 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const SimpleUploadIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19,12a1,1,0,0,1,1,.88V17a3,3,0,0,1-2.82,3H3a3,3,0,0,1-3-2.82V13a1,1,0,0,1,2-.12V17a1,1,0,0,0,.88,1H17a1,1,0,0,0,1-.88V13A1,1,0,0,1,19,12ZM10,0h0a1,1,0,0,1,.62.22l.09.07,5,5a1,1,0,0,1-1.32,1.5l-.1-.08L11,3.41V13a1,1,0,0,1-2,.12V3.41L5.71,6.71a1,1,0,0,1-1.32.08l-.1-.08a1,1,0,0,1-.08-1.32l.08-.1,5-5L9.38.22h0L9.29.29A1.05,1.05,0,0,1,10,0Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -9,3 +9,7 @@ export * from "./icons/CircledErrorIcon";
|
|||
export * from "./icons/CircledProgressIcon";
|
||||
export * from "./icons/CircledArrowUpIcon";
|
||||
export * from "./icons/PlusIcon";
|
||||
export * from "./icons/SearchIcon";
|
||||
export * from "./icons/CopyIcon";
|
||||
export * from "./icons/ShareIcon";
|
||||
export * from "./icons/SimpleUploadIcon";
|
||||
|
|
|
@ -4,7 +4,7 @@ const propTypes = {
|
|||
/**
|
||||
* Size of the icon's bounding box.
|
||||
*/
|
||||
size: PropTypes.number,
|
||||
size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import useSWR from "swr";
|
||||
|
||||
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
|
||||
import useFormattedActivityData from "./useFormattedActivityData";
|
||||
|
||||
|
@ -12,10 +13,10 @@ export default function ActivityTable({ type }) {
|
|||
if (!items.length) {
|
||||
return (
|
||||
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
||||
{/* TODO: proper loading indicator / error message */}
|
||||
{!data && !error && <p>Loading...</p>}
|
||||
{/* TODO: proper error message */}
|
||||
{!data && !error && <ContainerLoadingIndicator />}
|
||||
{!data && error && <p>An error occurred while loading this data.</p>}
|
||||
{data && <p>No files found.</p>}
|
||||
{data && !error && <p>No files found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export default function LatestActivity() {
|
|||
<Tab id="uploads" title="Uploads" />
|
||||
<Tab id="downloads" title="Downloads" />
|
||||
<TabPanel tabId="uploads" className="pt-4">
|
||||
<ActivityTable type="uplodads" />
|
||||
<ActivityTable type="uploads" />
|
||||
<ViewAllLink to="/files?tab=uploads" />
|
||||
</TabPanel>
|
||||
<TabPanel tabId="downloads" className="pt-4">
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import styled from "styled-components";
|
||||
import { CircledProgressIcon } from "../Icons";
|
||||
|
||||
/**
|
||||
* This loading indicator is designed to be replace entire blocks (i.e. components)
|
||||
* while they are fetching required data.
|
||||
*
|
||||
* It will take 50% of the parent's height, but won't get bigger than 150x150 px.
|
||||
*/
|
||||
const Wrapper = styled.div.attrs({
|
||||
className: "flex w-full h-full justify-center items-center p-8 text-palette-100",
|
||||
})``;
|
||||
|
||||
export const ContainerLoadingIndicator = (props) => (
|
||||
<Wrapper {...props}>
|
||||
<CircledProgressIcon size="50%" className="max-w-[150px] max-h-[150px] animate-[spin_3s_linear_infinite]" />
|
||||
</Wrapper>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export * from "./ContainerLoadingIndicator";
|
|
@ -9,7 +9,7 @@ import { PageContainer } from "../PageContainer";
|
|||
import { NavBarLink, NavBarSection } from ".";
|
||||
|
||||
const NavBarContainer = styled.div.attrs({
|
||||
className: `grid sticky top-0 bg-white`,
|
||||
className: `grid sticky top-0 bg-white z-10 shadow-sm`,
|
||||
})``;
|
||||
|
||||
const NavBarBody = styled.nav.attrs({
|
||||
|
@ -68,8 +68,21 @@ export const NavBar = () => (
|
|||
</NavBarSection>
|
||||
<NavBarSection className="dropdown-area justify-end">
|
||||
<DropdownMenu title="My account">
|
||||
<DropdownMenuLink href="/settings" icon={CogIcon} label="Settings" />
|
||||
<DropdownMenuLink href="/logout" icon={LockClosedIcon} label="Log out" />
|
||||
<DropdownMenuLink
|
||||
to="/settings"
|
||||
as={Link}
|
||||
activeClassName="text-primary"
|
||||
icon={CogIcon}
|
||||
label="Settings"
|
||||
partiallyActive
|
||||
/>
|
||||
<DropdownMenuLink
|
||||
to="/logout"
|
||||
as={Link}
|
||||
activeClassName="text-primary"
|
||||
icon={LockClosedIcon}
|
||||
label="Log out"
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</NavBarSection>
|
||||
</NavBarBody>
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { Children, cloneElement, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useClickAway } from "react-use";
|
||||
import styled, { css, keyframes } from "styled-components";
|
||||
|
||||
const dropDown = keyframes`
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
80% {
|
||||
transform: scaleY(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div.attrs({ className: "relative inline-flex" })``;
|
||||
|
||||
const Flyout = styled.ul.attrs({
|
||||
className: `absolute right-0 z-10 py-2
|
||||
border rounded bg-white
|
||||
overflow-hidden pointer-events-none
|
||||
shadow-md shadow-palette-200/50
|
||||
pointer-events-auto h-auto overflow-visible border-primary`,
|
||||
})`
|
||||
top: calc(100% + 2px);
|
||||
animation: ${css`
|
||||
${dropDown} 0.1s ease-in-out
|
||||
`};
|
||||
`;
|
||||
|
||||
const Option = styled.li.attrs({
|
||||
className: `font-sans text-xs uppercase
|
||||
relative pl-3 pr-5 py-1
|
||||
text-palette-600 cursor-pointer
|
||||
hover:text-primary hover:font-normal
|
||||
active:text-primary active:font-normal
|
||||
|
||||
before:content-[initial] before:absolute before:left-0 before:h-3 before:w-0.5 before:bg-primary before:top-1.5
|
||||
hover:before:content-['']`,
|
||||
})``;
|
||||
|
||||
export const PopoverMenu = ({ options, children, openClassName, ...props }) => {
|
||||
const containerRef = useRef();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useClickAway(containerRef, () => setOpen(false));
|
||||
|
||||
const handleChoice = (callback) => () => {
|
||||
setOpen(false);
|
||||
callback();
|
||||
};
|
||||
|
||||
return (
|
||||
<Container ref={containerRef} {...props}>
|
||||
{Children.only(
|
||||
cloneElement(children, {
|
||||
onClick: () => setOpen((open) => !open),
|
||||
className: `${children.props.className ?? ""} ${open ? openClassName : ""}`,
|
||||
})
|
||||
)}
|
||||
{open && (
|
||||
<Flyout>
|
||||
{options.map(({ label, callback }) => (
|
||||
<Option key={label} onClick={handleChoice(callback)}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</Flyout>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
PopoverMenu.propTypes = {
|
||||
/**
|
||||
* Accepts a single child node that will become a menu toggle.
|
||||
*/
|
||||
children: PropTypes.element.isRequired,
|
||||
/**
|
||||
* Positions in the menu
|
||||
*/
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
callback: PropTypes.func.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
};
|
|
@ -21,9 +21,11 @@ const dropDown = keyframes`
|
|||
|
||||
const Container = styled.div.attrs({ className: "relative inline-flex" })``;
|
||||
|
||||
const Trigger = styled.button.attrs(({ placeholder }) => ({
|
||||
className: `flex items-center cursor-pointer ${placeholder ? "text-palette-300" : ""}`,
|
||||
}))``;
|
||||
const Trigger = styled.button.attrs(({ $placeholder }) => ({
|
||||
className: `flex items-center cursor-pointer font-bold ${$placeholder ? "text-palette-300" : ""}`,
|
||||
}))`
|
||||
text-transform: inherit;
|
||||
`;
|
||||
|
||||
const TriggerIcon = styled(ChevronDownIcon).attrs({
|
||||
className: "transition-transform text-primary",
|
||||
|
@ -32,13 +34,14 @@ const TriggerIcon = styled(ChevronDownIcon).attrs({
|
|||
`;
|
||||
|
||||
const Flyout = styled.ul.attrs(({ open }) => ({
|
||||
className: `absolute top-[20px] right-0
|
||||
p-0 h-0 border rounded bg-white
|
||||
className: `absolute right-0 z-10
|
||||
p-0 border rounded bg-white
|
||||
overflow-hidden pointer-events-none
|
||||
shadow-md shadow-palette-200/50
|
||||
${open ? "pointer-events-auto h-auto overflow-visible border-primary" : ""}
|
||||
${open ? "visible" : "invisible"}`,
|
||||
}))`
|
||||
top: calc(100% + 2px);
|
||||
animation: ${({ open }) =>
|
||||
open
|
||||
? css`
|
||||
|
@ -47,7 +50,7 @@ const Flyout = styled.ul.attrs(({ open }) => ({
|
|||
: "none"};
|
||||
`;
|
||||
|
||||
export const Select = ({ defaultValue, children, onChange, placeholder }) => {
|
||||
export const Select = ({ defaultValue, children, onChange, placeholder, ...props }) => {
|
||||
const selectRef = useRef();
|
||||
const options = useMemo(() => Children.toArray(children).filter(({ type }) => type === SelectOption), [children]);
|
||||
const [state, dispatch] = useSelectReducer({ defaultValue, placeholder, options });
|
||||
|
@ -65,8 +68,8 @@ export const Select = ({ defaultValue, children, onChange, placeholder }) => {
|
|||
const activeLabel = activeOption?.props?.label ?? null;
|
||||
|
||||
return (
|
||||
<Container ref={selectRef}>
|
||||
<Trigger placeholder={!activeLabel && placeholder} onClick={toggle}>
|
||||
<Container ref={selectRef} {...props}>
|
||||
<Trigger $placeholder={!activeLabel && placeholder} onClick={toggle} className={state.open ? "text-primary" : ""}>
|
||||
{activeLabel ?? placeholder} <TriggerIcon open={state.open} />
|
||||
</Trigger>
|
||||
<Flyout role="listbox" open={state.open}>
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
.react-switch-checkbox {
|
||||
height: 0;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-switch-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
width: 44px;
|
||||
height: 22px;
|
||||
background: white;
|
||||
border-radius: 11px;
|
||||
@apply border-palette-200;
|
||||
border-width: 1px;
|
||||
position: relative;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.react-switch-label .react-switch-button {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.react-switch-checkbox:checked + .react-switch-label .react-switch-button {
|
||||
left: calc(100% - 2px);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.react-switch-label:active .react-switch-button {
|
||||
width: 20px;
|
||||
}
|
|
@ -1,37 +1,91 @@
|
|||
import PropTypes from "prop-types";
|
||||
import "./Switch.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const Container = styled.div.attrs({
|
||||
className: "inline-flex items-center gap-1 cursor-pointer select-none",
|
||||
})``;
|
||||
|
||||
const Checkbox = styled.input.attrs({
|
||||
type: "checkbox",
|
||||
className: `h-0 w-0 hidden`,
|
||||
})``;
|
||||
|
||||
const Label = styled.label.attrs({
|
||||
className: "cursor-pointer inline-flex items-center gap-2",
|
||||
})`
|
||||
&:active .toggle-pin {
|
||||
width: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Toggle = styled.span.attrs({
|
||||
className: `flex flex-row items-center justify-between shrink-0
|
||||
w-[44px] h-[22px] bg-white rounded-full
|
||||
border border-palette-200 relative cursor-pointer`,
|
||||
})`
|
||||
&:active .toggle-pin {
|
||||
width: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const TogglePin = styled.span.attrs(({ $checked }) => ({
|
||||
className: `toggle-pin
|
||||
absolute top-[2px] w-4 h-4 rounded-full
|
||||
transition-[width_left] active:w-5
|
||||
|
||||
${$checked ? "checked bg-primary" : "bg-palette-200"}`,
|
||||
}))`
|
||||
left: 2px;
|
||||
|
||||
&.checked {
|
||||
left: calc(100% - 2px);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
`;
|
||||
|
||||
export const Switch = ({ children, defaultChecked, labelClassName, onChange, ...props }) => {
|
||||
const id = useMemo(nanoid, [onChange]);
|
||||
const [checked, setChecked] = useState(defaultChecked);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(checked);
|
||||
}, [checked, onChange]);
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const Switch = ({ isOn, handleToggle }) => {
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
checked={isOn}
|
||||
onChange={handleToggle}
|
||||
className="react-switch-checkbox"
|
||||
id={`react-switch-new`}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label className={"react-switch-label"} htmlFor={`react-switch-new`}>
|
||||
<span className={`react-switch-button ${isOn ? "bg-primary" : "bg-palette-200"}`} />
|
||||
</label>
|
||||
</>
|
||||
<Container {...props}>
|
||||
<Checkbox checked={checked} onChange={(ev) => setChecked(ev.target.checked)} id={id} />
|
||||
<Label htmlFor={id} className={labelClassName}>
|
||||
<Toggle>
|
||||
<TogglePin $checked={checked} />
|
||||
</Toggle>
|
||||
<div className="-mt-0.5">{children}</div>
|
||||
</Label>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Switch.propTypes = {
|
||||
/**
|
||||
* Switch's current value
|
||||
* Should the checkbox be checked by default?
|
||||
*/
|
||||
isOn: PropTypes.bool,
|
||||
defaultChecked: PropTypes.bool,
|
||||
/**
|
||||
* Element to be rendered as the switch label
|
||||
*/
|
||||
children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
|
||||
/**
|
||||
* Pass additional CSS classes to the `label` element.
|
||||
*/
|
||||
labelClassName: PropTypes.string,
|
||||
/**
|
||||
* Function to execute on change
|
||||
*/
|
||||
handleToggle: PropTypes.func,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Switch.defaultProps = {
|
||||
isOn: false,
|
||||
defaultChecked: false,
|
||||
labelClassName: "",
|
||||
};
|
||||
|
|
|
@ -13,10 +13,10 @@ const Template = (args) => <Switch {...args} />;
|
|||
export const SwitchTrue = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
SwitchTrue.args = {
|
||||
isOn: true,
|
||||
defaultChecked: true,
|
||||
};
|
||||
export const SwitchFalse = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
SwitchFalse.args = {
|
||||
isOn: false,
|
||||
defaultChecked: false,
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
const Container = styled.div.attrs({
|
||||
className: "p-1 max-w-full overflow-x-auto",
|
||||
className: "p-1 max-w-full",
|
||||
})``;
|
||||
|
||||
const StyledTable = styled.table.attrs({
|
||||
|
|
|
@ -4,7 +4,8 @@ import styled from "styled-components";
|
|||
* Accepts all HMTL attributes a `<th>` element does.
|
||||
*/
|
||||
export const TableHeadCell = styled.th.attrs({
|
||||
className: `px-6 py-2.5 truncate h-tableRow
|
||||
className: `first:pl-6 last:pr-6 px-2 py-4
|
||||
truncate h-tableRow
|
||||
text-palette-600 font-sans font-light text-xs
|
||||
first:rounded-l-sm last:rounded-r-sm`,
|
||||
})`
|
||||
|
|
|
@ -34,7 +34,11 @@ const Body = styled.div.attrs({ className: "grow min-h-0" })``;
|
|||
export const Tabs = ({ defaultTab, children, variant }) => {
|
||||
const getTabId = usePrefixedTabIds();
|
||||
const { tabs, panels, tabsRefs } = useTabsChildren(children, getTabId);
|
||||
const defaultTabId = useMemo(() => getTabId(defaultTab || tabs[0].props.id), [getTabId, defaultTab, tabs]);
|
||||
const defaultTabId = useMemo(() => {
|
||||
const requestedTabIsPresent = tabs.find(({ props }) => props.id === defaultTab);
|
||||
|
||||
return getTabId(requestedTabIsPresent ? defaultTab : tabs[0].props.id);
|
||||
}, [getTabId, defaultTab, tabs]);
|
||||
const [activeTabId, setActiveTabId] = useState(defaultTabId);
|
||||
const [activeTabRef, setActiveTabRef] = useState(tabsRefs[activeTabId]);
|
||||
const isActive = (id) => id === activeTabId;
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import { nanoid } from "nanoid";
|
||||
import PropTypes from "prop-types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const TextInputBasic = ({ label, placeholder, ...props }) => {
|
||||
const id = useMemo(() => `input-${nanoid()}`, []);
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const TextInputBasic = ({ label, placeholder }) => {
|
||||
return (
|
||||
<div className={""}>
|
||||
<p className={"font-sans uppercase text-palette-300 text-inputLabel mb-textInputLabelBottom"}>{label}</p>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label className="font-sans uppercase text-palette-300 text-xs" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
className={
|
||||
"w-full bg-palette-100 h-textInput px-textInputBasicX focus:outline-none bg-transparent " +
|
||||
"placeholder-palette-400 text-content tracking-inputPlaceholder text-textInput"
|
||||
}
|
||||
className="w-full py-2 px-4 bg-palette-100 rounded-sm placeholder:text-palette-200 focus:outline outline-1 outline-palette-200"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,20 +1,45 @@
|
|||
import PropTypes from "prop-types";
|
||||
import cn from "classnames";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { PlusIcon } from "../Icons";
|
||||
|
||||
export const TextInputIcon = ({ className, icon, placeholder, onChange }) => {
|
||||
const inputRef = useRef();
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
onChange(value);
|
||||
}, [value, onChange]);
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const TextInputIcon = ({ icon, position, placeholder }) => {
|
||||
return (
|
||||
<div className={"flex flex-row items-center px-textInputIcon h-textInput rounded-full bg-palette-100"}>
|
||||
{position === "left" ? <div className={"w-buttonIconLg h-buttonIconLg"}>{icon}</div> : null}
|
||||
<input
|
||||
placeholder={placeholder}
|
||||
className={
|
||||
"w-full focus:outline-none mx-textInputHorizontal rounded-full bg-transparent " +
|
||||
"placeholder-palette-400 text-content tracking-inputPlaceholder text-textInput"
|
||||
<div
|
||||
className={cn(
|
||||
"grid-flow-col inline-grid grid-cols-[2rem_1fr_1.5rem] items-center rounded-full bg-palette-100 px-4 py-2",
|
||||
className,
|
||||
{
|
||||
"outline outline-1 outline-primary": focused,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div>{icon}</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="focus:outline-none bg-transparent placeholder:text-palette-400"
|
||||
/>
|
||||
{position === "right" ? <div className={"w-buttonIconLg h-buttonIconLg"}>{icon}</div> : null}
|
||||
{value && (
|
||||
<PlusIcon
|
||||
size={14}
|
||||
role="button"
|
||||
className="justify-self-end text-palette-400 rotate-45"
|
||||
onClick={() => setValue("")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -23,13 +48,13 @@ TextInputIcon.propTypes = {
|
|||
/**
|
||||
* Icon to place in text input
|
||||
*/
|
||||
icon: PropTypes.element,
|
||||
/**
|
||||
* Side to place icon
|
||||
*/
|
||||
position: PropTypes.oneOf(["left", "right"]),
|
||||
icon: PropTypes.element.isRequired,
|
||||
/**
|
||||
* Input placeholder
|
||||
*/
|
||||
placeholder: PropTypes.string,
|
||||
/**
|
||||
* Function to be called whenever value changes
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { TextInputIcon } from "./TextInputIcon";
|
||||
import { CogIcon } from "../Icons";
|
||||
import { SearchIcon } from "../Icons";
|
||||
import { Panel } from "../Panel";
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
|
@ -9,19 +10,21 @@ export default {
|
|||
};
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template = (args) => <TextInputIcon {...args} />;
|
||||
const Template = (args) => (
|
||||
<Panel>
|
||||
<TextInputIcon {...args} />
|
||||
</Panel>
|
||||
);
|
||||
|
||||
export const IconLeft = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
IconLeft.args = {
|
||||
icon: <CogIcon />,
|
||||
position: "left",
|
||||
icon: <SearchIcon size={20} />,
|
||||
placeholder: "Search",
|
||||
};
|
||||
|
||||
export const IconRight = Template.bind({});
|
||||
IconRight.args = {
|
||||
icon: <CogIcon />,
|
||||
position: "right",
|
||||
icon: <SearchIcon size={20} />,
|
||||
placeholder: "Search",
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { createContext } from "react";
|
||||
|
||||
export const PlansContext = createContext({
|
||||
plans: [],
|
||||
limits: [],
|
||||
error: null,
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import freePlan from "../../lib/tiers";
|
||||
|
||||
import { PlansContext } from "./PlansContext";
|
||||
|
||||
/**
|
||||
* NOTE: this function heavily relies on the fact that each Plan's `tier`
|
||||
* property corresponds to the plan's index in UserLimits array in
|
||||
* skynet-accounts code.
|
||||
*
|
||||
* @see https://github.com/SkynetLabs/skynet-accounts/blob/7337e740b71b77e6d08016db801e293b8ad81abc/database/user.go#L53-L101
|
||||
*/
|
||||
const aggregatePlansAndLimits = (plans, limits) => {
|
||||
const sortedPlans = [freePlan, ...plans].sort((planA, planB) => planA.tier - planB.tier);
|
||||
|
||||
// Decorate each plan with its corresponding limits data, if available.
|
||||
if (limits?.length) {
|
||||
return sortedPlans.map((plan) => ({ ...plan, limits: limits[plan.tier] || null }));
|
||||
}
|
||||
|
||||
// If we don't have the limits data yet, set just return the plans.
|
||||
|
||||
return sortedPlans;
|
||||
};
|
||||
|
||||
export const PlansProvider = ({ children }) => {
|
||||
const { data: rawPlans, error: plansError } = useSWR("stripe/prices");
|
||||
const { data: limits, error: limitsError } = useSWR("limits");
|
||||
|
||||
const [plans, setPlans] = useState([freePlan]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (plansError || limitsError) {
|
||||
setLoading(false);
|
||||
setError(plansError || limitsError);
|
||||
} else if (rawPlans) {
|
||||
setLoading(false);
|
||||
setPlans(aggregatePlansAndLimits(rawPlans, limits?.userLimits));
|
||||
}
|
||||
}, [rawPlans, limits, plansError, limitsError]);
|
||||
|
||||
return <PlansContext.Provider value={{ plans, error, loading }}>{children}</PlansContext.Provider>;
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./PlansProvider";
|
||||
export * from "./usePlans";
|
|
@ -0,0 +1,5 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { PlansContext } from "./PlansContext";
|
||||
|
||||
export const usePlans = () => useContext(PlansContext);
|
|
@ -0,0 +1,22 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import freeTier from "../lib/tiers";
|
||||
import { usePlans } from "../contexts/plans";
|
||||
|
||||
export default function useActivePlan(user) {
|
||||
const { plans, error } = usePlans();
|
||||
|
||||
const [activePlan, setActivePlan] = useState(freeTier);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setActivePlan(plans.find((plan) => plan.tier === user.tier));
|
||||
}
|
||||
}, [plans, user]);
|
||||
|
||||
return {
|
||||
error,
|
||||
plans,
|
||||
activePlan,
|
||||
};
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import freeTier from "../lib/tiers";
|
||||
|
||||
export default function useSubscriptionPlans(user) {
|
||||
const { data: paidPlans, error, mutate } = useSWR("stripe/prices");
|
||||
const [plans, setPlans] = useState([freeTier]);
|
||||
const [activePlan, setActivePlan] = useState(freeTier);
|
||||
|
||||
useEffect(() => {
|
||||
if (paidPlans) {
|
||||
setPlans((plans) => [...plans, ...paidPlans].sort((planA, planB) => planA.tier - planB.tier));
|
||||
}
|
||||
}, [paidPlans]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setActivePlan(plans.find((plan) => plan.tier === user.tier));
|
||||
}
|
||||
}, [plans, user]);
|
||||
|
||||
return {
|
||||
error,
|
||||
mutate,
|
||||
plans,
|
||||
activePlan,
|
||||
};
|
||||
}
|
|
@ -8,6 +8,7 @@ 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",
|
||||
|
@ -25,7 +26,7 @@ const Layout = ({ children }) => {
|
|||
<Wrapper>
|
||||
{!user && (
|
||||
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
|
||||
<p>Loading...</p> {/* TODO: Do something nicer here */}
|
||||
<ContainerLoadingIndicator className="!text-palette-200/50" />
|
||||
</div>
|
||||
)}
|
||||
{user && <>{children}</>}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import * as React from "react";
|
||||
import { Link } from "gatsby";
|
||||
import styled from "styled-components";
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
import { authenticatedOnly } from "../lib/swrConfig";
|
||||
|
||||
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 = () => (
|
||||
<aside className="w-full lg:w-48 bg-white text-sm font-sans font-light text-palette-600 shrink-0">
|
||||
<nav>
|
||||
<SidebarLink activeClassName="!border-l-primary" to="/settings">
|
||||
Account
|
||||
</SidebarLink>
|
||||
<SidebarLink activeClassName="!border-l-primary" to="/settings/notifications">
|
||||
Notifications
|
||||
</SidebarLink>
|
||||
<SidebarLink activeClassName="!border-l-primary" to="/settings/export">
|
||||
Import / Export
|
||||
</SidebarLink>
|
||||
<SidebarLink activeClassName="!border-l-primary" to="/settings/api-keys">
|
||||
API Keys
|
||||
</SidebarLink>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
|
||||
const SidebarLink = styled(Link).attrs({
|
||||
className: `h-12 py-3 px-6 h-full w-full flex
|
||||
border-l-2 border-l-palette-200
|
||||
border-b border-b-palette-100 last:border-b-transparent`,
|
||||
})``;
|
||||
|
||||
const Content = styled.main.attrs({
|
||||
className: "relative bg-white rounded px-6 py-6 sm:px-16 sm:py-14 mt-6 lg:mt-0 bg-none xl:bg-corner-circle",
|
||||
})`
|
||||
background-repeat: no-repeat;
|
||||
`;
|
||||
|
||||
const UserSettingsLayout = ({ children }) => (
|
||||
<SWRConfig value={authenticatedOnly}>
|
||||
<UserProvider>
|
||||
<Layout>
|
||||
<NavBar />
|
||||
<PageContainer className="mt-2 md:mt-14">
|
||||
<h6 className="hidden md:block mb-2 text-palette-400">Settings</h6>
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<Sidebar />
|
||||
<Content className="lg:w-settings-lg xl:w-settings-xl">{children}</Content>
|
||||
</div>
|
||||
</PageContainer>
|
||||
<Footer />
|
||||
</Layout>
|
||||
</UserProvider>
|
||||
</SWRConfig>
|
||||
);
|
||||
|
||||
export default UserSettingsLayout;
|
|
@ -2,8 +2,28 @@ import * as React from "react";
|
|||
|
||||
import DashboardLayout from "../layouts/DashboardLayout";
|
||||
|
||||
import { Panel } from "../components/Panel";
|
||||
import { Tab, TabPanel, Tabs } from "../components/Tabs";
|
||||
import FileList from "../components/FileList/FileList";
|
||||
import { useSearchParam } from "react-use";
|
||||
|
||||
const FilesPage = () => {
|
||||
return <>FILES</>;
|
||||
const defaultTab = useSearchParam("tab");
|
||||
|
||||
return (
|
||||
<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" />
|
||||
</TabPanel>
|
||||
<TabPanel tabId="downloads" className="pt-4">
|
||||
<FileList type="downloads" />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
FilesPage.Layout = DashboardLayout;
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import { useMedia } from "react-use";
|
||||
|
||||
import theme from "../lib/theme";
|
||||
import { PlansProvider } from "../contexts/plans/PlansProvider";
|
||||
import { ArrowRightIcon } from "../components/Icons";
|
||||
import { Panel } from "../components/Panel";
|
||||
import { Tab, TabPanel, Tabs } from "../components/Tabs";
|
||||
|
@ -16,7 +17,7 @@ const IndexPage = () => {
|
|||
const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlansProvider>
|
||||
<div className="w-full">
|
||||
<Slider
|
||||
slides={[
|
||||
|
@ -60,7 +61,7 @@ const IndexPage = () => {
|
|||
<LatestActivity />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</PlansProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import * as React from "react";
|
||||
|
||||
import UserSettingsLayout from "../../layouts/UserSettingsLayout";
|
||||
|
||||
const APIKeysPage = () => {
|
||||
return (
|
||||
<>
|
||||
<h4>API Keys</h4>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
APIKeysPage.Layout = UserSettingsLayout;
|
||||
|
||||
export default APIKeysPage;
|
|
@ -0,0 +1,15 @@
|
|||
import * as React from "react";
|
||||
|
||||
import UserSettingsLayout from "../../layouts/UserSettingsLayout";
|
||||
|
||||
const ExportPage = () => {
|
||||
return (
|
||||
<>
|
||||
<h4>Import / export</h4>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExportPage.Layout = UserSettingsLayout;
|
||||
|
||||
export default ExportPage;
|
|
@ -0,0 +1,81 @@
|
|||
import * as React from "react";
|
||||
import { useMedia } from "react-use";
|
||||
import styled from "styled-components";
|
||||
|
||||
import theme from "../../lib/theme";
|
||||
import UserSettingsLayout from "../../layouts/UserSettingsLayout";
|
||||
import { TextInputBasic } from "../../components/TextInputBasic/TextInputBasic";
|
||||
import { Button } from "../../components/Button";
|
||||
import { AvatarUploader } from "../../components/AvatarUploader";
|
||||
|
||||
const FormGroup = styled.div.attrs({
|
||||
className: "grid sm:grid-cols-[1fr_min-content] w-full gap-y-2 gap-x-4 items-end",
|
||||
})``;
|
||||
|
||||
const AccountPage = () => {
|
||||
const isLargeScreen = useMedia(`(min-width: ${theme.screens.xl})`);
|
||||
return (
|
||||
<>
|
||||
<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]">
|
||||
<section>
|
||||
<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 />
|
||||
{!isLargeScreen && (
|
||||
<section>
|
||||
<AvatarUploader className="flex flex-col sm:flex-row gap-8 items-center" />
|
||||
</section>
|
||||
)}
|
||||
<section className="flex flex-col gap-8">
|
||||
<FormGroup>
|
||||
<TextInputBasic label="Display name" placeholder="John Doe" />
|
||||
<div className="flex mt-2 sm:mt-0 justify-center">
|
||||
<Button>Update</Button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<TextInputBasic label="Email" placeholder="john.doe@example.com" />
|
||||
<div className="flex mt-2 sm:mt-0 justify-center">
|
||||
<Button>Update</Button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<TextInputBasic type="password" label="Password" placeholder="dbf3htf*efh4pcy@PXB" />
|
||||
<div className="flex mt-2 sm:mt-0 justify-center order-last sm:order-none">
|
||||
<Button>Update</Button>
|
||||
</div>
|
||||
<small className="text-palette-400">
|
||||
The password must be at least 6 characters long. Significantly different from the email and old
|
||||
password.
|
||||
</small>
|
||||
</FormGroup>
|
||||
</section>
|
||||
<hr />
|
||||
<section>
|
||||
<h6 className="text-palette-400">Delete account</h6>
|
||||
<p>This will completely delete your account. This process can't be undone.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.confirm("TODO: confirmation modal")}
|
||||
className="text-error underline decoration-1 hover:decoration-dashed"
|
||||
>
|
||||
Delete account
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
<div className="flex w-full justify-start xl:justify-end">
|
||||
{isLargeScreen && <AvatarUploader className="flex flex-col gap-4" />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AccountPage.Layout = UserSettingsLayout;
|
||||
|
||||
export default AccountPage;
|
|
@ -0,0 +1,51 @@
|
|||
import * as React from "react";
|
||||
|
||||
import UserSettingsLayout from "../../layouts/UserSettingsLayout";
|
||||
|
||||
import { Switch } from "../../components/Switch";
|
||||
import { StaticImage } from "gatsby-plugin-image";
|
||||
|
||||
const NotificationsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex">
|
||||
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]">
|
||||
<h4>Notifications</h4>
|
||||
<section>
|
||||
{/* TODO: saves on change */}
|
||||
<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.
|
||||
</Switch>
|
||||
</section>
|
||||
<hr />
|
||||
<section>
|
||||
<h6 className="text-palette-300">Statistics</h6>
|
||||
{/* TODO: proper content :) */}
|
||||
<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">
|
||||
<li>
|
||||
{/* TODO: saves on change */}
|
||||
<Switch onChange={console.info.bind(console)}>Storage limit</Switch>
|
||||
</li>
|
||||
<li>
|
||||
{/* TODO: saves on change */}
|
||||
<Switch onChange={console.info.bind(console)}>File limit</Switch>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div className="hidden xl:block text-right w-full pr-14 pt-20">
|
||||
<StaticImage src="../../../static/images/inbox.svg" alt="" placeholder="none" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationsPage.Layout = UserSettingsLayout;
|
||||
|
||||
export default NotificationsPage;
|
|
@ -39,7 +39,14 @@
|
|||
font-size: 1rem;
|
||||
}
|
||||
h6 {
|
||||
@apply uppercase;
|
||||
font-size: 0.875rem;
|
||||
@apply uppercase text-xs;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply mt-2;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-t-palette-200;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 160 160"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#f4f6f7;}.cls-3{fill:#231f20;}.cls-4{mask:url(#mask);}.cls-5,.cls-7{fill:none;stroke-width:2px;}.cls-5{stroke:#0f0f0f;stroke-linejoin:round;}.cls-6,.cls-8{fill:#40b75e;}.cls-6{fill-rule:evenodd;}.cls-7{stroke:#231f20;stroke-miterlimit:10;}</style><mask id="mask" x="15.52" y="37.16" width="128" height="146" maskUnits="userSpaceOnUse"><g id="gf1qkqaq7b"><circle id="asdc4b43va" class="cls-1" cx="79.52" cy="101.16" r="64"/></g></mask></defs><g id="Layer_2" data-name="Layer 2"><circle class="cls-2" cx="80" cy="80" r="79"/><path class="cls-3" d="M80,2A78,78,0,1,1,2,80,78.09,78.09,0,0,1,80,2m0-2a80,80,0,1,0,80,80A80,80,0,0,0,80,0Z"/></g><g id="Layer_1" data-name="Layer 1"><g class="cls-4"><path class="cls-5" d="M103.52,129.16l6,2c12,4,22,9.85,22,22v30m-40-70v10m-36,6-6,2c-12,4-22,9.85-22,22v30m40-70v10m-10-40,6-4h14l6-6,12,6,6,4v8l-2,2c0,10-6,24-20,24s-20-14-20-24l-2-2Z"/><path class="cls-5" d="M57.52,83.16l-2-2v-14l6-10,4,4,8-4,8,4h12a10,10,0,0,1,10,10v10l-2,2"/><path class="cls-6" d="M69.52,128.16l10,14,10-14h8l8,4-20,20h-12l-20-20,8-4Z"/><path class="cls-5" d="M69.52,123.16l10,14,10-14h8l8,4-20,20h-12l-20-20,8-4Z"/></g></g><g id="Layer_3" data-name="Layer 3"><circle class="cls-7" cx="95.3" cy="33.27" r="4.55"/><circle class="cls-8" cx="20.61" cy="86.7" r="2.29"/><circle class="cls-8" cx="128.65" cy="60.06" r="5.08"/></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 171 126"><defs><style>.cls-1{fill:#40b75e;fill-rule:evenodd;}.cls-2,.cls-3{fill:none;stroke:#0f0f0f;stroke-width:2px;}.cls-2{stroke-linejoin:round;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M107.89,26.18a23.09,23.09,0,0,1,9.26,1.94l-9.26,21.06,22,6.63a23,23,0,1,1-22-29.63Z"/><path class="cls-2" d="M115.89,45.18l-10,10-6-6"/><path class="cls-2" d="M123.89,49.18a16,16,0,1,1-16-16A16,16,0,0,1,123.89,49.18Z"/><path class="cls-3" d="M156,45h14v80H46V45H60"/><path class="cls-3" d="M150,65V17L134,1H66V65"/><path class="cls-2" d="M72,9h56M72,17h56M108,77,46,125H170ZM46,45,86,85m84-40L130,85"/><path class="cls-3" d="M150,17H134V1M106,117h4m4,0h4m-20,0h4"/><path class="cls-3" d="M29,45H0m34,9H15M44,34H25"/></g></g></svg>
|
After Width: | Height: | Size: 843 B |
|
@ -28,6 +28,7 @@ module.exports = {
|
|||
borderColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||
textColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||
placeholderColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||
outlineColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["Sora", ...defaultTheme.fontFamily.sans],
|
||||
|
@ -38,6 +39,10 @@ module.exports = {
|
|||
tab: ["18px", "28px"],
|
||||
},
|
||||
backgroundColor: ["disabled"],
|
||||
backgroundImage: {
|
||||
"corner-circle":
|
||||
"radial-gradient(circle at calc(100% - 60px) -50px, #F5F5F7 0%, #f5f5f7 250px,rgba(0,0,0,0) 250px)",
|
||||
},
|
||||
textColor: ["disabled"],
|
||||
keyframes: {
|
||||
wiggle: {
|
||||
|
@ -53,6 +58,8 @@ module.exports = {
|
|||
"page-md": "640px",
|
||||
"page-lg": "896px",
|
||||
"page-xl": "1312px",
|
||||
"settings-lg": "704px",
|
||||
"settings-xl": "928px",
|
||||
},
|
||||
minWidth: {
|
||||
button: "112px",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@fontsource/sora": "4.5.3",
|
||||
"@fontsource/source-sans-pro": "4.5.3",
|
||||
"@fontsource/source-sans-pro": "4.5.4",
|
||||
"@stripe/react-stripe-js": "1.7.0",
|
||||
"@stripe/stripe-js": "1.24.0",
|
||||
"classnames": "2.3.1",
|
||||
|
@ -27,7 +27,7 @@
|
|||
"react-dom": "17.0.2",
|
||||
"react-toastify": "8.2.0",
|
||||
"skynet-js": "3.0.2",
|
||||
"stripe": "8.207.0",
|
||||
"stripe": "8.209.0",
|
||||
"swr": "1.2.2",
|
||||
"yup": "0.32.11"
|
||||
},
|
||||
|
@ -35,7 +35,7 @@
|
|||
"@tailwindcss/forms": "0.5.0",
|
||||
"@tailwindcss/typography": "0.5.2",
|
||||
"autoprefixer": "10.4.2",
|
||||
"eslint": "8.10.0",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-config-next": "12.1.0",
|
||||
"postcss": "8.4.8",
|
||||
"prettier": "2.5.1",
|
||||
|
|
|
@ -38,16 +38,16 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@eslint/eslintrc@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.0.tgz#7ce1547a5c46dfe56e1e45c3c9ed18038c721c6a"
|
||||
integrity sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w==
|
||||
"@eslint/eslintrc@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6"
|
||||
integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==
|
||||
dependencies:
|
||||
ajv "^6.12.4"
|
||||
debug "^4.3.2"
|
||||
espree "^9.3.1"
|
||||
globals "^13.9.0"
|
||||
ignore "^4.0.6"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.2.1"
|
||||
js-yaml "^4.1.0"
|
||||
minimatch "^3.0.4"
|
||||
|
@ -58,10 +58,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@fontsource/sora/-/sora-4.5.3.tgz#987c9b43acb00c9e3fa5377ebcedfd4ec9b760a7"
|
||||
integrity sha512-0ipYkroLonvChAyLajgIt6mImXMhvjrHwD5g7iX2ZR1eJ4hLDnwq6haW5pSeehe79lPjgp0BX6ZHivFIP0xR2g==
|
||||
|
||||
"@fontsource/source-sans-pro@4.5.3":
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/source-sans-pro/-/source-sans-pro-4.5.3.tgz#bdb1eeed5db70bcd1f68cd1e8c859834f0e6bc67"
|
||||
integrity sha512-9xWGu3ArKsjf6+WVrNoCUywybTB3rIidpvOI2tByQpzYVOupFUv6qohyrjDrVvPb6XHJQTD0NIzisR7RKhiP7A==
|
||||
"@fontsource/source-sans-pro@4.5.4":
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/source-sans-pro/-/source-sans-pro-4.5.4.tgz#51510723ff40f446c7800f133e9ae604ae2f38d7"
|
||||
integrity sha512-+YYw6HRvH9wYE+U2Hvxyossg+MHPApAj7VIjEqaXenNeNQa4U3uPD0e7pc+1Gic3srCQATN15O3S9WSFLXTmwQ==
|
||||
|
||||
"@humanwhocodes/config-array@^0.9.2":
|
||||
version "0.9.2"
|
||||
|
@ -911,12 +911,12 @@ 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"
|
||||
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
|
||||
|
||||
eslint@8.10.0:
|
||||
version "8.10.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.10.0.tgz#931be395eb60f900c01658b278e05b6dae47199d"
|
||||
integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==
|
||||
eslint@8.11.0:
|
||||
version "8.11.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.11.0.tgz#88b91cfba1356fc10bb9eb592958457dfe09fb37"
|
||||
integrity sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==
|
||||
dependencies:
|
||||
"@eslint/eslintrc" "^1.2.0"
|
||||
"@eslint/eslintrc" "^1.2.1"
|
||||
"@humanwhocodes/config-array" "^0.9.2"
|
||||
ajv "^6.10.0"
|
||||
chalk "^4.0.0"
|
||||
|
@ -1247,11 +1247,6 @@ ieee754@^1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
ignore@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
|
||||
ignore@^5.1.4, ignore@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
|
||||
|
@ -2255,10 +2250,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"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
stripe@8.207.0:
|
||||
version "8.207.0"
|
||||
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.207.0.tgz#4b7002f19cecefbc3c48f09f6658c39e359f99c1"
|
||||
integrity sha512-ZCjdqN2adGfrC5uAAo0v7IquzaiQ3+pDzB324/iV3Q3Deiot9VO7KMVSNVx/0i6E6ywhgV33ko3FMT7iUgxKYA==
|
||||
stripe@8.209.0:
|
||||
version "8.209.0"
|
||||
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.209.0.tgz#a8f34132fb4140bdf9152943b15c641ad99cd3b1"
|
||||
integrity sha512-ozfs8t0fxA/uvCK1DNvitSdEublOHK5CTRsrd2AWWk9LogjXcfkxmtz3KGSSQd+jyA2+rbee9TMzhJ6aabQ5WQ==
|
||||
dependencies:
|
||||
"@types/node" ">=8.1.0"
|
||||
qs "^6.6.0"
|
||||
|
|
|
@ -201,11 +201,7 @@ async function genericAccessCheck(name, url) {
|
|||
const data = { up: false, url };
|
||||
|
||||
try {
|
||||
const cookie = `nocache=true;${authCookie}`;
|
||||
const response = await got(url, {
|
||||
headers: { cookie },
|
||||
hooks: { beforeRedirect: [(options) => (options.headers.cookie = cookie)] },
|
||||
});
|
||||
const response = await got(url, { headers: { cookie: `nocache=true;${authCookie}` } });
|
||||
|
||||
data.statusCode = response.statusCode;
|
||||
data.up = true;
|
||||
|
|
|
@ -1023,27 +1023,13 @@ function fileEndpointCheck(done) {
|
|||
}
|
||||
|
||||
// check whether hns/note-to-self would properly redirect to note-to-self/
|
||||
function skylinkRootDomainEndpointRedirect(done) {
|
||||
function hnsEndpointDirectoryRedirect(done) {
|
||||
const expected = {
|
||||
name: "skylink root domain endpoint redirect",
|
||||
skylink: "AACogzrAimYPG42tDOKhS3lXZD8YvlF8Q8R17afe95iV2Q",
|
||||
statusCode: 301,
|
||||
headers: {
|
||||
location: `https://000ah0pqo256c3orhmmgpol19dslep1v32v52v23ohqur9uuuuc9bm8.${process.env.PORTAL_DOMAIN}`,
|
||||
},
|
||||
};
|
||||
|
||||
skylinkVerification(done, expected, { followRedirect: false });
|
||||
}
|
||||
|
||||
// check whether hns/note-to-self would properly redirect to note-to-self/
|
||||
function hnsRootDomainEndpointRedirect(done) {
|
||||
const expected = {
|
||||
name: "hns root domain endpoint redirect",
|
||||
name: "hns endpoint directory redirect",
|
||||
skylink: "hns/note-to-self",
|
||||
statusCode: 301,
|
||||
statusCode: 308,
|
||||
headers: {
|
||||
location: `https://note-to-self.hns.${process.env.PORTAL_DOMAIN}`,
|
||||
location: "note-to-self/",
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1150,12 +1136,7 @@ async function skylinkVerification(done, expected, { followRedirect = true, meth
|
|||
|
||||
try {
|
||||
const query = `https://${process.env.PORTAL_DOMAIN}/${expected.skylink}`;
|
||||
const cookie = `nocache=true;${authCookie}`;
|
||||
const response = await got[method](query, {
|
||||
followRedirect,
|
||||
headers: { cookie },
|
||||
hooks: { beforeRedirect: [(options) => (options.headers.cookie = cookie)] },
|
||||
});
|
||||
const response = await got[method](query, { followRedirect, headers: { cookie: `nocache=true;${authCookie}` } });
|
||||
const entry = { ...details, up: true, statusCode: response.statusCode, time: calculateElapsedTime(time) };
|
||||
const info = {};
|
||||
|
||||
|
@ -1256,8 +1237,7 @@ module.exports = [
|
|||
// uniswapHNSRedirectCheck,
|
||||
uniswapHNSResolverCheck,
|
||||
uniswapHNSResolverRedirectCheck,
|
||||
skylinkRootDomainEndpointRedirect,
|
||||
hnsRootDomainEndpointRedirect,
|
||||
hnsEndpointDirectoryRedirect,
|
||||
skappSkySend,
|
||||
skappNoteToSelf,
|
||||
skappUniswap,
|
||||
|
|
|
@ -6,24 +6,24 @@
|
|||
"author": "Skynet Labs.",
|
||||
"dependencies": {
|
||||
"@fontsource/sora": "4.5.3",
|
||||
"@fontsource/source-sans-pro": "4.5.3",
|
||||
"@fontsource/source-sans-pro": "4.5.4",
|
||||
"@svgr/webpack": "6.2.1",
|
||||
"bytes": "3.1.2",
|
||||
"classnames": "2.3.1",
|
||||
"copy-text-to-clipboard": "3.0.1",
|
||||
"crypto-browserify": "3.12.0",
|
||||
"framer-motion": "6.2.8",
|
||||
"gatsby": "4.9.2",
|
||||
"gatsby": "4.9.3",
|
||||
"gatsby-background-image": "1.6.0",
|
||||
"gatsby-plugin-image": "2.9.0",
|
||||
"gatsby-plugin-manifest": "4.9.0",
|
||||
"gatsby-plugin-manifest": "4.9.1",
|
||||
"gatsby-plugin-postcss": "5.9.0",
|
||||
"gatsby-plugin-react-helmet": "5.9.0",
|
||||
"gatsby-plugin-robots-txt": "1.7.0",
|
||||
"gatsby-plugin-sharp": "4.9.0",
|
||||
"gatsby-plugin-sharp": "4.9.1",
|
||||
"gatsby-plugin-sitemap": "5.9.0",
|
||||
"gatsby-plugin-svgr": "3.0.0-beta.0",
|
||||
"gatsby-source-filesystem": "4.9.0",
|
||||
"gatsby-source-filesystem": "4.9.1",
|
||||
"gatsby-transformer-sharp": "4.9.0",
|
||||
"gatsby-transformer-yaml": "4.9.0",
|
||||
"gbimage-bridge": "0.2.1",
|
||||
|
@ -40,7 +40,7 @@
|
|||
"react-dropzone": "12.0.4",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-use": "17.3.2",
|
||||
"skynet-js": "4.0.23-beta",
|
||||
"skynet-js": "4.0.26-beta",
|
||||
"stream-browserify": "3.0.0",
|
||||
"swr": "1.2.2"
|
||||
},
|
||||
|
|
|
@ -1103,10 +1103,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@fontsource/sora/-/sora-4.5.3.tgz#987c9b43acb00c9e3fa5377ebcedfd4ec9b760a7"
|
||||
integrity sha512-0ipYkroLonvChAyLajgIt6mImXMhvjrHwD5g7iX2ZR1eJ4hLDnwq6haW5pSeehe79lPjgp0BX6ZHivFIP0xR2g==
|
||||
|
||||
"@fontsource/source-sans-pro@4.5.3":
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/source-sans-pro/-/source-sans-pro-4.5.3.tgz#bdb1eeed5db70bcd1f68cd1e8c859834f0e6bc67"
|
||||
integrity sha512-9xWGu3ArKsjf6+WVrNoCUywybTB3rIidpvOI2tByQpzYVOupFUv6qohyrjDrVvPb6XHJQTD0NIzisR7RKhiP7A==
|
||||
"@fontsource/source-sans-pro@4.5.4":
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/source-sans-pro/-/source-sans-pro-4.5.4.tgz#51510723ff40f446c7800f133e9ae604ae2f38d7"
|
||||
integrity sha512-+YYw6HRvH9wYE+U2Hvxyossg+MHPApAj7VIjEqaXenNeNQa4U3uPD0e7pc+1Gic3srCQATN15O3S9WSFLXTmwQ==
|
||||
|
||||
"@gatsbyjs/parcel-namer-relative-to-cwd@0.0.2":
|
||||
version "0.0.2"
|
||||
|
@ -2195,6 +2195,19 @@
|
|||
escape-string-regexp "^2.0.0"
|
||||
lodash.deburr "^4.1.0"
|
||||
|
||||
"@skynetlabs/tus-js-client@^2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@skynetlabs/tus-js-client/-/tus-js-client-2.3.0.tgz#a14fd4197e2bc4ce8be724967a0e4c17d937cb64"
|
||||
integrity sha512-piGvPlJh+Bu3Qf08bDlc/TnFLXE81KnFoPgvnsddNwTSLyyspxPFxJmHO5ki6SYyOl3HmUtGPoix+r2M2UpFEA==
|
||||
dependencies:
|
||||
buffer-from "^0.1.1"
|
||||
combine-errors "^3.0.3"
|
||||
is-stream "^2.0.0"
|
||||
js-base64 "^2.6.1"
|
||||
lodash.throttle "^4.1.1"
|
||||
proper-lockfile "^2.0.1"
|
||||
url-parse "^1.4.3"
|
||||
|
||||
"@svgr/babel-plugin-add-jsx-attribute@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.0.0.tgz#bd6d1ff32a31b82b601e73672a789cc41e84fe18"
|
||||
|
@ -3167,6 +3180,13 @@ async-cache@^1.1.0:
|
|||
dependencies:
|
||||
lru-cache "^4.0.0"
|
||||
|
||||
async-mutex@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df"
|
||||
integrity sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==
|
||||
dependencies:
|
||||
tslib "^2.3.1"
|
||||
|
||||
async-retry-ng@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/async-retry-ng/-/async-retry-ng-2.0.1.tgz#f5285ec1c52654a2ba6a505d0c18b1eadfaebd41"
|
||||
|
@ -3236,12 +3256,12 @@ axios@^0.21.1:
|
|||
dependencies:
|
||||
follow-redirects "^1.14.0"
|
||||
|
||||
axios@^0.24.0:
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
|
||||
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
|
||||
axios@^0.26.0:
|
||||
version "0.26.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
|
||||
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.4"
|
||||
follow-redirects "^1.14.8"
|
||||
|
||||
axobject-query@^2.2.0:
|
||||
version "2.2.0"
|
||||
|
@ -3319,23 +3339,23 @@ babel-plugin-polyfill-regenerator@^0.3.0:
|
|||
dependencies:
|
||||
"@babel/helper-define-polyfill-provider" "^0.3.1"
|
||||
|
||||
babel-plugin-remove-graphql-queries@^4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-remove-graphql-queries/-/babel-plugin-remove-graphql-queries-4.9.0.tgz#5804094466f12670e3e43434addb80a3561d96d6"
|
||||
integrity sha512-q3xS5KDPoxujHrNWbilChE0Q+riCdxmvUseZbSzaikfY+KV9z3nCzaGkuEHxU2xVVa+8K5Nvu9zKlf/KtQfxXw==
|
||||
babel-plugin-remove-graphql-queries@^4.9.0, babel-plugin-remove-graphql-queries@^4.9.1:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-remove-graphql-queries/-/babel-plugin-remove-graphql-queries-4.9.1.tgz#75290c6dd840d28343dc47f9517a634e02255b80"
|
||||
integrity sha512-Mg+NB34cjdV6rIGIahMe0qij3KpWf7M8NFe8J1w2kxjQty4mpGX2qqmMUHhwxqwVWAhH1LZeiqitFZ6D/+CbJg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
|
||||
babel-plugin-transform-react-remove-prop-types@^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"
|
||||
integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==
|
||||
|
||||
babel-preset-gatsby@^2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-preset-gatsby/-/babel-preset-gatsby-2.9.0.tgz#6a478ee39b99be21b5ed804ebd8cc96a0fd492d3"
|
||||
integrity sha512-naGwVr1uCX2NsyM38pTp0f+vO0UfCH2h7wEC1H8B748twHTUpoOPJ/GWUs+/FQzNrfgOvHSspcqkRyD3Vb2EPg==
|
||||
babel-preset-gatsby@^2.9.1:
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/babel-preset-gatsby/-/babel-preset-gatsby-2.9.1.tgz#2f62f7f7899ed7282f0a84ef0b5a0e131225ed96"
|
||||
integrity sha512-HkZyo5Phb5+vbICx0Q8Goj+FV8xPH4detCqJUDHH9sfBvAjvdnKfL2dtDFd0QvKhUQ/55rO3Rdcmo6PU5zYwZw==
|
||||
dependencies:
|
||||
"@babel/plugin-proposal-class-properties" "^7.14.0"
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5"
|
||||
|
@ -3350,7 +3370,7 @@ babel-preset-gatsby@^2.9.0:
|
|||
babel-plugin-dynamic-import-node "^2.3.3"
|
||||
babel-plugin-macros "^2.8.0"
|
||||
babel-plugin-transform-react-remove-prop-types "^0.4.24"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
gatsby-legacy-polyfills "^2.9.0"
|
||||
|
||||
backo2@^1.0.2, backo2@~1.0.2:
|
||||
|
@ -5985,7 +6005,7 @@ flatted@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
|
||||
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.4:
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.8:
|
||||
version "1.14.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
|
||||
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
|
||||
|
@ -6147,10 +6167,10 @@ gatsby-background-image@1.6.0:
|
|||
short-uuid "^4.2.0"
|
||||
sort-media-queries "^0.2.2"
|
||||
|
||||
gatsby-cli@^4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-cli/-/gatsby-cli-4.9.0.tgz#3c6c9a26252671eb60abd399abdaca37836ab2a1"
|
||||
integrity sha512-e4pQ+7Z+9Cppql59fy0OOo2El+ERkzOCVW2+ev5CojiljDb4x/nUHIx9ahKhgA5136F0DaCZ6w/lrOWJmi3ZSQ==
|
||||
gatsby-cli@^4.9.1:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-cli/-/gatsby-cli-4.9.1.tgz#abe57cc656329deb69aef3d75b5cb14f36473d8e"
|
||||
integrity sha512-iU5pmwAq5d1XXo98BkYe2KccH3Dy/jsj7QsvP0CpfzOO0EFtidg5KUzPPaekLaGyoqxiMwWf0uAX7S1ERzMFYw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.14.0"
|
||||
"@babel/core" "^7.15.5"
|
||||
|
@ -6172,8 +6192,8 @@ gatsby-cli@^4.9.0:
|
|||
execa "^5.1.1"
|
||||
fs-exists-cached "^1.0.0"
|
||||
fs-extra "^10.0.0"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-telemetry "^3.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
gatsby-telemetry "^3.9.1"
|
||||
hosted-git-info "^3.0.8"
|
||||
is-valid-path "^0.1.1"
|
||||
joi "^17.4.2"
|
||||
|
@ -6197,10 +6217,10 @@ gatsby-cli@^4.9.0:
|
|||
yoga-layout-prebuilt "^1.10.0"
|
||||
yurnalist "^2.1.0"
|
||||
|
||||
gatsby-core-utils@^3.8.2, gatsby-core-utils@^3.9.0:
|
||||
version "3.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-core-utils/-/gatsby-core-utils-3.9.0.tgz#7be5969622e44c4475cb14f1ac64b49a4072ab6c"
|
||||
integrity sha512-SvPnr86oXTY3ldbQ4QAkEew3BQE9vlzUXcXVJqTOhMUeGEz2kibBFUmVp8ia9Y1eOD+K/0xXQ54jUqaResj69w==
|
||||
gatsby-core-utils@^3.8.2, gatsby-core-utils@^3.9.0, gatsby-core-utils@^3.9.1:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-core-utils/-/gatsby-core-utils-3.9.1.tgz#a4c1bb2021a7e7c06b4aad8d71c9c76ca9cdc21f"
|
||||
integrity sha512-DNf7NhhH0WrFuoBvyURjsw4w+eKbp1GlRA0cchYHJwVTaDPvLvX1o7zxN76xIBx+m0kttpnO3KuJ9LDOSli3ag==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
ci-info "2.0.0"
|
||||
|
@ -6233,26 +6253,26 @@ gatsby-legacy-polyfills@^2.9.0:
|
|||
"@babel/runtime" "^7.15.4"
|
||||
core-js-compat "3.9.0"
|
||||
|
||||
gatsby-link@^4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-link/-/gatsby-link-4.9.0.tgz#3aac564fbc406d550d8654ffad116f200f34a9c1"
|
||||
integrity sha512-5QiqDQo16b8VwEx4mRkAnPhAt+fyWOTT+sz2AkG7enR7psoQ7fg9Ndck0CHfME9aLQFm/2GLw8JwD1OGyARPRw==
|
||||
gatsby-link@^4.9.1:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-link/-/gatsby-link-4.9.1.tgz#af444bd3a28e90816a1291f85b70de920c302f1d"
|
||||
integrity sha512-c5YbR43fESNKlScS+ciJaLjuJuFruoL1my9z6k0ZbMIJUI7z6qH+XIVteH00Kbryz0fejNDaGWeAr3gvEyDlSA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
"@types/reach__router" "^1.3.10"
|
||||
gatsby-page-utils "^2.9.0"
|
||||
gatsby-page-utils "^2.9.1"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
gatsby-page-utils@^2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-page-utils/-/gatsby-page-utils-2.9.0.tgz#ec836db457eed98bc3a46fef3aba8c3d9ecd1c29"
|
||||
integrity sha512-/WaS9FJismKPZfRsS2vIhVAd/1eGRe9+dmFZS7Rp6OUPGrELlen3V8h6msE2BpbkmaTf4eQbKgFdrTBkykkAyw==
|
||||
gatsby-page-utils@^2.9.1:
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-page-utils/-/gatsby-page-utils-2.9.1.tgz#4da1fd5bb21623334868a9b77976755364ad03ed"
|
||||
integrity sha512-Otgwt30usTa94pWF3915+w/6uCPJIQHSVEAF8BD9iBl2mCBCZIS0+rCrVNm9lBrg+tc3JuezvIQsSycwaWnO5Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
bluebird "^3.7.2"
|
||||
chokidar "^3.5.2"
|
||||
fs-exists-cached "^1.0.0"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
glob "^7.2.0"
|
||||
lodash "^4.17.21"
|
||||
micromatch "^4.0.4"
|
||||
|
@ -6299,31 +6319,31 @@ gatsby-plugin-image@2.9.0:
|
|||
objectFitPolyfill "^2.3.5"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
gatsby-plugin-manifest@4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-manifest/-/gatsby-plugin-manifest-4.9.0.tgz#ddfd6e8d8597df7bdef07d527b08430508837ecb"
|
||||
integrity sha512-aRoY9pan+7rR1SGoGcm1039A9tWy0w2oS+s7GmNJqqBHJqN2JZUlvJrqjU4ZdyoZ1/DXx9zuzUQDzgiEBFJ2xw==
|
||||
gatsby-plugin-manifest@4.9.1:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-manifest/-/gatsby-plugin-manifest-4.9.1.tgz#20513c7d942b424795b802506893c0f909c69b7a"
|
||||
integrity sha512-Fye2vr7ioc7ETVKdCfpbc5ByU28+EB7ocqSORbazPgAT8OiPazpaBAYm98BONceuK3WaxGoEXMsmwmNBIIPjRA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
gatsby-plugin-utils "^3.3.0"
|
||||
semver "^7.3.5"
|
||||
sharp "^0.30.1"
|
||||
|
||||
gatsby-plugin-page-creator@^4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-4.9.0.tgz#b0ddff77698f2d33f4789e0396e7788a2930fd70"
|
||||
integrity sha512-eryfrvg/d2L4oL6VR6FHQKX1gkRuVkqHe6gTLRBBk4B2+2x5UoxxSOSSxXlsTByFW0K3vyzD2nPpdeE08ArvEQ==
|
||||
gatsby-plugin-page-creator@^4.9.1:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-4.9.1.tgz#d73b6af0d26a19a25ddece1ea2276149f0ee2c44"
|
||||
integrity sha512-06EA9nd+LxZxtxTsr6G8xYuaMCDZN2z0qEHX8TvQXcgFVktFB18nUISjfeMBTdiyM1zeVxMCWffBarbUG6IMGA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
"@babel/traverse" "^7.15.4"
|
||||
"@sindresorhus/slugify" "^1.1.2"
|
||||
chokidar "^3.5.2"
|
||||
fs-exists-cached "^1.0.0"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-page-utils "^2.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
gatsby-page-utils "^2.9.1"
|
||||
gatsby-plugin-utils "^3.3.0"
|
||||
gatsby-telemetry "^3.9.0"
|
||||
gatsby-telemetry "^3.9.1"
|
||||
globby "^11.0.4"
|
||||
lodash "^4.17.21"
|
||||
|
||||
|
@ -6350,10 +6370,10 @@ gatsby-plugin-robots-txt@1.7.0:
|
|||
"@babel/runtime" "^7.16.7"
|
||||
generate-robotstxt "^8.0.3"
|
||||
|
||||
gatsby-plugin-sharp@4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-sharp/-/gatsby-plugin-sharp-4.9.0.tgz#c1370d00d55e025d2e5d1d9b3017865ea9d890c7"
|
||||
integrity sha512-65JcqL11kyecTDYl3uZ/SvBI2FRJgSC7JTxzoV1ZOJkNQpn4nRLRUFL5UTijnnwU9+tba3i6gxKtZEAlE94pcA==
|
||||
gatsby-plugin-sharp@4.9.1:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-sharp/-/gatsby-plugin-sharp-4.9.1.tgz#44667f134be1855fe666ed58839bd280527337bd"
|
||||
integrity sha512-oHnuxIok0Ct3nktn53XQFX36QXwa4H9hjj5lkxaY3zh0giYJmFAsHyvus6DKzGQ14cTC3AkvaD+rqv4SGdjRcg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
async "^3.2.3"
|
||||
|
@ -6361,9 +6381,9 @@ gatsby-plugin-sharp@4.9.0:
|
|||
debug "^4.3.3"
|
||||
filenamify "^4.3.0"
|
||||
fs-extra "^10.0.0"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
gatsby-plugin-utils "^3.3.0"
|
||||
gatsby-telemetry "^3.9.0"
|
||||
gatsby-telemetry "^3.9.1"
|
||||
got "^11.8.3"
|
||||
lodash "^4.17.21"
|
||||
mini-svg-data-uri "^1.4.3"
|
||||
|
@ -6390,10 +6410,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"
|
||||
integrity sha512-oALTh6VwO6l3khgC/vGr706aqt38EkXwdr6iXVei/auOKGxpCLEuDCQVal1a4SpYXdjHjRsEyab6bxaHL2lzsA==
|
||||
|
||||
gatsby-plugin-typescript@^4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-typescript/-/gatsby-plugin-typescript-4.9.0.tgz#e81031863b72c4471292759923325761a1e23c3a"
|
||||
integrity sha512-kY1kV5zGaPRbRAyHtbpPhxg/aW+6wqNhe6kA+H6EWCE+JcNZkKojyoIe+tKq3+ddq4ZE7BEDXU97N6t4WJFBvg==
|
||||
gatsby-plugin-typescript@^4.9.1:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-typescript/-/gatsby-plugin-typescript-4.9.1.tgz#343f0cb6c4e72115875c264c127b293f8e1915a5"
|
||||
integrity sha512-VYkosDqk4CLDz11snEdSIBSW/RAPi8eXD4fHyicuFx5dh11BGi7TMUzVVmwvYWHHleQdvboC4qYlDrzXXV++zw==
|
||||
dependencies:
|
||||
"@babel/core" "^7.15.5"
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5"
|
||||
|
@ -6401,7 +6421,7 @@ gatsby-plugin-typescript@^4.9.0:
|
|||
"@babel/plugin-proposal-optional-chaining" "^7.14.5"
|
||||
"@babel/preset-typescript" "^7.15.0"
|
||||
"@babel/runtime" "^7.15.4"
|
||||
babel-plugin-remove-graphql-queries "^4.9.0"
|
||||
babel-plugin-remove-graphql-queries "^4.9.1"
|
||||
|
||||
gatsby-plugin-utils@^3.3.0:
|
||||
version "3.3.0"
|
||||
|
@ -6427,16 +6447,16 @@ gatsby-sharp@^0.3.0:
|
|||
"@types/sharp" "^0.29.5"
|
||||
sharp "^0.30.1"
|
||||
|
||||
gatsby-source-filesystem@4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-source-filesystem/-/gatsby-source-filesystem-4.9.0.tgz#8cf6f3f67cc97f8a75e284814f444a0eea3263e6"
|
||||
integrity sha512-woSxEgYeZSVZSpxm+FwB+RjRIyhcix1AR9766W4yk5RwYH2wciF2OfxRC73WW/o/v1ztzeW6RoqIIY+GBXaA1A==
|
||||
gatsby-source-filesystem@4.9.1:
|
||||
version "4.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-source-filesystem/-/gatsby-source-filesystem-4.9.1.tgz#e619d8a482b0477c28225ffce9c28cbb0606ce67"
|
||||
integrity sha512-2HS9+5i+F7tRgxBiv8Op9xK/jvd5DGUfedFsJ6/6sfoXUBddowvW4rVEj4XO42TsIQJe7eVj7FfzfqzSqQN8ow==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
chokidar "^3.5.2"
|
||||
file-type "^16.5.3"
|
||||
fs-extra "^10.0.0"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
got "^9.6.0"
|
||||
md5-file "^5.0.0"
|
||||
mime "^2.5.2"
|
||||
|
@ -6445,10 +6465,10 @@ gatsby-source-filesystem@4.9.0:
|
|||
valid-url "^1.0.9"
|
||||
xstate "^4.26.1"
|
||||
|
||||
gatsby-telemetry@^3.9.0:
|
||||
version "3.9.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-telemetry/-/gatsby-telemetry-3.9.0.tgz#d438841a6351ab1fd02c71244cdf78350694f070"
|
||||
integrity sha512-ifqJ4KS16mbpfZ5oVaU4WEbk6gccivVqjCbzfVGgqtl+C8B0u1CeShvr4NcJE1FdVFYIOB4uJeV9Wym03B075A==
|
||||
gatsby-telemetry@^3.9.1:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-telemetry/-/gatsby-telemetry-3.9.1.tgz#3c20c7e0cb363ccaae41fb581ab289330b5d7f69"
|
||||
integrity sha512-ChXTshfvo5njd/u6kSZErDUvc/uSmtOEuo7wrt/68Xjz2JVG6nsLlRxaZpx0DxnDAInouItMVX0VF40RAU7qKg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.14.0"
|
||||
"@babel/runtime" "^7.15.4"
|
||||
|
@ -6458,7 +6478,7 @@ gatsby-telemetry@^3.9.0:
|
|||
boxen "^4.2.0"
|
||||
configstore "^5.0.1"
|
||||
fs-extra "^10.0.0"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
git-up "^4.0.5"
|
||||
is-docker "^2.2.1"
|
||||
lodash "^4.17.21"
|
||||
|
@ -6496,10 +6516,10 @@ gatsby-worker@^1.9.0:
|
|||
"@babel/core" "^7.15.5"
|
||||
"@babel/runtime" "^7.15.4"
|
||||
|
||||
gatsby@4.9.2:
|
||||
version "4.9.2"
|
||||
resolved "https://registry.yarnpkg.com/gatsby/-/gatsby-4.9.2.tgz#2a31f8fe8b5007ecfed67b78af27af0587fee817"
|
||||
integrity sha512-dYUcCLZbyRIuzaswqxSzYFSGs+o1rdJFqZb6AODnq9jsMeDjN2jRC4DieDLzrWwDJCerW3rupDrHWsNfDj68Mw==
|
||||
gatsby@4.9.3:
|
||||
version "4.9.3"
|
||||
resolved "https://registry.yarnpkg.com/gatsby/-/gatsby-4.9.3.tgz#a69a05a47048b6d140c89cfc05c3cb0e24f8a7aa"
|
||||
integrity sha512-XZFmdW30vm1+s/kSxFFhMVl33u2qesWPdLEFtrQgtAnFiVjI/ukS/95gVOilhIMYyiTtuhQXiBKygkTl08oKFw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.14.0"
|
||||
"@babel/core" "^7.15.5"
|
||||
|
@ -6526,8 +6546,8 @@ gatsby@4.9.2:
|
|||
babel-plugin-add-module-exports "^1.0.4"
|
||||
babel-plugin-dynamic-import-node "^2.3.3"
|
||||
babel-plugin-lodash "^3.3.4"
|
||||
babel-plugin-remove-graphql-queries "^4.9.0"
|
||||
babel-preset-gatsby "^2.9.0"
|
||||
babel-plugin-remove-graphql-queries "^4.9.1"
|
||||
babel-preset-gatsby "^2.9.1"
|
||||
better-opn "^2.1.1"
|
||||
bluebird "^3.7.2"
|
||||
body-parser "^1.19.0"
|
||||
|
@ -6570,18 +6590,18 @@ gatsby@4.9.2:
|
|||
find-cache-dir "^3.3.2"
|
||||
fs-exists-cached "1.0.0"
|
||||
fs-extra "^10.0.0"
|
||||
gatsby-cli "^4.9.0"
|
||||
gatsby-core-utils "^3.9.0"
|
||||
gatsby-cli "^4.9.1"
|
||||
gatsby-core-utils "^3.9.1"
|
||||
gatsby-graphiql-explorer "^2.9.0"
|
||||
gatsby-legacy-polyfills "^2.9.0"
|
||||
gatsby-link "^4.9.0"
|
||||
gatsby-page-utils "^2.9.0"
|
||||
gatsby-link "^4.9.1"
|
||||
gatsby-page-utils "^2.9.1"
|
||||
gatsby-parcel-config "^0.0.1"
|
||||
gatsby-plugin-page-creator "^4.9.0"
|
||||
gatsby-plugin-typescript "^4.9.0"
|
||||
gatsby-plugin-page-creator "^4.9.1"
|
||||
gatsby-plugin-typescript "^4.9.1"
|
||||
gatsby-plugin-utils "^3.3.0"
|
||||
gatsby-react-router-scroll "^5.9.0"
|
||||
gatsby-telemetry "^3.9.0"
|
||||
gatsby-telemetry "^3.9.1"
|
||||
gatsby-worker "^1.9.0"
|
||||
glob "^7.2.0"
|
||||
got "^11.8.2"
|
||||
|
@ -10960,12 +10980,14 @@ sjcl@^1.0.8:
|
|||
resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a"
|
||||
integrity sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==
|
||||
|
||||
skynet-js@4.0.23-beta:
|
||||
version "4.0.23-beta"
|
||||
resolved "https://registry.yarnpkg.com/skynet-js/-/skynet-js-4.0.23-beta.tgz#bfc8c6fa477f927d40213ec529392930edd04600"
|
||||
integrity sha512-5Hd8aOZM85sIjBT065Zf+korhQvCbA3Shm4DJnQT1kPlwvj+C+ZPPt/dwx1buzvDvfPmpXOhd377NskXzrFeDA==
|
||||
skynet-js@4.0.26-beta:
|
||||
version "4.0.26-beta"
|
||||
resolved "https://registry.yarnpkg.com/skynet-js/-/skynet-js-4.0.26-beta.tgz#5b6e924a0efa5fd6ee2c00760e1d4ce92d1ba0a9"
|
||||
integrity sha512-YPqjNyqL6AhS9jMLyJ5PoilDZ7f2YFrqqhXUnzLBrjmWxICxcDeRu2GJh9MGCJUZ2Cv35IlG1ch4eiqFbs1wqA==
|
||||
dependencies:
|
||||
axios "^0.24.0"
|
||||
"@skynetlabs/tus-js-client" "^2.3.0"
|
||||
async-mutex "^0.3.2"
|
||||
axios "^0.26.0"
|
||||
base32-decode "^1.0.0"
|
||||
base32-encode "^1.1.1"
|
||||
base64-js "^1.3.1"
|
||||
|
@ -10977,7 +10999,6 @@ skynet-js@4.0.23-beta:
|
|||
randombytes "^2.1.0"
|
||||
sjcl "^1.0.8"
|
||||
skynet-mysky-utils "^0.3.0"
|
||||
tus-js-client "^2.2.0"
|
||||
tweetnacl "^1.0.3"
|
||||
url-join "^4.0.1"
|
||||
url-parse "^1.5.1"
|
||||
|
@ -11881,7 +11902,7 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2, tslib@^2.0.3, tslib@^2.1.0, tslib@~2.3.0:
|
||||
tslib@^2, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@~2.3.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
@ -11915,19 +11936,6 @@ tunnel-agent@^0.6.0:
|
|||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tus-js-client@^2.2.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tus-js-client/-/tus-js-client-2.3.1.tgz#cee9dcc9dbf3a7d9c1f8ca102ec5d75317465e36"
|
||||
integrity sha512-QEM7ySnthWT+wwePLTXVSQP8vBLCy0ZoJNDGFzNlsU+YVoK2WevIZwcRnKyo962xhYMiABe3aMvXvk4Ln+VRzQ==
|
||||
dependencies:
|
||||
buffer-from "^0.1.1"
|
||||
combine-errors "^3.0.3"
|
||||
is-stream "^2.0.0"
|
||||
js-base64 "^2.6.1"
|
||||
lodash.throttle "^4.1.1"
|
||||
proper-lockfile "^2.0.1"
|
||||
url-parse "^1.5.7"
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
@ -12157,7 +12165,7 @@ url-parse-lax@^3.0.0:
|
|||
dependencies:
|
||||
prepend-http "^2.0.0"
|
||||
|
||||
url-parse@^1.5.1, url-parse@^1.5.7:
|
||||
url-parse@^1.4.3, url-parse@^1.5.1:
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
|
||||
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
|
||||
|
|
|
@ -29,12 +29,6 @@ the health check.
|
|||
The `portal-upgrade.sh` script upgrades the docker images for a portal and
|
||||
clears and leftover images.
|
||||
|
||||
**nginx-prune.sh**\
|
||||
The `nginx-prune.sh` script deletes all entries from nginx cache larger than
|
||||
the given size and smaller entries until nginx cache disk size is smaller than
|
||||
the given cache size limit. Both values are configured in
|
||||
`lib/nginx-prune-cache-subscript.sh`. The script doesn't require `sudo`.
|
||||
|
||||
## Webportal Upgrade Procedures
|
||||
|
||||
TODO...
|
||||
|
|
|
@ -34,18 +34,16 @@ else
|
|||
skylinks=("$1") # just single skylink passed as input argument
|
||||
fi
|
||||
|
||||
# get local nginx ip adress
|
||||
nginx_ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx)
|
||||
# get local skyd ip adress
|
||||
ipaddress=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' sia)
|
||||
|
||||
# get sia api password either from env variable if exists or from apipassword file in sia-data directory
|
||||
apipassword=$(docker exec sia sh -c '[ ! -z "${SIA_API_PASSWORD}" ] && echo ${SIA_API_PASSWORD} || $(cat /sia-data/apipassword | tr -d '\n')')
|
||||
|
||||
# iterate over provided skylinks and block them one by one
|
||||
for skylink in "${skylinks[@]}"; do
|
||||
printf "Blocking ${skylink} ... "
|
||||
status_code=$(curl --write-out '%{http_code}' --silent --output /dev/null --data "{\"add\":[\"$skylink\"]}" "http://${nginx_ip}:8000/skynet/blocklist")
|
||||
echo "> Blocking ${skylink} ... "
|
||||
|
||||
# print blocklist response status code
|
||||
if [ $status_code = "204" ]; then
|
||||
echo "done"
|
||||
else
|
||||
echo "error $status_code"
|
||||
fi
|
||||
# POST /skynet/blocklist always returns 200 and in case of failure print error message
|
||||
curl -A Sia-Agent -u "":${apipassword} --data "{\"add\":[\"$skylink\"]}" "http://${ipaddress}:9980/skynet/blocklist"
|
||||
done
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/local/bin/bash
|
||||
|
||||
# This subscript is expected to be run inside docker container using 'bash'
|
||||
# image. The image is based on Alpine Linux. It's tools (find, stat, awk, sort)
|
||||
# are non-standard versions from BusyBox.
|
||||
|
||||
MAX_CACHE_DIR_SIZE=20000000000
|
||||
MAX_KEEP_FILE_SIZE=1000000000
|
||||
|
||||
total=0
|
||||
|
||||
# We sort files by time, newest files are first. Format is:
|
||||
# time (last modification as seconds since Epoch), filepath, size (bytes)
|
||||
find /home/user/skynet-webportal/docker/data/nginx/cache -type f -exec stat -c "%Y %n %s" {} + | sort -rgk1 | while read line
|
||||
do
|
||||
size=$(echo $line | awk '{print $3}')
|
||||
new_total=$(($total + $size))
|
||||
|
||||
# We always delete all files larger than MAX_KEEP_FILE_SIZE.
|
||||
# We keep all files smaller than MAX_KEEP_FILE_SIZE when cache size is
|
||||
# below MAX_CACHE_DIR_SIZE, then we delete also smaller files.
|
||||
if (("$size" <= "$MAX_KEEP_FILE_SIZE" && "$new_total" < "$MAX_CACHE_DIR_SIZE"))
|
||||
then
|
||||
total=$new_total
|
||||
continue
|
||||
fi
|
||||
|
||||
filename=$(echo $line | awk '{print $2}')
|
||||
rm $filename
|
||||
done
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# We execute the nginx cache pruning subscript from docker container so that we
|
||||
# can run the pruning script in user crontab without sudo.
|
||||
docker run --rm -v /home/user:/home/user bash /home/user/skynet-webportal/scripts/lib/nginx-prune-cache-subscript.sh
|
||||
|
||||
# Some cache files are deleted, but are kept open, we hot reload nginx to get
|
||||
# them closed and removed from filesystem.
|
||||
docker exec nginx nginx -s reload
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from bot_utils import setup, send_msg
|
||||
from bot_utils import get_api_password, setup, send_msg
|
||||
from random import randint
|
||||
from time import sleep
|
||||
|
||||
|
@ -11,6 +11,8 @@ import asyncio
|
|||
import requests
|
||||
import json
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
setup()
|
||||
|
||||
|
||||
|
@ -38,14 +40,14 @@ def exec(command):
|
|||
|
||||
|
||||
async def block_skylinks_from_airtable():
|
||||
# Get nginx's IP before doing anything else. If this step fails we don't
|
||||
# Get sia IP before doing anything else. If this step fails we don't
|
||||
# need to continue with the execution of the script.
|
||||
ipaddress = exec(
|
||||
"docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx"
|
||||
"docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' sia"
|
||||
)
|
||||
|
||||
if ipaddress == "":
|
||||
print("Nginx's IP could not be detected. Exiting.")
|
||||
print("Skyd IP could not be detected. Exiting.")
|
||||
return
|
||||
|
||||
print("Pulling blocked skylinks from Airtable via api integration")
|
||||
|
@ -117,11 +119,13 @@ async def block_skylinks_from_airtable():
|
|||
print(
|
||||
"Sending /skynet/blocklist request with "
|
||||
+ str(len(skylinks))
|
||||
+ " skylinks to siad through nginx"
|
||||
+ " skylinks to siad"
|
||||
)
|
||||
response = requests.post(
|
||||
"http://" + ipaddress + ":8000/skynet/blocklist",
|
||||
"http://" + ipaddress + ":9980/skynet/blocklist",
|
||||
data=json.dumps({"add": skylinks}),
|
||||
headers={"User-Agent": "Sia-Agent"},
|
||||
auth=HTTPBasicAuth("", get_api_password()),
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
|
@ -153,5 +157,5 @@ loop.run_until_complete(run_checks())
|
|||
|
||||
# --- BASH EQUIVALENT
|
||||
# skylinks=$(curl "https://api.airtable.com/v0/${AIRTABLE_BASE}/${AIRTABLE_TABLE}?fields%5B%5D=${AIRTABLE_FIELD}" -H "Authorization: Bearer ${AIRTABLE_KEY}" | python3 -c "import sys, json; print('[\"' + '\",\"'.join([entry['fields']['Link'] for entry in json.load(sys.stdin)['records']]) + '\"]')")
|
||||
# ipaddress=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx)
|
||||
# ipaddress=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' sia)
|
||||
# curl --data "{\"add\" : ${skylinks}}" "${ipaddress}:8000/skynet/blocklist"
|
||||
|
|
|
@ -8,3 +8,4 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPM43lzbKjFLChe5rKETxDpWpNlqXCGTBPiWlDN2vlLD
|
|||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN6Kcx8yetova4/ALUQHigo/PBMJO33ZTKOsg2jxSO2a user@deploy.siasky.dev
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDcenWnMQ6q/OEC4ZmQgjLDV2obWlR3fENV0zRGFvJF+ marcins@siasky.net
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB7prtVOTwtcSN9HkXum107RwcW5H8Vggx6Qv7T57ItT daniel@siasky.net
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII85HxoIRbPyr+xUjpuFUlQNW7smCNdIcmx2XgpmXnB0 marissa@skynetlabs.com
|
||||
|
|
|
@ -5,4 +5,3 @@
|
|||
44 5 * * * /home/user/skynet-webportal/scripts/backup-aws-s3.sh 1>>/home/user/skynet-webportal/logs/backup-aws-s3.log 2>>/home/user/skynet-webportal/logs/backup-aws-s3.log
|
||||
6 13 * * * /home/user/skynet-webportal/scripts/db_backup.sh 1>>/home/user/skynet-webportal/logs/db_backup.log 2>>/home/user/skynet-webportal/logs/db_backup.log
|
||||
0 5 * * * /home/user/skynet-webportal/scripts/es_cleaner.py 1 http://localhost:9200
|
||||
15 * * * * /home/user/skynet-webportal/scripts/nginx-prune.sh
|
||||
|
|
Reference in New Issue