Merge branch 'master' into portal-latest

This commit is contained in:
Filip Rysavy 2022-01-12 14:18:58 +01:00
commit 0fd93c479b
No known key found for this signature in database
GPG Key ID: EA1F430401C92D99
24 changed files with 273 additions and 164 deletions

View File

@ -1,7 +1,6 @@
version: "3.7" version: "3.7"
x-logging: x-logging: &default-logging
&default-logging
driver: json-file driver: json-file
options: options:
max-size: "10m" max-size: "10m"
@ -11,6 +10,7 @@ services:
nginx: nginx:
environment: environment:
- ACCOUNTS_ENABLED=true - ACCOUNTS_ENABLED=true
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only
depends_on: depends_on:
- accounts - accounts

View File

@ -25,10 +25,10 @@ services:
logging: *default-logging logging: *default-logging
environment: environment:
- SIA_MODULES=gctwra - SIA_MODULES=gctwra
- SKYD_DISK_CACHE_ENABLED=false - SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-false}
- SKYD_DISK_CACHE_SIZE=53690000000 # 50GB - SKYD_DISK_CACHE_SIZE=${SKYD_DISK_CACHE_SIZE:-53690000000} # 50GB
- SKYD_DISK_CACHE_MIN_HITS=3 - SKYD_DISK_CACHE_MIN_HITS=${SKYD_DISK_CACHE_MIN_HITS:-3}
- SKYD_DISK_CACHE_HIT_PERIOD=3600 # 1h - SKYD_DISK_CACHE_HIT_PERIOD=${SKYD_DISK_CACHE_HIT_PERIOD:-3600} # 1h
env_file: env_file:
- .env - .env
@ -66,7 +66,6 @@ services:
env_file: env_file:
- .env - .env
volumes: volumes:
- ./docker/nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
- ./docker/data/nginx/cache:/data/nginx/cache - ./docker/data/nginx/cache:/data/nginx/cache
- ./docker/data/nginx/blocker:/data/nginx/blocker - ./docker/data/nginx/blocker:/data/nginx/blocker
- ./docker/data/nginx/logs:/usr/local/openresty/nginx/logs - ./docker/data/nginx/logs:/usr/local/openresty/nginx/logs

View File

@ -11,6 +11,7 @@ COPY mo ./
COPY libs /etc/nginx/libs COPY libs /etc/nginx/libs
COPY conf.d /etc/nginx/conf.d COPY conf.d /etc/nginx/conf.d
COPY conf.d.templates /etc/nginx/conf.d.templates COPY conf.d.templates /etc/nginx/conf.d.templates
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
CMD [ "bash", "-c", \ CMD [ "bash", "-c", \
"./mo < /etc/nginx/conf.d.templates/server.account.conf > /etc/nginx/conf.d/server.account.conf ; \ "./mo < /etc/nginx/conf.d.templates/server.account.conf > /etc/nginx/conf.d/server.account.conf ; \

View File

@ -3,10 +3,16 @@
# because otherwise logger with throw error # because otherwise logger with throw error
# set only on hns routes # set only on hns routes
set $hns_domain ''; set $hns_domain "";
# set only if server has been access through SERVER_DOMAIN # set only if server has been access through SERVER_DOMAIN
set $server_alias ''; set $server_alias "";
# expose skylink variable so we can use it in access log # expose skylink variable so we can use it in access log
set $skylink ''; set $skylink "";
# cached account limits (json string) - applies only if accounts are enabled
set $account_limits "";
# set this internal flag to true if current request should not be limited in any way
set $internal_no_limits "false";

View File

@ -1,11 +1,12 @@
include /etc/nginx/conf.d/include/proxy-buffer; include /etc/nginx/conf.d/include/proxy-buffer;
include /etc/nginx/conf.d/include/proxy-pass-internal; 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 # 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 raw 46 bit 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 # resolve handshake domain by requesting to /hnsres endpoint and assign correct values to $skylink and $rest
access_by_lua_block { rewrite_by_lua_block {
local json = require('cjson') local json = require('cjson')
local httpc = require("resty.http").new() local httpc = require("resty.http").new()

View File

@ -53,10 +53,7 @@ access_by_lua_block {
-- check if skylink v1 is present on blocklist (compare hashes) -- check if skylink v1 is present on blocklist (compare hashes)
if require("skynet.blocklist").is_blocked(ngx.var.skylink_v1) then if require("skynet.blocklist").is_blocked(ngx.var.skylink_v1) then
ngx.status = ngx.HTTP_ILLEGAL return require("skynet.blocklist").exit_illegal()
ngx.header["content-type"] = "text/plain"
ngx.say("Unavailable For Legal Reasons")
return ngx.exit(ngx.status)
end end
-- if skylink is found on nocache list then set internal nocache variable -- if skylink is found on nocache list then set internal nocache variable
@ -65,21 +62,16 @@ access_by_lua_block {
ngx.var.nocache = "1" ngx.var.nocache = "1"
end end
-- this block runs only when accounts are enabled if require("skynet.account").accounts_enabled() then
if not os.getenv("PORTAL_MODULES"):match("a") then return end -- check if portal is in authenticated only mode
if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
end
-- 10.10.10.70 points to accounts service (alias not available when using resty-http) -- get account limits of currently authenticated user
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits", { local limits = require("skynet.account").get_account_limits()
headers = { ["Cookie"] = "skynet-jwt=" .. ngx.var.skynet_jwt }
}) -- apply download speed limit
-- 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.var.limit_rate = 2621440 -- (20 * 1024 * 1024 / 8) conservative fallback to 20 mbps in case accounts failed to return limits
elseif res and res.status == ngx.HTTP_OK then
local json = require('cjson')
local limits = json.decode(res.body)
ngx.var.limit_rate = limits.download ngx.var.limit_rate = limits.download
end end
} }
@ -97,7 +89,8 @@ header_filter_by_lua_block {
end end
-- add skylink to nocache list if it exceeds 1GB (1e+9 bytes) threshold -- add skylink to nocache list if it exceeds 1GB (1e+9 bytes) threshold
if tonumber(ngx.header["Content-Length"]) > 1e+9 then -- (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"]) ngx.shared.nocache:set(ngx.var.skylink_v1, ngx.header["Content-Length"])
end end
} }

View File

@ -10,22 +10,16 @@ proxy_read_timeout 600; # siad should timeout with 404 after 5 minutes
proxy_pass http://sia:9980/skynet/registry; proxy_pass http://sia:9980/skynet/registry;
access_by_lua_block { access_by_lua_block {
-- this block runs only when accounts are enabled if require("skynet.account").accounts_enabled() then
if not os.getenv("PORTAL_MODULES"):match("a") then return end -- check if portal is in authenticated only mode
if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
end
local httpc = require("resty.http").new() -- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
-- 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", { -- apply registry rate limits (forced delay)
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))
elseif res and res.status == ngx.HTTP_OK then
local json = require('cjson')
local limits = json.decode(res.body)
if limits.registry > 0 then if limits.registry > 0 then
ngx.sleep(limits.registry / 1000) ngx.sleep(limits.registry / 1000)
end end

View File

@ -0,0 +1,6 @@
access_by_lua_block {
-- check portal access rules and exit if access is restricted
if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
end
}

View File

@ -1,7 +1,7 @@
# register the download in accounts service (cookies should contain jwt) # register the download in accounts service (cookies should contain jwt)
log_by_lua_block { log_by_lua_block {
-- this block runs only when accounts are enabled -- this block runs only when accounts are enabled
if os.getenv("PORTAL_MODULES"):match("a") then if require("skynet.account").accounts_enabled() then
local function track(premature, skylink, status, body_bytes_sent, jwt) local function track(premature, skylink, status, body_bytes_sent, jwt)
if premature then return end if premature then return end

View File

@ -1,7 +1,7 @@
# register the registry access in accounts service (cookies should contain jwt) # register the registry access in accounts service (cookies should contain jwt)
log_by_lua_block { log_by_lua_block {
-- this block runs only when accounts are enabled -- this block runs only when accounts are enabled
if os.getenv("PORTAL_MODULES"):match("a") then if require("skynet.account").accounts_enabled() then
local function track(premature, request_method, jwt) local function track(premature, request_method, jwt)
if premature then return end if premature then return end

View File

@ -1,7 +1,7 @@
# register the upload in accounts service (cookies should contain jwt) # register the upload in accounts service (cookies should contain jwt)
log_by_lua_block { log_by_lua_block {
-- this block runs only when accounts are enabled -- this block runs only when accounts are enabled
if os.getenv("PORTAL_MODULES"):match("a") then if require("skynet.account").accounts_enabled() then
local function track(premature, skylink, jwt) local function track(premature, skylink, jwt)
if premature then return end if premature then return end

View File

@ -28,6 +28,7 @@ location / {
set $skylink "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30"; set $skylink "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30";
set $path $uri; set $path $uri;
set $internal_no_limits "true";
include /etc/nginx/conf.d/include/location-skylink; include /etc/nginx/conf.d/include/location-skylink;
@ -117,6 +118,8 @@ location /abuse/report {
} }
location /hns { location /hns {
include /etc/nginx/conf.d/include/cors;
# match the request_uri and extract the hns domain and anything that is passed in the uri after it # 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: # example: /hns/something/foo/bar matches:
# > hns_domain: something # > hns_domain: something
@ -130,6 +133,7 @@ location /hns {
location /hnsres { location /hnsres {
include /etc/nginx/conf.d/include/cors; include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/portal-access-check;
proxy_pass http://handshake-api:3100; proxy_pass http://handshake-api:3100;
} }
@ -141,6 +145,7 @@ location /skynet/registry {
location /skynet/restore { location /skynet/restore {
include /etc/nginx/conf.d/include/cors; include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/sia-auth; include /etc/nginx/conf.d/include/sia-auth;
include /etc/nginx/conf.d/include/portal-access-check;
client_max_body_size 5M; client_max_body_size 5M;
@ -197,6 +202,7 @@ location /skynet/skyfile {
include /etc/nginx/conf.d/include/sia-auth; include /etc/nginx/conf.d/include/sia-auth;
include /etc/nginx/conf.d/include/track-upload; include /etc/nginx/conf.d/include/track-upload;
include /etc/nginx/conf.d/include/generate-siapath; include /etc/nginx/conf.d/include/generate-siapath;
include /etc/nginx/conf.d/include/portal-access-check;
limit_req zone=uploads_by_ip burst=10 nodelay; limit_req zone=uploads_by_ip burst=10 nodelay;
limit_req zone=uploads_by_ip_throttled; limit_req zone=uploads_by_ip_throttled;
@ -214,19 +220,6 @@ location /skynet/skyfile {
proxy_set_header Expect $http_expect; proxy_set_header Expect $http_expect;
proxy_set_header User-Agent: Sia-Agent; proxy_set_header User-Agent: Sia-Agent;
# access_by_lua_block {
# -- this block runs only when accounts are enabled
# if not os.getenv("PORTAL_MODULES"):match("a") then return end
# ngx.var.upload_limit_rate = 5 * 1024 * 1024
# local res = ngx.location.capture("/accounts/user", { copy_all_vars = true })
# if res.status == ngx.HTTP_OK then
# local json = require('cjson')
# local user = json.decode(res.body)
# ngx.var.upload_limit_rate = ngx.var.upload_limit_rate * (user.tier + 1)
# end
# }
# proxy this call to siad endpoint (make sure the ip is correct) # proxy this call to siad endpoint (make sure the ip is correct)
proxy_pass http://sia:9980/skynet/skyfile/$dir1/$dir2/$dir3$is_args$args; proxy_pass http://sia:9980/skynet/skyfile/$dir1/$dir2/$dir3$is_args$args;
} }
@ -261,27 +254,17 @@ location /skynet/tus {
# proxy /skynet/tus requests to siad endpoint with all arguments # proxy /skynet/tus requests to siad endpoint with all arguments
proxy_pass http://sia:9980; proxy_pass http://sia:9980;
# set max upload size dynamically based on account limits access_by_lua_block {
rewrite_by_lua_block { if require("skynet.account").accounts_enabled() then
-- set default limit value to 5 GB -- check if portal is in authenticated only mode
ngx.req.set_header("SkynetMaxUploadSize", 5368709120) if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
-- this block runs only when accounts are enabled end
if not os.getenv("PORTAL_MODULES"):match("a") then return end
-- get account limits of currently authenticated user
local httpc = require("resty.http").new() local limits = require("skynet.account").get_account_limits()
-- fetch account limits and set max upload size accordingly -- apply upload size limits
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits", {
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))
elseif res and res.status == ngx.HTTP_OK then
local json = require('cjson')
local limits = json.decode(res.body)
ngx.req.set_header("SkynetMaxUploadSize", limits.maxUploadSize) ngx.req.set_header("SkynetMaxUploadSize", limits.maxUploadSize)
end end
} }
@ -306,6 +289,7 @@ location /skynet/pin {
include /etc/nginx/conf.d/include/sia-auth; include /etc/nginx/conf.d/include/sia-auth;
include /etc/nginx/conf.d/include/track-upload; include /etc/nginx/conf.d/include/track-upload;
include /etc/nginx/conf.d/include/generate-siapath; include /etc/nginx/conf.d/include/generate-siapath;
include /etc/nginx/conf.d/include/portal-access-check;
limit_req zone=uploads_by_ip burst=10 nodelay; limit_req zone=uploads_by_ip burst=10 nodelay;
limit_req zone=uploads_by_ip_throttled; limit_req zone=uploads_by_ip_throttled;
@ -319,6 +303,7 @@ location /skynet/pin {
location /skynet/metadata { location /skynet/metadata {
include /etc/nginx/conf.d/include/cors; include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/portal-access-check;
header_filter_by_lua_block { header_filter_by_lua_block {
ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API") ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API")
@ -331,6 +316,7 @@ location /skynet/metadata {
location /skynet/resolve { location /skynet/resolve {
include /etc/nginx/conf.d/include/cors; include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/portal-access-check;
header_filter_by_lua_block { header_filter_by_lua_block {
ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API") ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API")
@ -357,7 +343,7 @@ location ~ "^/file/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
include /etc/nginx/conf.d/include/location-skylink; include /etc/nginx/conf.d/include/location-skylink;
} }
location /__internal/do/not/use/authenticated { location /__internal/do/not/use/accounts {
include /etc/nginx/conf.d/include/cors; include /etc/nginx/conf.d/include/cors;
charset utf-8; charset utf-8;
@ -366,29 +352,16 @@ location /__internal/do/not/use/authenticated {
content_by_lua_block { content_by_lua_block {
local json = require('cjson') local json = require('cjson')
local accounts_enabled = require("skynet.account").accounts_enabled()
local is_auth_required = require("skynet.account").is_auth_required()
local is_authenticated = accounts_enabled and require("skynet.account").is_authenticated()
-- this block runs only when accounts are enabled ngx.say(json.encode{
if not os.getenv("PORTAL_MODULES"):match("a") then enabled = accounts_enabled,
ngx.say(json.encode{authenticated = false}) auth_required = is_auth_required,
return ngx.exit(ngx.HTTP_OK) authenticated = is_authenticated,
end
local httpc = require("resty.http").new()
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
local res, err = httpc:request_uri("http://10.10.10.70:3000/user", {
headers = { ["Cookie"] = "skynet-jwt=" .. ngx.var.skynet_jwt }
}) })
return ngx.exit(ngx.HTTP_OK)
-- endpoint /user should return HTTP_OK for authenticated and HTTP_UNAUTHORIZED for not authenticated
if res and (res.status == ngx.HTTP_OK or res.status == ngx.HTTP_UNAUTHORIZED) then
ngx.say(json.encode{authenticated = res.status == ngx.HTTP_OK})
return ngx.exit(ngx.HTTP_OK)
else
ngx.log(ngx.ERR, "Failed accounts service request /user: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
ngx.say(json.encode{authenticated = false})
return ngx.exit(ngx.HTTP_OK)
end
} }
} }

View File

@ -0,0 +1,70 @@
local _M = {}
-- fallback - remember to keep those updated
local anon_limits = { ["tierName"] = "anonymous", ["upload"] = 655360, ["download"] = 655360, ["maxUploadSize"] = 1073741824, ["registry"] = 250 }
-- no limits applied
local no_limits = { ["tierName"] = "internal", ["upload"] = 0, ["download"] = 0, ["maxUploadSize"] = 0, ["registry"] = 0 }
-- handle request exit when access to portal should be restricted
-- currently handles only HTTP_UNAUTHORIZED but can be extended in future
function _M.exit_access_unauthorized(message)
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header["content-type"] = "text/plain"
ngx.say(message or "Portal operator restricted access to authenticated users only")
return ngx.exit(ngx.status)
end
function _M.accounts_enabled()
return os.getenv("PORTAL_MODULES"):match("a") ~= nil
end
function _M.get_account_limits()
local cjson = require('cjson')
if ngx.var.internal_no_limits == "true" then
return no_limits
end
if ngx.var.skynet_jwt == "" then
return anon_limits
end
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", {
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.var.account_limits = cjson.encode(anon_limits)
elseif res and res.status == ngx.HTTP_OK then
ngx.var.account_limits = res.body
end
end
return cjson.decode(ngx.var.account_limits)
end
-- detect whether current user is authenticated
function _M.is_authenticated()
local limits = _M.get_account_limits()
return limits.tierName ~= anon_limits.tierName
end
function _M.is_auth_required()
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "authenticated"
end
-- check whether access to portal should be restricted
-- based on the configurable environment variable
function _M.is_access_unauthorized()
return _M.accounts_enabled() and _M.is_auth_required() and not _M.is_authenticated()
end
return _M

View File

@ -52,4 +52,12 @@ function _M.is_blocked(skylink)
return ngx.shared.blocklist:get_stale(hash) == true return ngx.shared.blocklist:get_stale(hash) == true
end 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 return _M

View File

@ -1,4 +1,4 @@
local skynet_skylink = require("skynet/skylink") local skynet_skylink = require("skynet.skylink")
describe("parse", function() describe("parse", function()
local base32 = "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30" local base32 = "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30"

View File

@ -29,6 +29,7 @@ worker_processes auto;
env SKYNET_PORTAL_API; env SKYNET_PORTAL_API;
env SKYNET_SERVER_API; env SKYNET_SERVER_API;
env PORTAL_MODULES; env PORTAL_MODULES;
env ACCOUNTS_LIMIT_ACCESS;
events { events {
worker_connections 8192; worker_connections 8192;

View File

@ -78,7 +78,7 @@
"build": "gatsby build", "build": "gatsby build",
"develop": "cross-env GATSBY_API_URL=https://siasky.net gatsby develop", "develop": "cross-env GATSBY_API_URL=https://siasky.net gatsby develop",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"start": "yarn develop", "start": "gatsby develop",
"serve": "gatsby serve", "serve": "gatsby serve",
"clean": "gatsby clean", "clean": "gatsby clean",
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1", "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1",

View File

@ -2,7 +2,8 @@ import * as React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import Link from "../Link"; import Link from "../Link";
import classnames from "classnames"; import classnames from "classnames";
import useAuthenticatedStatus from "../../services/useAuthenticatedStatus"; import useAccounts from "../../services/useAccounts";
import useAccountsUrl from "../../services/useAccountsUrl";
import { LogoWhiteText, LogoBlackText, MenuMobile, MenuMobileClose, DiscordSmall } from "../Icons"; import { LogoWhiteText, LogoBlackText, MenuMobile, MenuMobileClose, DiscordSmall } from "../Icons";
import { useWindowSize, useWindowScroll } from "react-use"; import { useWindowSize, useWindowScroll } from "react-use";
@ -24,8 +25,8 @@ const Navigation = ({ mode, uri }) => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const isWindowTop = useWindowTop(); const isWindowTop = useWindowTop();
const { data: authenticationStatus } = useAuthenticatedStatus(); const { data: accounts } = useAccounts();
const authenticated = authenticationStatus?.authenticated ?? false; const accountsUrl = useAccountsUrl();
React.useEffect(() => { React.useEffect(() => {
setOpen(false); setOpen(false);
@ -40,6 +41,8 @@ const Navigation = ({ mode, uri }) => {
}, [open]); }, [open]);
const mobileMenuOffset = navRef.current ? navRef.current.offsetTop : 0; const mobileMenuOffset = navRef.current ? navRef.current.offsetTop : 0;
const showLoginNavigation = accounts?.enabled && !accounts?.authenticated;
const showAccountNavigation = accounts?.enabled && accounts?.authenticated;
return ( return (
<nav <nav
@ -85,20 +88,20 @@ const Navigation = ({ mode, uri }) => {
</Link> </Link>
))} ))}
{!authenticated && ( {showLoginNavigation && (
<Link href="https://account.siasky.net" className="button-link-primary"> <>
Log in <Link href={`${accountsUrl}/auth/login`} className="button-link-primary">
</Link> Log in
</Link>
<Link href={`${accountsUrl}/auth/registration`} className="button-primary">
Sign up
</Link>
</>
)} )}
{!authenticated && ( {showAccountNavigation && (
<Link href="https://account.siasky.net/auth/registration" className="button-primary"> <Link href={accountsUrl} className="button-primary">
Sign up
</Link>
)}
{authenticated && (
<Link href="https://account.siasky.net" className="button-primary">
My account My account
</Link> </Link>
)} )}
@ -139,20 +142,20 @@ const Navigation = ({ mode, uri }) => {
</div> </div>
<div className="pt-12 pb-8 border-t border-palette-500"> <div className="pt-12 pb-8 border-t border-palette-500">
<div className="flex items-center justify-center px-4 space-x-6"> <div className="flex items-center justify-center px-4 space-x-6">
{!authenticated && ( {showLoginNavigation && (
<Link href="https://account.siasky.net" className="button-secondary-light"> <>
Log in <Link href={`${accountsUrl}/auth/login`} className="button-secondary-light">
</Link> Log in
</Link>
<Link href={`${accountsUrl}/auth/registration`} className="button-primary">
Sign up
</Link>
</>
)} )}
{!authenticated && ( {showAccountNavigation && (
<Link href="https://account.siasky.net/auth/registration" className="button-primary"> <Link href={accountsUrl} className="button-primary">
Sign up
</Link>
)}
{authenticated && (
<Link href="https://account.siasky.net" className="button-primary">
My account My account
</Link> </Link>
)} )}

View File

@ -5,7 +5,8 @@ import classNames from "classnames";
import path from "path-browserify"; import path from "path-browserify";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import useAuthenticatedStatus from "../../services/useAuthenticatedStatus"; import useAccounts from "../../services/useAccounts";
import useAccountsUrl from "../../services/useAccountsUrl";
import Link from "../Link"; import Link from "../Link";
import UploaderElement from "./UploaderElement"; import UploaderElement from "./UploaderElement";
@ -20,21 +21,39 @@ const getRootDirectory = (file) => {
return path.normalize(dir).slice(root.length).split(path.sep)[0]; return path.normalize(dir).slice(root.length).split(path.sep)[0];
}; };
const RegistrationLink = () => ( const RegistrationLink = () => {
<Link const accountsUrl = useAccountsUrl();
href="https://account.siasky.net/auth/registration"
className="uppercase underline-primary hover:text-primary transition-colors duration-200" return (
> <Link
Sign up href={`${accountsUrl}/auth/registration`}
</Link> className="uppercase underline-primary hover:text-primary transition-colors duration-200"
); >
Sign up
</Link>
);
};
const LogInLink = () => {
const accountsUrl = useAccountsUrl();
return (
<Link
href={`${accountsUrl}/auth/login`}
className="uppercase underline-primary hover:text-primary transition-colors duration-200"
>
Log in
</Link>
);
};
const Uploader = () => { const Uploader = () => {
const [mode, setMode] = React.useState("file"); const [mode, setMode] = React.useState("file");
const [uploads, setUploads] = React.useState([]); const [uploads, setUploads] = React.useState([]);
const { data: authenticationStatus } = useAuthenticatedStatus(); const { data: accounts } = useAccounts();
const authenticated = authenticationStatus?.authenticated ?? false; const showAccountFeatures = accounts?.enabled && !accounts?.auth_required && !accounts?.authenticated;
const disabledComponent = accounts?.enabled && accounts?.auth_required && !accounts?.authenticated;
const onUploadStateChange = React.useCallback((id, state) => { const onUploadStateChange = React.useCallback((id, state) => {
setUploads((uploads) => { setUploads((uploads) => {
@ -80,7 +99,7 @@ const Uploader = () => {
}, [inputElement, mode]); }, [inputElement, mode]);
return ( return (
<div> <div className={classnames("relative", { "p-8": disabledComponent })}>
<div className="max-w-content mx-auto rounded-lg shadow bg-white z-0 relative"> <div className="max-w-content mx-auto rounded-lg shadow bg-white z-0 relative">
<div className="flex"> <div className="flex">
<button <button
@ -115,6 +134,7 @@ const Uploader = () => {
"drop-active": isDragActive, "drop-active": isDragActive,
})} })}
{...getRootProps()} {...getRootProps()}
disabled={true}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />
<div <div
@ -131,11 +151,13 @@ const Uploader = () => {
{mode === "directory" && <span>Drop any folder with an index.html file to deploy to Skynet</span>} {mode === "directory" && <span>Drop any folder with an index.html file to deploy to Skynet</span>}
</h4> </h4>
</div> </div>
<div className="absolute left-1/2 -bottom-4 desktop:-bottom-8"> {!disabledComponent && (
<div className="relative -left-1/2 transform transition-transform hover:rotate-180" role="button"> <div className="absolute left-1/2 -bottom-4 desktop:-bottom-8">
<Add /> <div className="relative -left-1/2 transform transition-transform hover:rotate-180" role="button">
<Add />
</div>
</div> </div>
</div> )}
</div> </div>
{uploads.length > 0 && ( {uploads.length > 0 && (
@ -144,7 +166,7 @@ const Uploader = () => {
<UploaderElement key={upload.id} onUploadStateChange={onUploadStateChange} upload={upload} /> <UploaderElement key={upload.id} onUploadStateChange={onUploadStateChange} upload={upload} />
))} ))}
{!authenticated && ( {showAccountFeatures && (
<div className="z-0 relative flex flex-col items-center space-y-1 pt-8"> <div className="z-0 relative flex flex-col items-center space-y-1 pt-8">
<Info /> <Info />
@ -166,7 +188,7 @@ const Uploader = () => {
)} )}
</div> </div>
{uploads.length === 0 && !authenticated && ( {uploads.length === 0 && showAccountFeatures && (
<div className="z-0 relative flex flex-col items-center space-y-1 mt-10"> <div className="z-0 relative flex flex-col items-center space-y-1 mt-10">
<Unlock /> <Unlock />
<p className="text-sm font-light text-palette-600"> <p className="text-sm font-light text-palette-600">
@ -174,6 +196,18 @@ const Uploader = () => {
</p> </p>
</div> </div>
)} )}
{disabledComponent && (
<div className="absolute inset-0 bg-palette-500 bg-opacity-90 rounded-lg">
<div className="flex h-full">
<div className="m-auto">
<h4 className="font-light text-palette-100 text-lg mt-2 text-center">
<LogInLink /> or <RegistrationLink /> for free
</h4>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -5,9 +5,9 @@ import bytes from "bytes";
import { getReasonPhrase, StatusCodes } from "http-status-codes"; import { getReasonPhrase, StatusCodes } from "http-status-codes";
import copy from "copy-text-to-clipboard"; import copy from "copy-text-to-clipboard";
import path from "path-browserify"; import path from "path-browserify";
import { SkynetClient } from "skynet-js";
import { useTimeoutFn } from "react-use"; import { useTimeoutFn } from "react-use";
import ms from "ms"; import ms from "ms";
import skynetClient from "../../services/skynetClient";
import Link from "../Link"; import Link from "../Link";
const getFilePath = (file) => file.webkitRelativePath || file.path || file.name; const getFilePath = (file) => file.webkitRelativePath || file.path || file.name;
@ -55,8 +55,6 @@ const createUploadErrorMessage = (error) => {
return `Critical error, please refresh the application and try again. ${error.message}`; return `Critical error, please refresh the application and try again. ${error.message}`;
}; };
const client = new SkynetClient(process.env.GATSBY_API_URL);
export default function UploaderElement({ onUploadStateChange, upload }) { export default function UploaderElement({ onUploadStateChange, upload }) {
const [copied, setCopied] = React.useState(false); const [copied, setCopied] = React.useState(false);
const [, , reset] = useTimeoutFn(() => setCopied(false), ms("3 seconds")); const [, , reset] = useTimeoutFn(() => setCopied(false), ms("3 seconds"));
@ -87,12 +85,12 @@ export default function UploaderElement({ onUploadStateChange, upload }) {
const directory = files.reduce((acc, file) => ({ ...acc, [getRelativeFilePath(file)]: file }), {}); const directory = files.reduce((acc, file) => ({ ...acc, [getRelativeFilePath(file)]: file }), {});
const name = encodeURIComponent(upload.file.name); const name = encodeURIComponent(upload.file.name);
response = await client.uploadDirectory(directory, name, { onUploadProgress }); response = await skynetClient.uploadDirectory(directory, name, { onUploadProgress });
} else { } else {
response = await client.uploadFile(upload.file, { onUploadProgress }); response = await skynetClient.uploadFile(upload.file, { onUploadProgress });
} }
const url = await client.getSkylinkUrl(response.skylink, { subdomain: upload.mode === "directory" }); const url = await skynetClient.getSkylinkUrl(response.skylink, { subdomain: upload.mode === "directory" });
onUploadStateChange(upload.id, { status: "complete", url }); onUploadStateChange(upload.id, { status: "complete", url });
} catch (error) { } catch (error) {

View File

@ -0,0 +1,3 @@
import { SkynetClient } from "skynet-js";
export default new SkynetClient(process.env.GATSBY_API_URL);

View File

@ -0,0 +1,8 @@
import useSWR from "swr";
const prefix = process.env.GATSBY_API_URL ?? "";
const fetcher = (url) => fetch(`${prefix}${url}`).then((response) => response.json());
export default function useAccounts() {
return useSWR("/__internal/do/not/use/accounts", fetcher);
}

View File

@ -0,0 +1,18 @@
import * as React from "react";
import skynetClient from "./skynetClient";
export default function useAccountsUrl() {
const [url, setUrl] = React.useState("");
React.useEffect(() => {
(async function resolve() {
const portalUrl = new URL(await skynetClient.portalUrl());
portalUrl.host = `account.${portalUrl.host}`;
setUrl(portalUrl.toString());
})();
}, [setUrl]);
return url;
}

View File

@ -1,7 +0,0 @@
import useSWR from "swr";
const fetcher = (url) => fetch(url).then((response) => response.json());
export default function useAuthenticatedStatus() {
return useSWR("/__internal/do/not/use/authenticated", fetcher);
}