Merge branch 'master' into ivo/nginx_passes_apikey

This commit is contained in:
Karol Wypchło 2022-03-18 15:27:36 +01:00 committed by GitHub
commit f47bd3f9a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1080 additions and 565 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,4 +1,3 @@
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;

View File

@ -1,6 +1,4 @@
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
@ -9,62 +7,10 @@ limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
# this is important because we want only one format in cache keys and logs
set_by_lua_block $skylink { return require("skynet.skylink").parse(ngx.var.skylink) }
# $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 '';
# 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 {
@ -355,7 +357,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

@ -38,8 +38,6 @@ location / {
end
ngx.var.skylink = require("skynet.skylink").parse(ngx.var.skylink)
ngx.var.skylink_v1 = ngx.var.skylink
ngx.var.skylink_v2 = 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

@ -72,13 +72,13 @@ function _M.get_account_limits()
local httpc = require("resty.http").new()
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits", {
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits?unit=byte", {
headers = auth_headers,
})
-- 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

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

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

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

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

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

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