Merge branch 'master' into improve-subscription-only-portal-communication

This commit is contained in:
Karol Wypchło 2022-03-21 13:20:00 +01:00 committed by GitHub
commit 5ede788667
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 1292 additions and 725 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
@ -266,10 +269,10 @@ location /skynet/tus {
if require("skynet.account").is_access_forbidden() then
return require("skynet.account").exit_access_forbidden()
end
-- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
-- apply upload size limits
ngx.req.set_header("SkynetMaxUploadSize", limits.maxUploadSize)
end
@ -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

View File

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

View File

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

View File

@ -43,15 +43,15 @@ function _M.get_account_limits()
if ngx.var.account_limits == "" then
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}
</DropdownLink>
</>
<DropdownLink {...props}>
{Icon ? <Icon /> : null}
<span className="text-palette-500">{label}</span>
</DropdownLink>
);
DropdownMenuLink.propTypes = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import { createContext } from "react";
export const PlansContext = createContext({
plans: [],
limits: [],
error: null,
});

View File

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

View File

@ -0,0 +1,2 @@
export * from "./PlansProvider";
export * from "./usePlans";

View File

@ -0,0 +1,5 @@
import { useContext } from "react";
import { PlansContext } from "./PlansContext";
export const usePlans = () => useContext(PlansContext);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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