mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-03-17 21:12:22 +01:00
Cleanup/Formatting
This commit is contained in:
parent
ef4ca27f4b
commit
0408f77dea
@ -45,7 +45,7 @@ public class UserController : Controller
|
||||
var requestedId = isMe ? loggedUser!.Value : id.FromBase58Guid();
|
||||
var user = await _store.Get(requestedId);
|
||||
if (user == default) return NotFound();
|
||||
if (loggedUser != requestedId && !user.Flags.HasFlag(UserFlags.PublicProfile))
|
||||
if (loggedUser != requestedId && !user.Flags.HasFlag(UserFlags.PublicProfile) && !HttpContext.IsRole(Roles.Admin))
|
||||
return NotFound();
|
||||
|
||||
var isMyProfile = requestedId == user.Id;
|
||||
|
File diff suppressed because one or more lines are too long
20
VoidCat/spa/.yarn/sdks/eslint/lib/api.js
vendored
Normal file
20
VoidCat/spa/.yarn/sdks/eslint/lib/api.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint your application uses
|
||||
module.exports = absRequire(`eslint`);
|
6
VoidCat/spa/.yarn/sdks/eslint/package.json
vendored
Normal file
6
VoidCat/spa/.yarn/sdks/eslint/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "eslint",
|
||||
"version": "8.51.0-sdk",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs"
|
||||
}
|
5
VoidCat/spa/.yarn/sdks/integrations.yml
vendored
Normal file
5
VoidCat/spa/.yarn/sdks/integrations.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# This file is automatically generated by @yarnpkg/sdks.
|
||||
# Manual changes might be lost!
|
||||
|
||||
integrations:
|
||||
- vscode
|
20
VoidCat/spa/.yarn/sdks/prettier/index.js
vendored
Executable file
20
VoidCat/spa/.yarn/sdks/prettier/index.js
vendored
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require prettier
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real prettier your application uses
|
||||
module.exports = absRequire(`prettier`);
|
6
VoidCat/spa/.yarn/sdks/prettier/package.json
vendored
Normal file
6
VoidCat/spa/.yarn/sdks/prettier/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "prettier",
|
||||
"version": "3.0.3-sdk",
|
||||
"main": "./index.js",
|
||||
"type": "commonjs"
|
||||
}
|
20
VoidCat/spa/.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
20
VoidCat/spa/.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsc.js your application uses
|
||||
module.exports = absRequire(`typescript/lib/tsc.js`);
|
272
VoidCat/spa/.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
272
VoidCat/spa/.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = (tsserver) => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const { isAbsolute } = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = (str) => str.startsWith("portal:/");
|
||||
const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(
|
||||
pnpApi.getDependencyTreeRoots().map((locator) => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}),
|
||||
);
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (
|
||||
isAbsolute(str) &&
|
||||
!str.match(/^\^?(zip:|\/zip\/)/) &&
|
||||
(str.match(/\.zip\//) || isVirtual(str))
|
||||
) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (
|
||||
locator &&
|
||||
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) ||
|
||||
isPortal(locator.reference))
|
||||
) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`:
|
||||
{
|
||||
str = `^zip:${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode <1.66`:
|
||||
{
|
||||
str = `^/zip/${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode <1.68`:
|
||||
{
|
||||
str = `^/zip${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode`:
|
||||
{
|
||||
str = `^/zip/${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`:
|
||||
{
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
}
|
||||
break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`:
|
||||
{
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
str = `zip:${str}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`:
|
||||
{
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
}
|
||||
break;
|
||||
|
||||
case `neovim`:
|
||||
{
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode`:
|
||||
default:
|
||||
{
|
||||
return str.replace(
|
||||
/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/,
|
||||
process.platform === `win32` ? `` : `/`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } =
|
||||
ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const { onMessage: originalOnMessage, send: originalSend } =
|
||||
Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === "string";
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (
|
||||
process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
|
||||
) ?? []
|
||||
).map(Number);
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(
|
||||
parsedMessage,
|
||||
(key, value) => {
|
||||
return typeof value === "string" ? fromEditorPath(value) : value;
|
||||
},
|
||||
);
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage
|
||||
? processedMessageJSON
|
||||
: JSON.parse(processedMessageJSON),
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(
|
||||
this,
|
||||
JSON.parse(
|
||||
JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
272
VoidCat/spa/.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
272
VoidCat/spa/.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = (tsserver) => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const { isAbsolute } = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = (str) => str.startsWith("portal:/");
|
||||
const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(
|
||||
pnpApi.getDependencyTreeRoots().map((locator) => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}),
|
||||
);
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (
|
||||
isAbsolute(str) &&
|
||||
!str.match(/^\^?(zip:|\/zip\/)/) &&
|
||||
(str.match(/\.zip\//) || isVirtual(str))
|
||||
) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (
|
||||
locator &&
|
||||
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) ||
|
||||
isPortal(locator.reference))
|
||||
) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`:
|
||||
{
|
||||
str = `^zip:${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode <1.66`:
|
||||
{
|
||||
str = `^/zip/${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode <1.68`:
|
||||
{
|
||||
str = `^/zip${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode`:
|
||||
{
|
||||
str = `^/zip/${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`:
|
||||
{
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
}
|
||||
break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`:
|
||||
{
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
str = `zip:${str}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`:
|
||||
{
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
}
|
||||
break;
|
||||
|
||||
case `neovim`:
|
||||
{
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode`:
|
||||
default:
|
||||
{
|
||||
return str.replace(
|
||||
/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/,
|
||||
process.platform === `win32` ? `` : `/`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } =
|
||||
ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const { onMessage: originalOnMessage, send: originalSend } =
|
||||
Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === "string";
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (
|
||||
process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
|
||||
) ?? []
|
||||
).map(Number);
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(
|
||||
parsedMessage,
|
||||
(key, value) => {
|
||||
return typeof value === "string" ? fromEditorPath(value) : value;
|
||||
},
|
||||
);
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage
|
||||
? processedMessageJSON
|
||||
: JSON.parse(processedMessageJSON),
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(
|
||||
this,
|
||||
JSON.parse(
|
||||
JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
20
VoidCat/spa/.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
20
VoidCat/spa/.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/typescript.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/typescript.js your application uses
|
||||
module.exports = absRequire(`typescript/lib/typescript.js`);
|
6
VoidCat/spa/.yarn/sdks/typescript/package.json
vendored
Normal file
6
VoidCat/spa/.yarn/sdks/typescript/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "5.2.2-sdk",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs"
|
||||
}
|
@ -1 +1 @@
|
||||
yarnPath: .yarn/releases/yarn-3.6.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-3.6.4.cjs
|
||||
|
@ -7,5 +7,10 @@
|
||||
"build": "yarn workspace @void-cat/api build && yarn workspace @void-cat/app build",
|
||||
"start": "yarn workspace @void-cat/api build && yarn workspace @void-cat/app start"
|
||||
},
|
||||
"packageManager": "yarn@3.6.2"
|
||||
"packageManager": "yarn@3.6.4",
|
||||
"devDependencies": {
|
||||
"eslint": "^8.51.0",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export class VoidApi {
|
||||
stateChange?: StateChangeHandler,
|
||||
progress?: ProgressHandler,
|
||||
proxyChallenge?: ProxyChallengeHandler,
|
||||
chunkSize?: number
|
||||
chunkSize?: number,
|
||||
): VoidUploader {
|
||||
if (StreamUploader.canUse()) {
|
||||
return new StreamUploader(
|
||||
@ -76,7 +76,7 @@ export class VoidApi {
|
||||
progress,
|
||||
proxyChallenge,
|
||||
this.#auth,
|
||||
chunkSize
|
||||
chunkSize,
|
||||
);
|
||||
} else {
|
||||
return new XHRUploader(
|
||||
@ -86,7 +86,7 @@ export class VoidApi {
|
||||
progress,
|
||||
proxyChallenge,
|
||||
this.#auth,
|
||||
chunkSize
|
||||
chunkSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -142,7 +142,7 @@ export class VoidApi {
|
||||
return this.#req<PagedResponse<VoidFileResponse>>(
|
||||
"POST",
|
||||
`/user/${uid}/files`,
|
||||
pageReq
|
||||
pageReq,
|
||||
);
|
||||
}
|
||||
|
||||
@ -170,7 +170,7 @@ export class VoidApi {
|
||||
return this.#req<PagedResponse<VoidFileResponse>>(
|
||||
"POST",
|
||||
"/admin/file",
|
||||
pageReq
|
||||
pageReq,
|
||||
);
|
||||
}
|
||||
|
||||
@ -182,7 +182,7 @@ export class VoidApi {
|
||||
return this.#req<PagedResponse<AdminUserListResult>>(
|
||||
"POST",
|
||||
`/admin/users`,
|
||||
pageReq
|
||||
pageReq,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -11,32 +11,37 @@ import sjcl from "sjcl";
|
||||
* @namespace
|
||||
*/
|
||||
export const sjclcodec = {
|
||||
/** Convert from a bitArray to an array of bytes. */
|
||||
fromBits: function (arr) {
|
||||
var out = [], bl = sjcl.bitArray.bitLength(arr), i, tmp;
|
||||
for (i = 0; i < bl / 8; i++) {
|
||||
if ((i & 3) === 0) {
|
||||
tmp = arr[i / 4];
|
||||
}
|
||||
out.push(tmp >>> 24);
|
||||
tmp <<= 8;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
/** Convert from an array of bytes to a bitArray. */
|
||||
/** @return {bitArray} */
|
||||
toBits: function (bytes) {
|
||||
var out = [], i, tmp = 0;
|
||||
for (i = 0; i < bytes.length; i++) {
|
||||
tmp = tmp << 8 | bytes[i];
|
||||
if ((i & 3) === 3) {
|
||||
out.push(tmp);
|
||||
tmp = 0;
|
||||
}
|
||||
}
|
||||
if (i & 3) {
|
||||
out.push(sjcl.bitArray.partial(8 * (i & 3), tmp));
|
||||
}
|
||||
return out;
|
||||
/** Convert from a bitArray to an array of bytes. */
|
||||
fromBits: function (arr) {
|
||||
var out = [],
|
||||
bl = sjcl.bitArray.bitLength(arr),
|
||||
i,
|
||||
tmp;
|
||||
for (i = 0; i < bl / 8; i++) {
|
||||
if ((i & 3) === 0) {
|
||||
tmp = arr[i / 4];
|
||||
}
|
||||
out.push(tmp >>> 24);
|
||||
tmp <<= 8;
|
||||
}
|
||||
};
|
||||
return out;
|
||||
},
|
||||
/** Convert from an array of bytes to a bitArray. */
|
||||
/** @return {bitArray} */
|
||||
toBits: function (bytes) {
|
||||
var out = [],
|
||||
i,
|
||||
tmp = 0;
|
||||
for (i = 0; i < bytes.length; i++) {
|
||||
tmp = (tmp << 8) | bytes[i];
|
||||
if ((i & 3) === 3) {
|
||||
out.push(tmp);
|
||||
tmp = 0;
|
||||
}
|
||||
}
|
||||
if (i & 3) {
|
||||
out.push(sjcl.bitArray.partial(8 * (i & 3), tmp));
|
||||
}
|
||||
return out;
|
||||
},
|
||||
};
|
||||
|
@ -1,176 +1,176 @@
|
||||
export { VoidApi } from "./api"
|
||||
export { VoidApi } from "./api";
|
||||
export { UploadState } from "./upload";
|
||||
export { StreamEncryption } from "./stream-encryption";
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly statusCode: number
|
||||
readonly statusCode: number;
|
||||
|
||||
constructor(statusCode: number, msg: string) {
|
||||
super(msg);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
constructor(statusCode: number, msg: string) {
|
||||
super(msg);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoginSession {
|
||||
jwt?: string
|
||||
profile?: Profile
|
||||
error?: string
|
||||
jwt?: string;
|
||||
profile?: Profile;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface AdminUserListResult {
|
||||
user: AdminProfile
|
||||
uploads: number
|
||||
user: AdminProfile;
|
||||
uploads: number;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
id: string
|
||||
avatar?: string
|
||||
name?: string
|
||||
created: string
|
||||
lastLogin: string
|
||||
roles: Array<string>
|
||||
publicProfile: boolean
|
||||
publicUploads: boolean
|
||||
needsVerification?: boolean
|
||||
id: string;
|
||||
avatar?: string;
|
||||
name?: string;
|
||||
created: string;
|
||||
lastLogin: string;
|
||||
roles: Array<string>;
|
||||
publicProfile: boolean;
|
||||
publicUploads: boolean;
|
||||
needsVerification?: boolean;
|
||||
}
|
||||
|
||||
export interface PagedResponse<T> {
|
||||
totalResults: number,
|
||||
results: Array<T>
|
||||
totalResults: number;
|
||||
results: Array<T>;
|
||||
}
|
||||
|
||||
export interface AdminProfile extends Profile {
|
||||
email: string
|
||||
storage: string
|
||||
email: string;
|
||||
storage: string;
|
||||
}
|
||||
|
||||
export interface Bandwidth {
|
||||
ingress: number
|
||||
egress: number
|
||||
ingress: number;
|
||||
egress: number;
|
||||
}
|
||||
|
||||
export interface BandwidthPoint {
|
||||
time: string
|
||||
ingress: number
|
||||
egress: number
|
||||
time: string;
|
||||
ingress: number;
|
||||
egress: number;
|
||||
}
|
||||
|
||||
export interface SiteInfoResponse {
|
||||
count: number
|
||||
totalBytes: number
|
||||
buildInfo: {
|
||||
version: string
|
||||
gitHash: string
|
||||
buildTime: string
|
||||
}
|
||||
bandwidth: Bandwidth
|
||||
fileStores: Array<string>
|
||||
uploadSegmentSize: number
|
||||
captchaSiteKey?: string
|
||||
oAuthProviders: Array<string>
|
||||
timeSeriesMetrics?: Array<BandwidthPoint>
|
||||
count: number;
|
||||
totalBytes: number;
|
||||
buildInfo: {
|
||||
version: string;
|
||||
gitHash: string;
|
||||
buildTime: string;
|
||||
};
|
||||
bandwidth: Bandwidth;
|
||||
fileStores: Array<string>;
|
||||
uploadSegmentSize: number;
|
||||
captchaSiteKey?: string;
|
||||
oAuthProviders: Array<string>;
|
||||
timeSeriesMetrics?: Array<BandwidthPoint>;
|
||||
}
|
||||
|
||||
export interface PagedRequest {
|
||||
pageSize: number
|
||||
page: number
|
||||
pageSize: number;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export interface VoidUploadResult {
|
||||
ok: boolean
|
||||
file?: VoidFileResponse
|
||||
errorMessage?: string
|
||||
ok: boolean;
|
||||
file?: VoidFileResponse;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface VoidFileResponse {
|
||||
id: string,
|
||||
metadata?: VoidFileMeta
|
||||
payment?: Payment
|
||||
uploader?: Profile
|
||||
bandwidth?: Bandwidth
|
||||
virusScan?: VirusScanStatus
|
||||
id: string;
|
||||
metadata?: VoidFileMeta;
|
||||
payment?: Payment;
|
||||
uploader?: Profile;
|
||||
bandwidth?: Bandwidth;
|
||||
virusScan?: VirusScanStatus;
|
||||
}
|
||||
|
||||
export interface VoidFileMeta {
|
||||
name?: string
|
||||
description?: string
|
||||
size: number
|
||||
uploaded: string
|
||||
mimeType: string
|
||||
digest?: string
|
||||
expires?: string
|
||||
url?: string
|
||||
editSecret?: string
|
||||
encryptionParams?: string
|
||||
magnetLink?: string
|
||||
storage: string
|
||||
name?: string;
|
||||
description?: string;
|
||||
size: number;
|
||||
uploaded: string;
|
||||
mimeType: string;
|
||||
digest?: string;
|
||||
expires?: string;
|
||||
url?: string;
|
||||
editSecret?: string;
|
||||
encryptionParams?: string;
|
||||
magnetLink?: string;
|
||||
storage: string;
|
||||
}
|
||||
|
||||
export interface VirusScanStatus {
|
||||
isVirus: boolean
|
||||
names?: string
|
||||
isVirus: boolean;
|
||||
names?: string;
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
service: PaymentServices
|
||||
required: boolean
|
||||
currency: PaymentCurrencies
|
||||
amount: number
|
||||
strikeHandle?: string
|
||||
service: PaymentServices;
|
||||
required: boolean;
|
||||
currency: PaymentCurrencies;
|
||||
amount: number;
|
||||
strikeHandle?: string;
|
||||
}
|
||||
|
||||
export interface SetPaymentConfigRequest {
|
||||
editSecret: string
|
||||
currency: PaymentCurrencies
|
||||
amount: number
|
||||
strikeHandle?: string
|
||||
required: boolean
|
||||
editSecret: string;
|
||||
currency: PaymentCurrencies;
|
||||
amount: number;
|
||||
strikeHandle?: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export interface PaymentOrder {
|
||||
id: string
|
||||
status: PaymentOrderState
|
||||
orderLightning?: PaymentOrderLightning
|
||||
id: string;
|
||||
status: PaymentOrderState;
|
||||
orderLightning?: PaymentOrderLightning;
|
||||
}
|
||||
|
||||
export interface PaymentOrderLightning {
|
||||
invoice: string
|
||||
expire: string
|
||||
invoice: string;
|
||||
expire: string;
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
id: string
|
||||
created: string
|
||||
expiry: string
|
||||
token: string
|
||||
id: string;
|
||||
created: string;
|
||||
expiry: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export enum PaymentCurrencies {
|
||||
BTC = 0,
|
||||
USD = 1,
|
||||
EUR = 2,
|
||||
GBP = 3
|
||||
BTC = 0,
|
||||
USD = 1,
|
||||
EUR = 2,
|
||||
GBP = 3,
|
||||
}
|
||||
|
||||
export enum PaymentServices {
|
||||
None = 0,
|
||||
Strike = 1
|
||||
None = 0,
|
||||
Strike = 1,
|
||||
}
|
||||
|
||||
export enum PaymentOrderState {
|
||||
Unpaid = 0,
|
||||
Paid = 1,
|
||||
Expired = 2
|
||||
Unpaid = 0,
|
||||
Paid = 1,
|
||||
Expired = 2,
|
||||
}
|
||||
|
||||
export enum PagedSortBy {
|
||||
Name = 0,
|
||||
Date = 1,
|
||||
Size = 2,
|
||||
Id = 3
|
||||
Name = 0,
|
||||
Date = 1,
|
||||
Size = 2,
|
||||
Id = 3,
|
||||
}
|
||||
|
||||
export enum PageSortOrder {
|
||||
Asc = 0,
|
||||
Dsc = 1
|
||||
Asc = 0,
|
||||
Dsc = 1,
|
||||
}
|
||||
|
@ -4,115 +4,155 @@ import sjcl, { SjclCipher } from "sjcl";
|
||||
import { buf2hex } from "./utils";
|
||||
|
||||
interface EncryptionParams {
|
||||
ts: number,
|
||||
cs: number
|
||||
ts: number;
|
||||
cs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES-GCM TransformStream
|
||||
*/
|
||||
export class StreamEncryption {
|
||||
readonly #tagSize: number;
|
||||
readonly #chunkSize: number;
|
||||
readonly #aes: SjclCipher;
|
||||
readonly #key: sjcl.BitArray;
|
||||
readonly #iv: sjcl.BitArray;
|
||||
readonly #tagSize: number;
|
||||
readonly #chunkSize: number;
|
||||
readonly #aes: SjclCipher;
|
||||
readonly #key: sjcl.BitArray;
|
||||
readonly #iv: sjcl.BitArray;
|
||||
|
||||
constructor(key: string | sjcl.BitArray | undefined, iv: string | sjcl.BitArray | undefined, params?: EncryptionParams | string) {
|
||||
if (!key && !iv) {
|
||||
key = buf2hex(globalThis.crypto.getRandomValues(new Uint8Array(16)));
|
||||
iv = buf2hex(globalThis.crypto.getRandomValues(new Uint8Array(12)));
|
||||
}
|
||||
if (typeof key === "string" && typeof iv === "string") {
|
||||
key = sjcl.codec.hex.toBits(key);
|
||||
iv = sjcl.codec.hex.toBits(iv);
|
||||
} else if (!Array.isArray(key) || !Array.isArray(iv)) {
|
||||
throw "Key and IV must be hex string or bitArray";
|
||||
}
|
||||
if (typeof params === "string") {
|
||||
params = JSON.parse(params) as EncryptionParams;
|
||||
}
|
||||
|
||||
this.#tagSize = params?.ts ?? 128;
|
||||
this.#chunkSize = params?.cs ?? (1024 * 1024 * 10);
|
||||
this.#aes = new sjcl.cipher.aes(key);
|
||||
this.#key = key;
|
||||
this.#iv = iv;
|
||||
|
||||
console.log(`ts=${this.#tagSize}, cs=${this.#chunkSize}, key=${key}, iv=${this.#iv}`);
|
||||
constructor(
|
||||
key: string | sjcl.BitArray | undefined,
|
||||
iv: string | sjcl.BitArray | undefined,
|
||||
params?: EncryptionParams | string,
|
||||
) {
|
||||
if (!key && !iv) {
|
||||
key = buf2hex(globalThis.crypto.getRandomValues(new Uint8Array(16)));
|
||||
iv = buf2hex(globalThis.crypto.getRandomValues(new Uint8Array(12)));
|
||||
}
|
||||
if (typeof key === "string" && typeof iv === "string") {
|
||||
key = sjcl.codec.hex.toBits(key);
|
||||
iv = sjcl.codec.hex.toBits(iv);
|
||||
} else if (!Array.isArray(key) || !Array.isArray(iv)) {
|
||||
throw "Key and IV must be hex string or bitArray";
|
||||
}
|
||||
if (typeof params === "string") {
|
||||
params = JSON.parse(params) as EncryptionParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return formatted encryption key
|
||||
*/
|
||||
getKey() {
|
||||
return `${sjcl.codec.hex.fromBits(this.#key)}:${sjcl.codec.hex.fromBits(this.#iv)}`;
|
||||
}
|
||||
this.#tagSize = params?.ts ?? 128;
|
||||
this.#chunkSize = params?.cs ?? 1024 * 1024 * 10;
|
||||
this.#aes = new sjcl.cipher.aes(key);
|
||||
this.#key = key;
|
||||
this.#iv = iv;
|
||||
|
||||
/**
|
||||
* Get encryption params
|
||||
*/
|
||||
getParams() {
|
||||
return {
|
||||
ts: this.#tagSize,
|
||||
cs: this.#chunkSize
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`ts=${this.#tagSize}, cs=${this.#chunkSize}, key=${key}, iv=${this.#iv}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption TransformStream
|
||||
*/
|
||||
getEncryptionTransform() {
|
||||
return this._getCryptoStream(0);
|
||||
}
|
||||
/**
|
||||
* Return formatted encryption key
|
||||
*/
|
||||
getKey() {
|
||||
return `${sjcl.codec.hex.fromBits(this.#key)}:${sjcl.codec.hex.fromBits(
|
||||
this.#iv,
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get decryption TransformStream
|
||||
*/
|
||||
getDecryptionTransform() {
|
||||
return this._getCryptoStream(1);
|
||||
}
|
||||
/**
|
||||
* Get encryption params
|
||||
*/
|
||||
getParams() {
|
||||
return {
|
||||
ts: this.#tagSize,
|
||||
cs: this.#chunkSize,
|
||||
};
|
||||
}
|
||||
|
||||
_getCryptoStream(mode: number) {
|
||||
let offset = 0;
|
||||
let buffer = new Uint8Array(this.#chunkSize + (mode === 1 ? this.#tagSize / 8 : 0));
|
||||
return new TransformStream({
|
||||
transform: async (chunk, controller) => {
|
||||
chunk = await chunk;
|
||||
try {
|
||||
let toBuffer = Math.min(chunk.byteLength, buffer.byteLength - offset);
|
||||
buffer.set(chunk.slice(0, toBuffer), offset);
|
||||
offset += toBuffer;
|
||||
/**
|
||||
* Get encryption TransformStream
|
||||
*/
|
||||
getEncryptionTransform() {
|
||||
return this._getCryptoStream(0);
|
||||
}
|
||||
|
||||
if (offset === buffer.byteLength) {
|
||||
let buff = sjclcodec.toBits(buffer);
|
||||
let encryptedBuf = sjclcodec.fromBits(
|
||||
mode === 0 ?
|
||||
sjcl.mode.gcm.encrypt(this.#aes, buff, this.#iv, [], this.#tagSize) :
|
||||
sjcl.mode.gcm.decrypt(this.#aes, buff, this.#iv, [], this.#tagSize)
|
||||
);
|
||||
controller.enqueue(new Uint8Array(encryptedBuf));
|
||||
/**
|
||||
* Get decryption TransformStream
|
||||
*/
|
||||
getDecryptionTransform() {
|
||||
return this._getCryptoStream(1);
|
||||
}
|
||||
|
||||
offset = chunk.byteLength - toBuffer;
|
||||
buffer.set(chunk.slice(toBuffer));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
flush: (controller) => {
|
||||
let lastBuffer = buffer.slice(0, offset);
|
||||
let buff = sjclcodec.toBits(lastBuffer);
|
||||
let encryptedBuf = sjclcodec.fromBits(
|
||||
mode === 0 ?
|
||||
sjcl.mode.gcm.encrypt(this.#aes, buff, this.#iv, [], this.#tagSize) :
|
||||
sjcl.mode.gcm.decrypt(this.#aes, buff, this.#iv, [], this.#tagSize)
|
||||
);
|
||||
controller.enqueue(new Uint8Array(encryptedBuf));
|
||||
_getCryptoStream(mode: number) {
|
||||
let offset = 0;
|
||||
let buffer = new Uint8Array(
|
||||
this.#chunkSize + (mode === 1 ? this.#tagSize / 8 : 0),
|
||||
);
|
||||
return new TransformStream(
|
||||
{
|
||||
transform: async (chunk, controller) => {
|
||||
chunk = await chunk;
|
||||
try {
|
||||
let toBuffer = Math.min(
|
||||
chunk.byteLength,
|
||||
buffer.byteLength - offset,
|
||||
);
|
||||
buffer.set(chunk.slice(0, toBuffer), offset);
|
||||
offset += toBuffer;
|
||||
|
||||
if (offset === buffer.byteLength) {
|
||||
let buff = sjclcodec.toBits(buffer);
|
||||
let encryptedBuf = sjclcodec.fromBits(
|
||||
mode === 0
|
||||
? sjcl.mode.gcm.encrypt(
|
||||
this.#aes,
|
||||
buff,
|
||||
this.#iv,
|
||||
[],
|
||||
this.#tagSize,
|
||||
)
|
||||
: sjcl.mode.gcm.decrypt(
|
||||
this.#aes,
|
||||
buff,
|
||||
this.#iv,
|
||||
[],
|
||||
this.#tagSize,
|
||||
),
|
||||
);
|
||||
controller.enqueue(new Uint8Array(encryptedBuf));
|
||||
|
||||
offset = chunk.byteLength - toBuffer;
|
||||
buffer.set(chunk.slice(toBuffer));
|
||||
}
|
||||
}, {
|
||||
highWaterMark: this.#chunkSize
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
flush: (controller) => {
|
||||
let lastBuffer = buffer.slice(0, offset);
|
||||
let buff = sjclcodec.toBits(lastBuffer);
|
||||
let encryptedBuf = sjclcodec.fromBits(
|
||||
mode === 0
|
||||
? sjcl.mode.gcm.encrypt(
|
||||
this.#aes,
|
||||
buff,
|
||||
this.#iv,
|
||||
[],
|
||||
this.#tagSize,
|
||||
)
|
||||
: sjcl.mode.gcm.decrypt(
|
||||
this.#aes,
|
||||
buff,
|
||||
this.#iv,
|
||||
[],
|
||||
this.#tagSize,
|
||||
),
|
||||
);
|
||||
controller.enqueue(new Uint8Array(encryptedBuf));
|
||||
},
|
||||
},
|
||||
{
|
||||
highWaterMark: this.#chunkSize,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,97 +3,115 @@ import { VoidUploadResult } from "./index";
|
||||
import { StreamEncryption } from "./stream-encryption";
|
||||
|
||||
export class StreamUploader extends VoidUploader {
|
||||
#encrypt?: StreamEncryption;
|
||||
#encrypt?: StreamEncryption;
|
||||
|
||||
static canUse() {
|
||||
const rawUA = globalThis.navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
||||
const majorVersion = rawUA ? parseInt(rawUA[2], 10) : 0;
|
||||
return majorVersion >= 105 && "getRandomValues" in globalThis.crypto && globalThis.location.protocol === "https:";
|
||||
static canUse() {
|
||||
const rawUA = globalThis.navigator.userAgent.match(
|
||||
/Chrom(e|ium)\/([0-9]+)\./,
|
||||
);
|
||||
const majorVersion = rawUA ? parseInt(rawUA[2], 10) : 0;
|
||||
return (
|
||||
majorVersion >= 105 &&
|
||||
"getRandomValues" in globalThis.crypto &&
|
||||
globalThis.location.protocol === "https:"
|
||||
);
|
||||
}
|
||||
|
||||
canEncrypt(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
setEncryption(s: boolean) {
|
||||
if (s) {
|
||||
this.#encrypt = new StreamEncryption(undefined, undefined, undefined);
|
||||
} else {
|
||||
this.#encrypt = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
canEncrypt(): boolean {
|
||||
return true;
|
||||
getEncryptionKey() {
|
||||
return this.#encrypt?.getKey();
|
||||
}
|
||||
|
||||
async upload(headers?: HeadersInit): Promise<VoidUploadResult> {
|
||||
this.onStateChange?.(UploadState.Hashing);
|
||||
const hash = await this.digest(this.file);
|
||||
let offset = 0;
|
||||
|
||||
const DefaultChunkSize = 1024 * 1024;
|
||||
const rsBase = new ReadableStream(
|
||||
{
|
||||
start: async () => {
|
||||
this.onStateChange?.(UploadState.Uploading);
|
||||
},
|
||||
pull: async (controller) => {
|
||||
const chunk = await this.readChunk(
|
||||
offset,
|
||||
controller.desiredSize ?? DefaultChunkSize,
|
||||
);
|
||||
if (chunk.byteLength === 0) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
this.onProgress?.(offset + chunk.byteLength);
|
||||
offset += chunk.byteLength;
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
cancel: (reason) => {
|
||||
console.log(reason);
|
||||
},
|
||||
type: "bytes",
|
||||
},
|
||||
{
|
||||
highWaterMark: DefaultChunkSize,
|
||||
},
|
||||
);
|
||||
|
||||
const absoluteUrl = `${this.uri}/upload`;
|
||||
const reqHeaders = {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"V-Content-Type": !this.file.type
|
||||
? "application/octet-stream"
|
||||
: this.file.type,
|
||||
"V-Filename": "name" in this.file ? this.file.name : "",
|
||||
"V-Full-Digest": hash,
|
||||
} as Record<string, string>;
|
||||
if (this.#encrypt) {
|
||||
reqHeaders["V-EncryptionParams"] = JSON.stringify(
|
||||
this.#encrypt!.getParams(),
|
||||
);
|
||||
}
|
||||
|
||||
setEncryption(s: boolean) {
|
||||
if (s) {
|
||||
this.#encrypt = new StreamEncryption(undefined, undefined, undefined);
|
||||
} else {
|
||||
this.#encrypt = undefined;
|
||||
}
|
||||
if (this.auth) {
|
||||
reqHeaders["Authorization"] = await this.auth(absoluteUrl, "POST");
|
||||
}
|
||||
const req = await fetch(absoluteUrl, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
body: this.#encrypt
|
||||
? rsBase.pipeThrough(this.#encrypt!.getEncryptionTransform())
|
||||
: rsBase,
|
||||
headers: {
|
||||
...reqHeaders,
|
||||
...headers,
|
||||
},
|
||||
// @ts-ignore New stream spec
|
||||
duplex: "half",
|
||||
});
|
||||
|
||||
getEncryptionKey() {
|
||||
return this.#encrypt?.getKey()
|
||||
if (req.ok) {
|
||||
return (await req.json()) as VoidUploadResult;
|
||||
} else {
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
async upload(headers?: HeadersInit): Promise<VoidUploadResult> {
|
||||
this.onStateChange?.(UploadState.Hashing);
|
||||
const hash = await this.digest(this.file);
|
||||
let offset = 0;
|
||||
|
||||
const DefaultChunkSize = 1024 * 1024;
|
||||
const rsBase = new ReadableStream({
|
||||
start: async () => {
|
||||
this.onStateChange?.(UploadState.Uploading);
|
||||
},
|
||||
pull: async (controller) => {
|
||||
const chunk = await this.readChunk(offset, controller.desiredSize ?? DefaultChunkSize);
|
||||
if (chunk.byteLength === 0) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
this.onProgress?.(offset + chunk.byteLength);
|
||||
offset += chunk.byteLength
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
cancel: (reason) => {
|
||||
console.log(reason);
|
||||
},
|
||||
type: "bytes"
|
||||
}, {
|
||||
highWaterMark: DefaultChunkSize
|
||||
});
|
||||
|
||||
const absoluteUrl = `${this.uri}/upload`;
|
||||
const reqHeaders = {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"V-Content-Type": !this.file.type ? "application/octet-stream" : this.file.type,
|
||||
"V-Filename": "name" in this.file ? this.file.name : "",
|
||||
"V-Full-Digest": hash,
|
||||
} as Record<string, string>;
|
||||
if (this.#encrypt) {
|
||||
reqHeaders["V-EncryptionParams"] = JSON.stringify(this.#encrypt!.getParams());
|
||||
}
|
||||
if (this.auth) {
|
||||
reqHeaders["Authorization"] = await this.auth(absoluteUrl, "POST");
|
||||
}
|
||||
const req = await fetch(absoluteUrl, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
body: this.#encrypt ? rsBase.pipeThrough(this.#encrypt!.getEncryptionTransform()) : rsBase,
|
||||
headers: {
|
||||
...reqHeaders,
|
||||
...headers
|
||||
},
|
||||
// @ts-ignore New stream spec
|
||||
duplex: 'half'
|
||||
});
|
||||
|
||||
if (req.ok) {
|
||||
return await req.json() as VoidUploadResult;
|
||||
} else {
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
async readChunk(offset: number, size: number) {
|
||||
if (offset > this.file.size) {
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
async readChunk(offset: number, size: number) {
|
||||
if (offset > this.file.size) {
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
const end = Math.min(offset + size, this.file.size);
|
||||
const blob = this.file.slice(offset, end, this.file.type);
|
||||
const data = await blob.arrayBuffer();
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
}
|
||||
const end = Math.min(offset + size, this.file.size);
|
||||
const blob = this.file.slice(offset, end, this.file.type);
|
||||
const data = await blob.arrayBuffer();
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export abstract class VoidUploader {
|
||||
progress?: ProgressHandler,
|
||||
proxyChallenge?: ProxyChallengeHandler,
|
||||
auth?: AuthHandler,
|
||||
chunkSize?: number
|
||||
chunkSize?: number,
|
||||
) {
|
||||
this.uri = uri;
|
||||
this.file = file;
|
||||
|
@ -1,3 +1,5 @@
|
||||
export function buf2hex(buffer: number[] | ArrayBuffer) {
|
||||
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
|
||||
return [...new Uint8Array(buffer)]
|
||||
.map((x) => x.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export class XHRUploader extends VoidUploader {
|
||||
undefined,
|
||||
1,
|
||||
1,
|
||||
headers
|
||||
headers,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,7 @@ export class XHRUploader extends VoidUploader {
|
||||
async #doSplitXHRUpload(
|
||||
hash: string,
|
||||
splitSize: number,
|
||||
headers?: HeadersInit
|
||||
headers?: HeadersInit,
|
||||
) {
|
||||
let xhr: VoidUploadResult | null = null;
|
||||
const segments = Math.ceil(this.file.size / splitSize);
|
||||
@ -49,7 +49,7 @@ export class XHRUploader extends VoidUploader {
|
||||
xhr?.file?.metadata?.editSecret,
|
||||
s + 1,
|
||||
segments,
|
||||
headers
|
||||
headers,
|
||||
);
|
||||
if (!xhr.ok) {
|
||||
break;
|
||||
@ -75,7 +75,7 @@ export class XHRUploader extends VoidUploader {
|
||||
editSecret?: string,
|
||||
part?: number,
|
||||
partOf?: number,
|
||||
headers?: HeadersInit
|
||||
headers?: HeadersInit,
|
||||
) {
|
||||
this.onStateChange?.(UploadState.Uploading);
|
||||
|
||||
@ -112,11 +112,11 @@ export class XHRUploader extends VoidUploader {
|
||||
req.setRequestHeader("Content-Type", "application/octet-stream");
|
||||
req.setRequestHeader(
|
||||
"V-Content-Type",
|
||||
!this.file.type ? "application/octet-stream" : this.file.type
|
||||
!this.file.type ? "application/octet-stream" : this.file.type,
|
||||
);
|
||||
req.setRequestHeader(
|
||||
"V-Filename",
|
||||
"name" in this.file ? this.file.name : ""
|
||||
"name" in this.file ? this.file.name : "",
|
||||
);
|
||||
req.setRequestHeader("V-Full-Digest", fullDigest);
|
||||
req.setRequestHeader("V-Segment", `${part}/${partOf}`);
|
||||
|
@ -1,11 +1,11 @@
|
||||
module.exports = {
|
||||
webpack: {
|
||||
configure: {
|
||||
resolve: {
|
||||
fallback: {
|
||||
"crypto": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
webpack: {
|
||||
configure: {
|
||||
resolve: {
|
||||
fallback: {
|
||||
crypto: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta name="description" content="void.cat - free, simple file sharing."/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.png"/>
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="void.cat - free, simple file sharing." />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>void.cat</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,17 +1,17 @@
|
||||
.admin {
|
||||
width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.admin h2 {
|
||||
background-color: #222;
|
||||
padding: 10px;
|
||||
background-color: #222;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.admin .btn {
|
||||
padding: 5px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: small;
|
||||
margin: 2px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: small;
|
||||
margin: 2px;
|
||||
}
|
||||
|
@ -1,53 +1,58 @@
|
||||
import "./Admin.css";
|
||||
import {useState} from "react";
|
||||
import {useSelector} from "react-redux";
|
||||
import {Navigate} from "react-router-dom";
|
||||
import {AdminProfile} from "@void-cat/api";
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { AdminProfile } from "@void-cat/api";
|
||||
|
||||
import {UserList} from "./UserList";
|
||||
import {VoidButton} from "../Components/Shared/VoidButton";
|
||||
import { UserList } from "./UserList";
|
||||
import { VoidButton } from "../Components/Shared/VoidButton";
|
||||
import VoidModal from "../Components/Shared/VoidModal";
|
||||
import EditUser from "./EditUser";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
import {RootState} from "Store";
|
||||
import { RootState } from "Store";
|
||||
import ImageGrid from "../Components/Shared/ImageGrid";
|
||||
|
||||
export function Admin() {
|
||||
const auth = useSelector((state: RootState) => state.login.jwt);
|
||||
const AdminApi = useApi();
|
||||
const [editUser, setEditUser] = useState<AdminProfile>();
|
||||
const auth = useSelector((state: RootState) => state.login.jwt);
|
||||
const AdminApi = useApi();
|
||||
const [editUser, setEditUser] = useState<AdminProfile>();
|
||||
|
||||
async function deleteFile(id: string) {
|
||||
if (window.confirm(`Are you sure you want to delete: ${id}?`)) {
|
||||
try {
|
||||
await AdminApi.adminDeleteFile(id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Failed to delete file!");
|
||||
}
|
||||
}
|
||||
async function deleteFile(id: string) {
|
||||
if (window.confirm(`Are you sure you want to delete: ${id}?`)) {
|
||||
try {
|
||||
await AdminApi.adminDeleteFile(id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Failed to delete file!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth) {
|
||||
return <Navigate to="/login"/>;
|
||||
} else {
|
||||
return (
|
||||
<div className="admin">
|
||||
<h2>Users</h2>
|
||||
<UserList actions={(i) => [
|
||||
<VoidButton key={`delete-${i.id}`}>Delete</VoidButton>,
|
||||
<VoidButton key={`edit-${i.id}`} onClick={() => setEditUser(i)}>Edit</VoidButton>
|
||||
]}/>
|
||||
if (!auth) {
|
||||
return <Navigate to="/login" />;
|
||||
} else {
|
||||
return (
|
||||
<div className="admin">
|
||||
<h2>Users</h2>
|
||||
<UserList
|
||||
actions={(i) => [
|
||||
<VoidButton key={`delete-${i.id}`}>Delete</VoidButton>,
|
||||
<VoidButton key={`edit-${i.id}`} onClick={() => setEditUser(i)}>
|
||||
Edit
|
||||
</VoidButton>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<h2>Files</h2>
|
||||
<ImageGrid loadPage={r => AdminApi.adminListFiles(r)}/>
|
||||
<h2>Files</h2>
|
||||
<ImageGrid loadPage={(r) => AdminApi.adminListFiles(r)} />
|
||||
|
||||
{editUser &&
|
||||
<VoidModal title="Edit user">
|
||||
<EditUser user={editUser} onClose={() => setEditUser(undefined)}/>
|
||||
</VoidModal>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
{editUser && (
|
||||
<VoidModal title="Edit user">
|
||||
<EditUser user={editUser} onClose={() => setEditUser(undefined)} />
|
||||
</VoidModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,68 @@
|
||||
import {useState} from "react";
|
||||
import {useSelector} from "react-redux";
|
||||
import {AdminProfile} from "@void-cat/api";
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AdminProfile } from "@void-cat/api";
|
||||
|
||||
import {VoidButton} from "../Components/Shared/VoidButton";
|
||||
import { VoidButton } from "../Components/Shared/VoidButton";
|
||||
import useApi from "Hooks/UseApi";
|
||||
import {RootState} from "Store";
|
||||
import { RootState } from "Store";
|
||||
|
||||
export default function EditUser({user, onClose}: {user: AdminProfile, onClose: () => void}) {
|
||||
export default function EditUser({
|
||||
user,
|
||||
onClose,
|
||||
}: {
|
||||
user: AdminProfile;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const adminApi = useApi();
|
||||
const fileStores = useSelector(
|
||||
(state: RootState) => state.info?.info?.fileStores ?? ["local-disk"],
|
||||
);
|
||||
const [storage, setStorage] = useState(user.storage);
|
||||
const [email, setEmail] = useState(user.email);
|
||||
|
||||
const adminApi = useApi();
|
||||
const fileStores = useSelector((state: RootState) => state.info?.info?.fileStores ?? ["local-disk"])
|
||||
const [storage, setStorage] = useState(user.storage);
|
||||
const [email, setEmail] = useState(user.email);
|
||||
async function updateUser() {
|
||||
await adminApi.adminUpdateUser({
|
||||
...user,
|
||||
email,
|
||||
storage,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
async function updateUser() {
|
||||
await adminApi.adminUpdateUser({
|
||||
...user,
|
||||
email,
|
||||
storage
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
return (
|
||||
<>
|
||||
Editing user '{user.name}' ({user.id})
|
||||
<dl>
|
||||
<dt>Email:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</dd>
|
||||
|
||||
return (
|
||||
<>
|
||||
Editing user '{user.name}' ({user.id})
|
||||
<dl>
|
||||
<dt>Email:</dt>
|
||||
<dd><input type="text" value={email} onChange={(e) => setEmail(e.target.value)}/></dd>
|
||||
<dt>File storage:</dt>
|
||||
<dd>
|
||||
<select value={storage} onChange={(e) => setStorage(e.target.value)}>
|
||||
<option disabled={true}>Current: {storage}</option>
|
||||
{fileStores.map((e) => (
|
||||
<option key={e}>{e}</option>
|
||||
))}
|
||||
</select>
|
||||
</dd>
|
||||
|
||||
<dt>File storage:</dt>
|
||||
<dd>
|
||||
<select value={storage} onChange={(e) => setStorage(e.target.value)}>
|
||||
<option disabled={true}>Current: {storage}</option>
|
||||
{fileStores.map(e => <option key={e}>{e}</option>)}
|
||||
</select>
|
||||
</dd>
|
||||
|
||||
<dt>Roles:</dt>
|
||||
<dd>{user.roles.map(e => <span className="btn" key={e}>{e}</span>)}</dd>
|
||||
</dl>
|
||||
<VoidButton onClick={() => updateUser()}>Save</VoidButton>
|
||||
<VoidButton onClick={() => onClose()}>Cancel</VoidButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<dt>Roles:</dt>
|
||||
<dd>
|
||||
{user.roles.map((e) => (
|
||||
<span className="btn" key={e}>
|
||||
{e}
|
||||
</span>
|
||||
))}
|
||||
</dd>
|
||||
</dl>
|
||||
<VoidButton onClick={() => updateUser()}>Save</VoidButton>
|
||||
<VoidButton onClick={() => onClose()}>Cancel</VoidButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,93 +1,103 @@
|
||||
import {useDispatch} from "react-redux";
|
||||
import {ReactNode, useEffect, useState} from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import moment from "moment";
|
||||
import {AdminProfile, AdminUserListResult, ApiError, PagedResponse, PagedSortBy, PageSortOrder} from "@void-cat/api";
|
||||
import {
|
||||
AdminProfile,
|
||||
AdminUserListResult,
|
||||
ApiError,
|
||||
PagedResponse,
|
||||
PagedSortBy,
|
||||
PageSortOrder,
|
||||
} from "@void-cat/api";
|
||||
|
||||
import {logout} from "../LoginState";
|
||||
import {PageSelector} from "../Components/Shared/PageSelector";
|
||||
import { logout } from "../LoginState";
|
||||
import { PageSelector } from "../Components/Shared/PageSelector";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
|
||||
interface UserListProps {
|
||||
actions: (u: AdminProfile) => ReactNode
|
||||
actions: (u: AdminProfile) => ReactNode;
|
||||
}
|
||||
|
||||
export function UserList({actions}: UserListProps) {
|
||||
const AdminApi = useApi();
|
||||
const dispatch = useDispatch();
|
||||
export function UserList({ actions }: UserListProps) {
|
||||
const AdminApi = useApi();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [users, setUsers] = useState<PagedResponse<AdminUserListResult>>();
|
||||
const [page, setPage] = useState(0);
|
||||
const pageSize = 10;
|
||||
const [accessDenied, setAccessDenied] = useState<boolean>();
|
||||
const [users, setUsers] = useState<PagedResponse<AdminUserListResult>>();
|
||||
const [page, setPage] = useState(0);
|
||||
const pageSize = 10;
|
||||
const [accessDenied, setAccessDenied] = useState<boolean>();
|
||||
|
||||
async function loadUserList() {
|
||||
try {
|
||||
const pageReq = {
|
||||
page: page,
|
||||
pageSize,
|
||||
sortBy: PagedSortBy.Date,
|
||||
sortOrder: PageSortOrder.Dsc
|
||||
};
|
||||
const rsp = await AdminApi.adminUserList(pageReq);
|
||||
setUsers(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof ApiError) {
|
||||
if (e.statusCode === 401) {
|
||||
dispatch(logout());
|
||||
} else if (e.statusCode === 403) {
|
||||
setAccessDenied(true);
|
||||
}
|
||||
}
|
||||
async function loadUserList() {
|
||||
try {
|
||||
const pageReq = {
|
||||
page: page,
|
||||
pageSize,
|
||||
sortBy: PagedSortBy.Date,
|
||||
sortOrder: PageSortOrder.Dsc,
|
||||
};
|
||||
const rsp = await AdminApi.adminUserList(pageReq);
|
||||
setUsers(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof ApiError) {
|
||||
if (e.statusCode === 401) {
|
||||
dispatch(logout());
|
||||
} else if (e.statusCode === 403) {
|
||||
setAccessDenied(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderUser(r: AdminUserListResult) {
|
||||
return (
|
||||
<tr key={r.user.id}>
|
||||
<td><a href={`/u/${r.user.id}`}>{r.user.name}</a></td>
|
||||
<td>{moment(r.user.created).fromNow()}</td>
|
||||
<td>{moment(r.user.lastLogin).fromNow()}</td>
|
||||
<td>{r.uploads}</td>
|
||||
<td>{actions(r.user)}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadUserList().catch(console.error);
|
||||
}, [page]);
|
||||
|
||||
if (accessDenied === true) {
|
||||
return <h3>Access Denied</h3>;
|
||||
}
|
||||
|
||||
function renderUser(r: AdminUserListResult) {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created</th>
|
||||
<th>Last Login</th>
|
||||
<th>Files</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users && users.results.map(renderUser)}
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{users && <PageSelector
|
||||
onSelectPage={(x) => setPage(x)}
|
||||
page={page}
|
||||
total={users.totalResults}
|
||||
pageSize={pageSize}/>}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<tr key={r.user.id}>
|
||||
<td>
|
||||
<a href={`/u/${r.user.id}`}>{r.user.name}</a>
|
||||
</td>
|
||||
<td>{moment(r.user.created).fromNow()}</td>
|
||||
<td>{moment(r.user.lastLogin).fromNow()}</td>
|
||||
<td>{r.uploads}</td>
|
||||
<td>{actions(r.user)}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadUserList().catch(console.error);
|
||||
}, [page]);
|
||||
|
||||
if (accessDenied === true) {
|
||||
return <h3>Access Denied</h3>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created</th>
|
||||
<th>Last Login</th>
|
||||
<th>Files</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{users && users.results.map(renderUser)}</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{users && (
|
||||
<PageSelector
|
||||
onSelectPage={(x) => setPage(x)}
|
||||
page={page}
|
||||
total={users.totalResults}
|
||||
pageSize={pageSize}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
.page {
|
||||
width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.page {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
.page {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +1,79 @@
|
||||
import './App.css';
|
||||
import "./App.css";
|
||||
|
||||
import {createBrowserRouter, LoaderFunctionArgs, Outlet, RouterProvider} from "react-router-dom";
|
||||
import {Provider} from "react-redux";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
LoaderFunctionArgs,
|
||||
Outlet,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import store from "./Store";
|
||||
import {FilePreview} from "./Pages/FilePreview";
|
||||
import {HomePage} from "./Pages/HomePage";
|
||||
import {Admin} from "./Admin/Admin";
|
||||
import {UserLogin} from "./Pages/UserLogin";
|
||||
import {ProfilePage} from "./Pages/Profile";
|
||||
import {Header} from "./Components/Shared/Header";
|
||||
import {Donate} from "./Pages/Donate";
|
||||
import {VoidApi} from "@void-cat/api";
|
||||
import {ApiHost} from "./Const";
|
||||
|
||||
import { FilePreview } from "./Pages/FilePreview";
|
||||
import { HomePage } from "./Pages/HomePage";
|
||||
import { Admin } from "./Admin/Admin";
|
||||
import { UserLogin } from "./Pages/UserLogin";
|
||||
import { ProfilePage } from "./Pages/Profile";
|
||||
import { Header } from "./Components/Shared/Header";
|
||||
import { Donate } from "./Pages/Donate";
|
||||
import { VoidApi } from "@void-cat/api";
|
||||
import { ApiHost } from "./Const";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
element: <AppLayout/>,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <HomePage/>
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <UserLogin/>
|
||||
},
|
||||
{
|
||||
path: "/u/:id",
|
||||
loader: async ({params}: LoaderFunctionArgs) => {
|
||||
const api = new VoidApi(ApiHost, store.getState().login.jwt);
|
||||
if(params.id) {
|
||||
return await api.getUser(params.id);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
element: <ProfilePage/>
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
element: <Admin/>
|
||||
},
|
||||
{
|
||||
path: "/:id",
|
||||
element: <FilePreview/>
|
||||
},
|
||||
{
|
||||
path: "/donate",
|
||||
element: <Donate/>
|
||||
{
|
||||
element: <AppLayout />,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <HomePage />,
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <UserLogin />,
|
||||
},
|
||||
{
|
||||
path: "/u/:id",
|
||||
loader: async ({ params }: LoaderFunctionArgs) => {
|
||||
const state = store.getState();
|
||||
const api = new VoidApi(ApiHost, state.login.jwt ? () => Promise.resolve(`Bearer ${state.login.jwt}`) : undefined);
|
||||
if (params.id) {
|
||||
try {
|
||||
return await api.getUser(params.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
}
|
||||
return null;
|
||||
},
|
||||
element: <ProfilePage />,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
element: <Admin />,
|
||||
},
|
||||
{
|
||||
path: "/:id",
|
||||
element: <FilePreview />,
|
||||
},
|
||||
{
|
||||
path: "/donate",
|
||||
element: <Donate />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export function AppLayout() {
|
||||
return (
|
||||
<div className="app">
|
||||
<Provider store={store}>
|
||||
<Header/>
|
||||
<Outlet/>
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="app">
|
||||
<Provider store={store}>
|
||||
<Header />
|
||||
<Outlet />
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router}/>
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
.file-edit {
|
||||
text-align: start;
|
||||
margin: 0 10px;
|
||||
text-align: start;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.file-edit svg {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.file-edit {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.file-edit {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
@ -1,105 +1,148 @@
|
||||
import "./FileEdit.css";
|
||||
import {useState} from "react";
|
||||
import {useSelector} from "react-redux";
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import moment from "moment";
|
||||
import {PaymentServices, SetPaymentConfigRequest, VoidFileResponse} from "@void-cat/api";
|
||||
import {
|
||||
PaymentServices,
|
||||
SetPaymentConfigRequest,
|
||||
VoidFileResponse,
|
||||
} from "@void-cat/api";
|
||||
|
||||
import {StrikePaymentConfig} from "./StrikePaymentConfig";
|
||||
import {NoPaymentConfig} from "./NoPaymentConfig";
|
||||
import {VoidButton} from "../Shared/VoidButton";
|
||||
import { StrikePaymentConfig } from "./StrikePaymentConfig";
|
||||
import { NoPaymentConfig } from "./NoPaymentConfig";
|
||||
import { VoidButton } from "../Shared/VoidButton";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
import {RootState} from "Store";
|
||||
import { RootState } from "Store";
|
||||
|
||||
interface FileEditProps {
|
||||
file: VoidFileResponse
|
||||
file: VoidFileResponse;
|
||||
}
|
||||
|
||||
export function FileEdit({file}: FileEditProps) {
|
||||
const Api = useApi();
|
||||
const profile = useSelector((s: RootState) => s.login.profile);
|
||||
const [payment, setPayment] = useState(file.payment?.service);
|
||||
const [name, setName] = useState(file.metadata?.name ?? "");
|
||||
const [description, setDescription] = useState(file.metadata?.description ?? "");
|
||||
const [expiry, setExpiry] = useState<number | undefined>(file.metadata?.expires ? moment(file.metadata?.expires).unix() * 1000 : undefined);
|
||||
export function FileEdit({ file }: FileEditProps) {
|
||||
const Api = useApi();
|
||||
const profile = useSelector((s: RootState) => s.login.profile);
|
||||
const [payment, setPayment] = useState(file.payment?.service);
|
||||
const [name, setName] = useState(file.metadata?.name ?? "");
|
||||
const [description, setDescription] = useState(
|
||||
file.metadata?.description ?? "",
|
||||
);
|
||||
const [expiry, setExpiry] = useState<number | undefined>(
|
||||
file.metadata?.expires
|
||||
? moment(file.metadata?.expires).unix() * 1000
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const localFile = window.localStorage.getItem(file.id);
|
||||
const privateFile: VoidFileResponse = profile?.id === file.uploader?.id
|
||||
? file
|
||||
: localFile ? JSON.parse(localFile) : undefined;
|
||||
if (!privateFile?.metadata?.editSecret) {
|
||||
return null;
|
||||
const localFile = window.localStorage.getItem(file.id);
|
||||
const privateFile: VoidFileResponse =
|
||||
profile?.id === file.uploader?.id
|
||||
? file
|
||||
: localFile
|
||||
? JSON.parse(localFile)
|
||||
: undefined;
|
||||
if (!privateFile?.metadata?.editSecret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function savePaymentConfig(cfg: SetPaymentConfigRequest) {
|
||||
try {
|
||||
await Api.setPaymentConfig(file.id, cfg);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePaymentConfig(cfg: SetPaymentConfigRequest) {
|
||||
try {
|
||||
await Api.setPaymentConfig(file.id, cfg);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
async function saveMeta() {
|
||||
const meta = {
|
||||
name,
|
||||
description,
|
||||
editSecret: privateFile?.metadata?.editSecret,
|
||||
expires: moment(expiry).toISOString(),
|
||||
};
|
||||
await Api.updateFileMetadata(file.id, meta);
|
||||
}
|
||||
|
||||
function renderPaymentConfig() {
|
||||
switch (payment) {
|
||||
case PaymentServices.None: {
|
||||
return (
|
||||
<NoPaymentConfig
|
||||
privateFile={privateFile}
|
||||
onSaveConfig={savePaymentConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case PaymentServices.Strike: {
|
||||
return (
|
||||
<StrikePaymentConfig
|
||||
file={file}
|
||||
privateFile={privateFile}
|
||||
onSaveConfig={savePaymentConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function saveMeta() {
|
||||
const meta = {
|
||||
name,
|
||||
description,
|
||||
editSecret: privateFile?.metadata?.editSecret,
|
||||
expires: moment(expiry).toISOString()
|
||||
};
|
||||
await Api.updateFileMetadata(file.id, meta);
|
||||
}
|
||||
|
||||
function renderPaymentConfig() {
|
||||
switch (payment) {
|
||||
case PaymentServices.None: {
|
||||
return <NoPaymentConfig privateFile={privateFile} onSaveConfig={savePaymentConfig}/>;
|
||||
}
|
||||
case PaymentServices.Strike: {
|
||||
return <StrikePaymentConfig file={file} privateFile={privateFile} onSaveConfig={savePaymentConfig}/>
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-edit flex">
|
||||
<div className="flx-1">
|
||||
<h3>File info</h3>
|
||||
<dl>
|
||||
<dt>Filename:</dt>
|
||||
<dd><input type="text" value={name} onChange={(e) => setName(e.target.value)}/></dd>
|
||||
<dt>Description:</dt>
|
||||
<dd><input type="text" value={description} onChange={(e) => setDescription(e.target.value)}/></dd>
|
||||
<dt>Expiry</dt>
|
||||
<dd>
|
||||
<input type="datetime-local"
|
||||
value={expiry ? moment(expiry).toISOString().replace("Z", "") : ""}
|
||||
max={moment.utc().add(1, "year").toISOString().replace("Z", "")}
|
||||
min={moment.utc().toISOString().replace("Z", "")}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.length > 0) {
|
||||
setExpiry(moment.utc(e.target.value).unix() * 1000);
|
||||
} else {
|
||||
setExpiry(undefined);
|
||||
}
|
||||
}}/>
|
||||
</dd>
|
||||
</dl>
|
||||
<VoidButton onClick={() => saveMeta()} options={{showSuccess: true}}>
|
||||
Save
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div className="flx-1">
|
||||
<h3>Payment Config</h3>
|
||||
Type:
|
||||
<select onChange={(e) => setPayment(parseInt(e.target.value))} value={payment}>
|
||||
<option value={0}>None</option>
|
||||
<option value={1}>Strike</option>
|
||||
</select>
|
||||
{renderPaymentConfig()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="file-edit flex">
|
||||
<div className="flx-1">
|
||||
<h3>File info</h3>
|
||||
<dl>
|
||||
<dt>Filename:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</dd>
|
||||
<dt>Description:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</dd>
|
||||
<dt>Expiry</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={
|
||||
expiry ? moment(expiry).toISOString().replace("Z", "") : ""
|
||||
}
|
||||
max={moment.utc().add(1, "year").toISOString().replace("Z", "")}
|
||||
min={moment.utc().toISOString().replace("Z", "")}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.length > 0) {
|
||||
setExpiry(moment.utc(e.target.value).unix() * 1000);
|
||||
} else {
|
||||
setExpiry(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
<VoidButton onClick={() => saveMeta()} options={{ showSuccess: true }}>
|
||||
Save
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div className="flx-1">
|
||||
<h3>Payment Config</h3>
|
||||
Type:
|
||||
<select
|
||||
onChange={(e) => setPayment(parseInt(e.target.value))}
|
||||
value={payment}
|
||||
>
|
||||
<option value={0}>None</option>
|
||||
<option value={1}>Strike</option>
|
||||
</select>
|
||||
{renderPaymentConfig()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,29 +1,36 @@
|
||||
import React from "react";
|
||||
import {VoidButton} from "../Shared/VoidButton";
|
||||
import {PaymentCurrencies, SetPaymentConfigRequest, VoidFileResponse} from "@void-cat/api";
|
||||
import { VoidButton } from "../Shared/VoidButton";
|
||||
import {
|
||||
PaymentCurrencies,
|
||||
SetPaymentConfigRequest,
|
||||
VoidFileResponse,
|
||||
} from "@void-cat/api";
|
||||
|
||||
interface NoPaymentConfigProps {
|
||||
privateFile: VoidFileResponse
|
||||
onSaveConfig: (c: SetPaymentConfigRequest) => Promise<any>
|
||||
privateFile: VoidFileResponse;
|
||||
onSaveConfig: (c: SetPaymentConfigRequest) => Promise<any>;
|
||||
}
|
||||
|
||||
export function NoPaymentConfig({privateFile, onSaveConfig}: NoPaymentConfigProps) {
|
||||
async function saveConfig() {
|
||||
const cfg = {
|
||||
editSecret: privateFile.metadata!.editSecret,
|
||||
required: false,
|
||||
amount: 0,
|
||||
currency: PaymentCurrencies.BTC
|
||||
} as SetPaymentConfigRequest;
|
||||
export function NoPaymentConfig({
|
||||
privateFile,
|
||||
onSaveConfig,
|
||||
}: NoPaymentConfigProps) {
|
||||
async function saveConfig() {
|
||||
const cfg = {
|
||||
editSecret: privateFile.metadata!.editSecret,
|
||||
required: false,
|
||||
amount: 0,
|
||||
currency: PaymentCurrencies.BTC,
|
||||
} as SetPaymentConfigRequest;
|
||||
|
||||
await onSaveConfig(cfg)
|
||||
}
|
||||
await onSaveConfig(cfg);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VoidButton onClick={saveConfig} options={{showSuccess: true}}>
|
||||
Save
|
||||
</VoidButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<VoidButton onClick={saveConfig} options={{ showSuccess: true }}>
|
||||
Save
|
||||
</VoidButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,57 +1,88 @@
|
||||
import {useState} from "react";
|
||||
import {VoidButton} from "../Shared/VoidButton";
|
||||
import { useState } from "react";
|
||||
import { VoidButton } from "../Shared/VoidButton";
|
||||
|
||||
import {PaymentCurrencies, SetPaymentConfigRequest, VoidFileResponse} from "@void-cat/api";
|
||||
import {
|
||||
PaymentCurrencies,
|
||||
SetPaymentConfigRequest,
|
||||
VoidFileResponse,
|
||||
} from "@void-cat/api";
|
||||
|
||||
interface StrikePaymentConfigProps {
|
||||
file: VoidFileResponse
|
||||
privateFile: VoidFileResponse
|
||||
onSaveConfig: (cfg: SetPaymentConfigRequest) => Promise<any>
|
||||
file: VoidFileResponse;
|
||||
privateFile: VoidFileResponse;
|
||||
onSaveConfig: (cfg: SetPaymentConfigRequest) => Promise<any>;
|
||||
}
|
||||
|
||||
export function StrikePaymentConfig({file, privateFile, onSaveConfig}: StrikePaymentConfigProps) {
|
||||
const payment = file.payment;
|
||||
const editSecret = privateFile.metadata!.editSecret;
|
||||
export function StrikePaymentConfig({
|
||||
file,
|
||||
privateFile,
|
||||
onSaveConfig,
|
||||
}: StrikePaymentConfigProps) {
|
||||
const payment = file.payment;
|
||||
const editSecret = privateFile.metadata!.editSecret;
|
||||
|
||||
const [username, setUsername] = useState(payment?.strikeHandle ?? "hrf");
|
||||
const [currency, setCurrency] = useState(payment?.currency ?? PaymentCurrencies.USD);
|
||||
const [price, setPrice] = useState(payment?.amount ?? 1);
|
||||
const [required, setRequired] = useState(payment?.required);
|
||||
const [username, setUsername] = useState(payment?.strikeHandle ?? "hrf");
|
||||
const [currency, setCurrency] = useState(
|
||||
payment?.currency ?? PaymentCurrencies.USD,
|
||||
);
|
||||
const [price, setPrice] = useState(payment?.amount ?? 1);
|
||||
const [required, setRequired] = useState(payment?.required);
|
||||
|
||||
async function saveStrikeConfig() {
|
||||
const cfg = {
|
||||
editSecret,
|
||||
strikeHandle: username,
|
||||
currency,
|
||||
amount: price,
|
||||
required
|
||||
} as SetPaymentConfigRequest;
|
||||
async function saveStrikeConfig() {
|
||||
const cfg = {
|
||||
editSecret,
|
||||
strikeHandle: username,
|
||||
currency,
|
||||
amount: price,
|
||||
required,
|
||||
} as SetPaymentConfigRequest;
|
||||
|
||||
await onSaveConfig(cfg)
|
||||
}
|
||||
await onSaveConfig(cfg);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<dl>
|
||||
<dt>Strike username:</dt>
|
||||
<dd><input type="text" value={username} onChange={(e) => setUsername(e.target.value)}/></dd>
|
||||
<dt>Currency:</dt>
|
||||
<dd>
|
||||
<select onChange={(e) => setCurrency(parseInt(e.target.value))} value={currency}>
|
||||
<option value={PaymentCurrencies.BTC}>BTC</option>
|
||||
<option value={PaymentCurrencies.USD}>USD</option>
|
||||
<option value={PaymentCurrencies.EUR}>EUR</option>
|
||||
<option value={PaymentCurrencies.GBP}>GBP</option>
|
||||
</select>
|
||||
</dd>
|
||||
<dt>Price:</dt>
|
||||
<dd><input type="number" value={price} onChange={(e) => setPrice(Number(e.target.value))}/></dd>
|
||||
<dt>Required:</dt>
|
||||
<dd><input type="checkbox" checked={required} onChange={(e) => setRequired(e.target.checked)}/></dd>
|
||||
</dl>
|
||||
<VoidButton onClick={saveStrikeConfig} options={{showSuccess: true}}>
|
||||
Save
|
||||
</VoidButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<dl>
|
||||
<dt>Strike username:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</dd>
|
||||
<dt>Currency:</dt>
|
||||
<dd>
|
||||
<select
|
||||
onChange={(e) => setCurrency(parseInt(e.target.value))}
|
||||
value={currency}
|
||||
>
|
||||
<option value={PaymentCurrencies.BTC}>BTC</option>
|
||||
<option value={PaymentCurrencies.USD}>USD</option>
|
||||
<option value={PaymentCurrencies.EUR}>EUR</option>
|
||||
<option value={PaymentCurrencies.GBP}>GBP</option>
|
||||
</select>
|
||||
</dd>
|
||||
<dt>Price:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(Number(e.target.value))}
|
||||
/>
|
||||
</dd>
|
||||
<dt>Required:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={required}
|
||||
onChange={(e) => setRequired(e.target.checked)}
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
<VoidButton onClick={saveStrikeConfig} options={{ showSuccess: true }}>
|
||||
Save
|
||||
</VoidButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
.payment {
|
||||
text-align: center;
|
||||
border: 1px solid lightgreen;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
text-align: center;
|
||||
border: 1px solid lightgreen;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
@ -1,67 +1,73 @@
|
||||
import "./FilePayment.css";
|
||||
import {useState} from "react";
|
||||
import {LightningPayment} from "./LightningPayment";
|
||||
import {VoidButton} from "../Shared/VoidButton";
|
||||
import {PaymentOrder, PaymentServices, VoidFileResponse} from "@void-cat/api";
|
||||
import { useState } from "react";
|
||||
import { LightningPayment } from "./LightningPayment";
|
||||
import { VoidButton } from "../Shared/VoidButton";
|
||||
import { PaymentOrder, PaymentServices, VoidFileResponse } from "@void-cat/api";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
import {FormatCurrency} from "Util";
|
||||
import { FormatCurrency } from "Util";
|
||||
|
||||
interface FilePaymentProps {
|
||||
file: VoidFileResponse
|
||||
onPaid: () => Promise<void>
|
||||
file: VoidFileResponse;
|
||||
onPaid: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function FilePayment({file, onPaid}: FilePaymentProps) {
|
||||
const Api = useApi();
|
||||
const paymentKey = `payment-${file.id}`;
|
||||
const [order, setOrder] = useState<any>();
|
||||
export function FilePayment({ file, onPaid }: FilePaymentProps) {
|
||||
const Api = useApi();
|
||||
const paymentKey = `payment-${file.id}`;
|
||||
const [order, setOrder] = useState<any>();
|
||||
|
||||
// Payment not required
|
||||
if (!file.payment) return null;
|
||||
// Payment not required
|
||||
if (!file.payment) return null;
|
||||
|
||||
async function fetchOrder() {
|
||||
try {
|
||||
const rsp = await Api.createOrder(file.id);
|
||||
setOrder(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
async function fetchOrder() {
|
||||
try {
|
||||
const rsp = await Api.createOrder(file.id);
|
||||
setOrder(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setOrder(undefined);
|
||||
function reset() {
|
||||
setOrder(undefined);
|
||||
}
|
||||
|
||||
function handlePaid(order: PaymentOrder) {
|
||||
window.localStorage.setItem(paymentKey, JSON.stringify(order));
|
||||
if (typeof onPaid === "function") {
|
||||
onPaid();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaid(order: PaymentOrder) {
|
||||
window.localStorage.setItem(paymentKey, JSON.stringify(order));
|
||||
if (typeof onPaid === "function") {
|
||||
onPaid();
|
||||
}
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
const amountString = FormatCurrency(file.payment.amount, file.payment.currency);
|
||||
if (file.payment.required) {
|
||||
return (
|
||||
<div className="payment">
|
||||
<h3>
|
||||
You must pay {amountString} to view this file.
|
||||
</h3>
|
||||
<VoidButton onClick={fetchOrder}>Pay</VoidButton>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<VoidButton onClick={fetchOrder}>Tip {amountString}</VoidButton>
|
||||
);
|
||||
}
|
||||
if (!order) {
|
||||
const amountString = FormatCurrency(
|
||||
file.payment.amount,
|
||||
file.payment.currency,
|
||||
);
|
||||
if (file.payment.required) {
|
||||
return (
|
||||
<div className="payment">
|
||||
<h3>You must pay {amountString} to view this file.</h3>
|
||||
<VoidButton onClick={fetchOrder}>Pay</VoidButton>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
switch (file.payment.service) {
|
||||
case PaymentServices.Strike: {
|
||||
return <LightningPayment file={file} order={order} onReset={reset} onPaid={handlePaid}/>;
|
||||
}
|
||||
}
|
||||
return <VoidButton onClick={fetchOrder}>Tip {amountString}</VoidButton>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
switch (file.payment.service) {
|
||||
case PaymentServices.Strike: {
|
||||
return (
|
||||
<LightningPayment
|
||||
file={file}
|
||||
order={order}
|
||||
onReset={reset}
|
||||
onPaid={handlePaid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,51 +1,59 @@
|
||||
import QRCode from "qrcode.react";
|
||||
import {useEffect} from "react";
|
||||
import {PaymentOrder, PaymentOrderState, VoidFileResponse} from "@void-cat/api";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
PaymentOrder,
|
||||
PaymentOrderState,
|
||||
VoidFileResponse,
|
||||
} from "@void-cat/api";
|
||||
|
||||
import {Countdown} from "../Shared/Countdown";
|
||||
import { Countdown } from "../Shared/Countdown";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
|
||||
interface LightningPaymentProps {
|
||||
file: VoidFileResponse
|
||||
order: PaymentOrder
|
||||
onPaid: (s: PaymentOrder) => void
|
||||
onReset: () => void
|
||||
file: VoidFileResponse;
|
||||
order: PaymentOrder;
|
||||
onPaid: (s: PaymentOrder) => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export function LightningPayment({file, order, onPaid, onReset}: LightningPaymentProps) {
|
||||
const Api = useApi();
|
||||
const link = `lightning:${order.orderLightning?.invoice}`;
|
||||
export function LightningPayment({
|
||||
file,
|
||||
order,
|
||||
onPaid,
|
||||
onReset,
|
||||
}: LightningPaymentProps) {
|
||||
const Api = useApi();
|
||||
const link = `lightning:${order.orderLightning?.invoice}`;
|
||||
|
||||
function openInvoice() {
|
||||
const a = document.createElement("a");
|
||||
a.href = link;
|
||||
a.click();
|
||||
function openInvoice() {
|
||||
const a = document.createElement("a");
|
||||
a.href = link;
|
||||
a.click();
|
||||
}
|
||||
|
||||
async function checkStatus() {
|
||||
const os = await Api.getOrder(file.id, order.id);
|
||||
if (os.status === PaymentOrderState.Paid && typeof onPaid === "function") {
|
||||
onPaid(os);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStatus() {
|
||||
const os = await Api.getOrder(file.id, order.id);
|
||||
if (os.status === PaymentOrderState.Paid && typeof onPaid === "function") {
|
||||
onPaid(os);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
let t = setInterval(checkStatus, 2500);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let t = setInterval(checkStatus, 2500);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="lightning-invoice" onClick={openInvoice}>
|
||||
<h1>Pay with Lightning ⚡</h1>
|
||||
<QRCode
|
||||
value={link}
|
||||
size={512}
|
||||
includeMargin={true}/>
|
||||
<dl>
|
||||
<dt>Expires:</dt>
|
||||
<dd><Countdown to={order.orderLightning!.expire} onEnded={onReset}/></dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="lightning-invoice" onClick={openInvoice}>
|
||||
<h1>Pay with Lightning ⚡</h1>
|
||||
<QRCode value={link} size={512} includeMargin={true} />
|
||||
<dl>
|
||||
<dt>Expires:</dt>
|
||||
<dd>
|
||||
<Countdown to={order.orderLightning!.expire} onEnded={onReset} />
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
.text-preview {
|
||||
border: 1px dashed;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
text-align: initial;
|
||||
overflow: auto;
|
||||
}
|
||||
border: 1px dashed;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
text-align: initial;
|
||||
overflow: auto;
|
||||
}
|
||||
|
@ -1,30 +1,28 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./TextPreview.css";
|
||||
|
||||
export function TextPreview({link}: { link: string }) {
|
||||
let [content, setContent] = useState("Loading..");
|
||||
export function TextPreview({ link }: { link: string }) {
|
||||
let [content, setContent] = useState("Loading..");
|
||||
|
||||
async function getContent(link: string) {
|
||||
let req = await fetch(`${link}?t=${new Date().getTime()}`, {
|
||||
headers: {
|
||||
"pragma": "no-cache",
|
||||
"cache-control": "no-cache"
|
||||
}
|
||||
});
|
||||
if (req.ok) {
|
||||
setContent(await req.text());
|
||||
} else {
|
||||
setContent("ERROR :(")
|
||||
}
|
||||
async function getContent(link: string) {
|
||||
let req = await fetch(`${link}?t=${new Date().getTime()}`, {
|
||||
headers: {
|
||||
pragma: "no-cache",
|
||||
"cache-control": "no-cache",
|
||||
},
|
||||
});
|
||||
if (req.ok) {
|
||||
setContent(await req.text());
|
||||
} else {
|
||||
setContent("ERROR :(");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (link !== undefined && link !== "#") {
|
||||
getContent(link).catch(console.error);
|
||||
}
|
||||
}, [link]);
|
||||
useEffect(() => {
|
||||
if (link !== undefined && link !== "#") {
|
||||
getContent(link).catch(console.error);
|
||||
}
|
||||
}, [link]);
|
||||
|
||||
return (
|
||||
<pre className="text-preview">{content}</pre>
|
||||
)
|
||||
}
|
||||
return <pre className="text-preview">{content}</pre>;
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
.drop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 20px;
|
||||
border: 2px dashed;
|
||||
margin: 5vh 2px 2px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
height: 250px;
|
||||
font-size: x-large;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 20px;
|
||||
border: 2px dashed;
|
||||
margin: 5vh 2px 2px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
height: 250px;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
.drop small {
|
||||
display: block;
|
||||
font-size: x-small;
|
||||
}
|
||||
display: block;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
@ -1,65 +1,64 @@
|
||||
import "./Dropzone.css";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {FileUpload} from "./FileUpload";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { FileUpload } from "./FileUpload";
|
||||
|
||||
export function Dropzone() {
|
||||
let [files, setFiles] = useState<Array<File>>([]);
|
||||
let [files, setFiles] = useState<Array<File>>([]);
|
||||
|
||||
function selectFiles() {
|
||||
let i = document.createElement('input');
|
||||
i.setAttribute('type', 'file');
|
||||
i.setAttribute('multiple', '');
|
||||
i.addEventListener('change', function (evt) {
|
||||
if (evt.target && "files" in evt.target) {
|
||||
setFiles(evt.target.files as Array<File>);
|
||||
}
|
||||
});
|
||||
i.click();
|
||||
function selectFiles() {
|
||||
let i = document.createElement("input");
|
||||
i.setAttribute("type", "file");
|
||||
i.setAttribute("multiple", "");
|
||||
i.addEventListener("change", function (evt) {
|
||||
if (evt.target && "files" in evt.target) {
|
||||
setFiles(evt.target.files as Array<File>);
|
||||
}
|
||||
});
|
||||
i.click();
|
||||
}
|
||||
|
||||
function dropFiles(e: DragEvent | ClipboardEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if ("dataTransfer" in e && (e.dataTransfer?.files?.length ?? 0) > 0) {
|
||||
setFiles([...e.dataTransfer!.files]);
|
||||
} else if (
|
||||
"clipboardData" in e &&
|
||||
(e.clipboardData?.files?.length ?? 0) > 0
|
||||
) {
|
||||
setFiles([...e.clipboardData!.files]);
|
||||
}
|
||||
}
|
||||
|
||||
function dropFiles(e: DragEvent | ClipboardEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if ("dataTransfer" in e && (e.dataTransfer?.files?.length ?? 0) > 0) {
|
||||
setFiles([...e.dataTransfer!.files]);
|
||||
} else if ("clipboardData" in e && (e.clipboardData?.files?.length ?? 0) > 0) {
|
||||
setFiles([...e.clipboardData!.files]);
|
||||
}
|
||||
function renderUploads() {
|
||||
let fElm = [];
|
||||
for (let f of files) {
|
||||
fElm.push(<FileUpload file={f} key={f.name} />);
|
||||
}
|
||||
return <Fragment>{fElm}</Fragment>;
|
||||
}
|
||||
|
||||
function renderUploads() {
|
||||
let fElm = [];
|
||||
for (let f of files) {
|
||||
fElm.push(<FileUpload file={f} key={f.name}/>);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
{fElm}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
function renderDrop() {
|
||||
return (
|
||||
<div className="drop" onClick={selectFiles}>
|
||||
<div>
|
||||
Click me!
|
||||
<small>Or drop files here</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDrop() {
|
||||
return (
|
||||
<div className="drop" onClick={selectFiles}>
|
||||
<div>
|
||||
Click me!
|
||||
<small>Or drop files here</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
document.addEventListener("paste", dropFiles);
|
||||
document.addEventListener("drop", dropFiles);
|
||||
document.addEventListener("dragover", dropFiles);
|
||||
return () => {
|
||||
document.removeEventListener("paste", dropFiles);
|
||||
document.removeEventListener("drop", dropFiles);
|
||||
document.removeEventListener("dragover", dropFiles);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("paste", dropFiles);
|
||||
document.addEventListener("drop", dropFiles);
|
||||
document.addEventListener("dragover", dropFiles);
|
||||
return () => {
|
||||
document.removeEventListener("paste", dropFiles);
|
||||
document.removeEventListener("drop", dropFiles);
|
||||
document.removeEventListener("dragover", dropFiles);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return files.length === 0 ? renderDrop() : renderUploads();
|
||||
}
|
||||
return files.length === 0 ? renderDrop() : renderUploads();
|
||||
}
|
||||
|
@ -1,41 +1,41 @@
|
||||
.upload {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border: 1px solid grey;
|
||||
border-radius: 10px;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border: 1px solid grey;
|
||||
border-radius: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.upload {
|
||||
flex-direction: column;
|
||||
}
|
||||
.upload {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.upload .info {
|
||||
flex: 2;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.upload .status {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.upload dt {
|
||||
font-size: 12px;
|
||||
color: grey;
|
||||
font-size: 12px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.upload .iframe-challenge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.upload .iframe-challenge iframe {
|
||||
margin-left: 10vw;
|
||||
width: 80vw;
|
||||
height: 100vh;
|
||||
}
|
||||
margin-left: 10vw;
|
||||
width: 80vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
@ -1,131 +1,159 @@
|
||||
import "./FileUpload.css";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useSelector} from "react-redux";
|
||||
import {UploadState, VoidFileResponse} from "@void-cat/api";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { UploadState, VoidFileResponse } from "@void-cat/api";
|
||||
|
||||
import {VoidButton} from "../Shared/VoidButton";
|
||||
import {useFileTransfer} from "../Shared/FileTransferHook";
|
||||
import { VoidButton } from "../Shared/VoidButton";
|
||||
import { useFileTransfer } from "../Shared/FileTransferHook";
|
||||
|
||||
import {RootState} from "Store";
|
||||
import {ConstName, FormatBytes} from "Util";
|
||||
import { RootState } from "Store";
|
||||
import { ConstName, FormatBytes } from "Util";
|
||||
import useApi from "Hooks/UseApi";
|
||||
|
||||
interface FileUploadProps {
|
||||
file: File | Blob
|
||||
file: File | Blob;
|
||||
}
|
||||
|
||||
export function FileUpload({file}: FileUploadProps) {
|
||||
const info = useSelector((s: RootState) => s.info.info);
|
||||
const {speed, progress, loaded, setFileSize, reset} = useFileTransfer();
|
||||
const Api = useApi();
|
||||
const [result, setResult] = useState<VoidFileResponse>();
|
||||
const [error, setError] = useState("");
|
||||
const [uState, setUState] = useState(UploadState.NotStarted);
|
||||
const [challenge, setChallenge] = useState("");
|
||||
const [encryptionKey, setEncryptionKey] = useState("");
|
||||
const [encrypt, setEncrypt] = useState(true);
|
||||
export function FileUpload({ file }: FileUploadProps) {
|
||||
const info = useSelector((s: RootState) => s.info.info);
|
||||
const { speed, progress, loaded, setFileSize, reset } = useFileTransfer();
|
||||
const Api = useApi();
|
||||
const [result, setResult] = useState<VoidFileResponse>();
|
||||
const [error, setError] = useState("");
|
||||
const [uState, setUState] = useState(UploadState.NotStarted);
|
||||
const [challenge, setChallenge] = useState("");
|
||||
const [encryptionKey, setEncryptionKey] = useState("");
|
||||
const [encrypt, setEncrypt] = useState(true);
|
||||
|
||||
const uploader = useMemo(() => {
|
||||
return Api.getUploader(file, setUState, loaded, setChallenge, info?.uploadSegmentSize);
|
||||
}, [Api, file]);
|
||||
|
||||
useEffect(() => {
|
||||
uploader.setEncryption(encrypt);
|
||||
}, [uploader, encrypt]);
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
setFileSize(file.size);
|
||||
if (!uploader.canEncrypt() && uState === UploadState.NotStarted) {
|
||||
startUpload().catch(console.error)
|
||||
}
|
||||
}, [file, uploader, uState]);
|
||||
|
||||
async function startUpload() {
|
||||
setUState(UploadState.Starting);
|
||||
try {
|
||||
const result = await uploader.upload();
|
||||
console.debug(result);
|
||||
if (result.ok) {
|
||||
setUState(UploadState.Done);
|
||||
setResult(result.file);
|
||||
setEncryptionKey(uploader.getEncryptionKey() ?? "");
|
||||
window.localStorage.setItem(result.file!.id, JSON.stringify(result.file!));
|
||||
} else {
|
||||
setUState(UploadState.Failed);
|
||||
setError(result.errorMessage!);
|
||||
}
|
||||
} catch (e) {
|
||||
setUState(UploadState.Failed);
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError("Unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatus() {
|
||||
if (result && uState === UploadState.Done) {
|
||||
let link = `/${result.id}`;
|
||||
return (<dl>
|
||||
<dt>Link:</dt>
|
||||
<dd><a target="_blank" href={link} rel="noreferrer">{result.id}</a></dd>
|
||||
{encryptionKey ? <>
|
||||
<dt>Encryption Key:</dt>
|
||||
<dd>
|
||||
<VoidButton onClick={() => navigator.clipboard.writeText(encryptionKey)}>Copy</VoidButton>
|
||||
</dd>
|
||||
</> : null}
|
||||
</dl>)
|
||||
} else if (uState === UploadState.NotStarted) {
|
||||
return (
|
||||
<>
|
||||
<dl>
|
||||
<dt>Encrypt file:</dt>
|
||||
<dd><input type="checkbox" checked={encrypt} onChange={(e) => setEncrypt(e.target.checked)}/>
|
||||
</dd>
|
||||
</dl>
|
||||
<VoidButton onClick={() => startUpload()}>Upload</VoidButton>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<dl>
|
||||
<dt>Speed:</dt>
|
||||
<dd>{FormatBytes(speed)}/s</dd>
|
||||
<dt>Progress:</dt>
|
||||
<dd>{(progress * 100).toFixed(0)}%</dd>
|
||||
<dt>Status:</dt>
|
||||
<dd>{error ? error : ConstName(UploadState, uState)}</dd>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getChallengeElement() {
|
||||
let elm = document.createElement("iframe");
|
||||
elm.contentWindow?.document.write(challenge);
|
||||
return <div dangerouslySetInnerHTML={{__html: elm.outerHTML}}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="upload">
|
||||
<div className="info">
|
||||
<dl>
|
||||
<dt>Name:</dt>
|
||||
<dd>{file.name}</dd>
|
||||
<dt>Size:</dt>
|
||||
<dd>{FormatBytes(file.size)}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div className="status">
|
||||
{renderStatus()}
|
||||
</div>
|
||||
{uState === UploadState.Challenge &&
|
||||
<div className="iframe-challenge" onClick={() => window.location.reload()}>
|
||||
{getChallengeElement()}
|
||||
</div>}
|
||||
</div>
|
||||
const uploader = useMemo(() => {
|
||||
return Api.getUploader(
|
||||
file,
|
||||
setUState,
|
||||
loaded,
|
||||
setChallenge,
|
||||
info?.uploadSegmentSize,
|
||||
);
|
||||
}
|
||||
}, [Api, file]);
|
||||
|
||||
useEffect(() => {
|
||||
uploader.setEncryption(encrypt);
|
||||
}, [uploader, encrypt]);
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
setFileSize(file.size);
|
||||
if (!uploader.canEncrypt() && uState === UploadState.NotStarted) {
|
||||
startUpload().catch(console.error);
|
||||
}
|
||||
}, [file, uploader, uState]);
|
||||
|
||||
async function startUpload() {
|
||||
setUState(UploadState.Starting);
|
||||
try {
|
||||
const result = await uploader.upload();
|
||||
console.debug(result);
|
||||
if (result.ok) {
|
||||
setUState(UploadState.Done);
|
||||
setResult(result.file);
|
||||
setEncryptionKey(uploader.getEncryptionKey() ?? "");
|
||||
window.localStorage.setItem(
|
||||
result.file!.id,
|
||||
JSON.stringify(result.file!),
|
||||
);
|
||||
} else {
|
||||
setUState(UploadState.Failed);
|
||||
setError(result.errorMessage!);
|
||||
}
|
||||
} catch (e) {
|
||||
setUState(UploadState.Failed);
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError("Unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatus() {
|
||||
if (result && uState === UploadState.Done) {
|
||||
let link = `/${result.id}`;
|
||||
return (
|
||||
<dl>
|
||||
<dt>Link:</dt>
|
||||
<dd>
|
||||
<a target="_blank" href={link} rel="noreferrer">
|
||||
{result.id}
|
||||
</a>
|
||||
</dd>
|
||||
{encryptionKey ? (
|
||||
<>
|
||||
<dt>Encryption Key:</dt>
|
||||
<dd>
|
||||
<VoidButton
|
||||
onClick={() => navigator.clipboard.writeText(encryptionKey)}
|
||||
>
|
||||
Copy
|
||||
</VoidButton>
|
||||
</dd>
|
||||
</>
|
||||
) : null}
|
||||
</dl>
|
||||
);
|
||||
} else if (uState === UploadState.NotStarted) {
|
||||
return (
|
||||
<>
|
||||
<dl>
|
||||
<dt>Encrypt file:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={encrypt}
|
||||
onChange={(e) => setEncrypt(e.target.checked)}
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
<VoidButton onClick={() => startUpload()}>Upload</VoidButton>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<dl>
|
||||
<dt>Speed:</dt>
|
||||
<dd>{FormatBytes(speed)}/s</dd>
|
||||
<dt>Progress:</dt>
|
||||
<dd>{(progress * 100).toFixed(0)}%</dd>
|
||||
<dt>Status:</dt>
|
||||
<dd>{error ? error : ConstName(UploadState, uState)}</dd>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getChallengeElement() {
|
||||
let elm = document.createElement("iframe");
|
||||
elm.contentWindow?.document.write(challenge);
|
||||
return <div dangerouslySetInnerHTML={{ __html: elm.outerHTML }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="upload">
|
||||
<div className="info">
|
||||
<dl>
|
||||
<dt>Name:</dt>
|
||||
<dd>{file.name}</dd>
|
||||
<dt>Size:</dt>
|
||||
<dd>{FormatBytes(file.size)}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div className="status">{renderStatus()}</div>
|
||||
{uState === UploadState.Challenge && (
|
||||
<div
|
||||
className="iframe-challenge"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
{getChallengeElement()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
.footer {
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer > a {
|
||||
margin-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid;
|
||||
margin-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
.footer > a:last-child {
|
||||
border: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.footer > a > img {
|
||||
filter: invert(1);
|
||||
vertical-align: middle;
|
||||
height: 20px;
|
||||
}
|
||||
filter: invert(1);
|
||||
vertical-align: middle;
|
||||
height: 20px;
|
||||
}
|
||||
|
@ -1,22 +1,26 @@
|
||||
import "./FooterLinks.css"
|
||||
import {useSelector} from "react-redux";
|
||||
import {Link} from "react-router-dom";
|
||||
import "./FooterLinks.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {RootState} from "Store";
|
||||
import { RootState } from "Store";
|
||||
|
||||
export function FooterLinks() {
|
||||
const profile = useSelector((s:RootState) => s.login.profile);
|
||||
const profile = useSelector((s: RootState) => s.login.profile);
|
||||
|
||||
return (
|
||||
<div className="footer">
|
||||
<a href="https://discord.gg/8BkxTGs" target="_blank" rel="noreferrer">
|
||||
Discord
|
||||
</a>
|
||||
<a href="https://git.v0l.io/Kieran/void.cat/" target="_blank" rel="noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
<Link to="/donate">Donate</Link>
|
||||
{profile?.roles?.includes("Admin") ? <a href="/admin">Admin</a> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="footer">
|
||||
<a href="https://discord.gg/8BkxTGs" target="_blank" rel="noreferrer">
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
href="https://git.v0l.io/Kieran/void.cat/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<Link to="/donate">Donate</Link>
|
||||
{profile?.roles?.includes("Admin") ? <a href="/admin">Admin</a> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,41 +1,41 @@
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
margin: 0 30px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
margin: 0 30px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats svg {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.stats > div {
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.stats {
|
||||
margin: 0 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.stats {
|
||||
margin: 0 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
.stats svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.build-info {
|
||||
width: 100vw;
|
||||
}
|
||||
.build-info {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
.build-info {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
font-size: x-small;
|
||||
padding: 10px;
|
||||
}
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
font-size: x-small;
|
||||
padding: 10px;
|
||||
}
|
||||
|
@ -1,40 +1,42 @@
|
||||
import "./GlobalStats.css";
|
||||
import {Fragment} from "react";
|
||||
import { Fragment } from "react";
|
||||
import moment from "moment";
|
||||
import {useSelector} from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import Icon from "Components/Shared/Icon";
|
||||
import {RootState} from "Store";
|
||||
import {FormatBytes} from "Util";
|
||||
import { RootState } from "Store";
|
||||
import { FormatBytes } from "Util";
|
||||
|
||||
export function GlobalStats() {
|
||||
let stats = useSelector((s: RootState) => s.info.info);
|
||||
let stats = useSelector((s: RootState) => s.info.info);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<dl className="stats">
|
||||
<div>
|
||||
<Icon name="upload-cloud"/>
|
||||
{FormatBytes(stats?.bandwidth?.ingress ?? 0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="download-cloud"/>
|
||||
{FormatBytes(stats?.bandwidth?.egress ?? 0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="save"/>
|
||||
{FormatBytes(stats?.totalBytes ?? 0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="hash"/>
|
||||
{stats?.count ?? 0}
|
||||
</div>
|
||||
</dl>
|
||||
{stats?.buildInfo && <div className="build-info">
|
||||
{stats.buildInfo.version}-{stats.buildInfo.gitHash}
|
||||
<br/>
|
||||
{moment(stats.buildInfo.buildTime).fromNow()}
|
||||
</div>}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<dl className="stats">
|
||||
<div>
|
||||
<Icon name="upload-cloud" />
|
||||
{FormatBytes(stats?.bandwidth?.ingress ?? 0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="download-cloud" />
|
||||
{FormatBytes(stats?.bandwidth?.egress ?? 0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="save" />
|
||||
{FormatBytes(stats?.totalBytes ?? 0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="hash" />
|
||||
{stats?.count ?? 0}
|
||||
</div>
|
||||
</dl>
|
||||
{stats?.buildInfo && (
|
||||
<div className="build-info">
|
||||
{stats.buildInfo.version}-{stats.buildInfo.gitHash}
|
||||
<br />
|
||||
{moment(stats.buildInfo.buildTime).fromNow()}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -1,26 +1,34 @@
|
||||
import {Bar, BarChart, Tooltip, XAxis} from "recharts";
|
||||
import { Bar, BarChart, Tooltip, XAxis } from "recharts";
|
||||
import moment from "moment";
|
||||
import {BandwidthPoint} from "@void-cat/api";
|
||||
import { BandwidthPoint } from "@void-cat/api";
|
||||
|
||||
import {FormatBytes} from "Util";
|
||||
import { FormatBytes } from "Util";
|
||||
|
||||
interface MetricsGraphProps {
|
||||
metrics?: Array<BandwidthPoint>
|
||||
metrics?: Array<BandwidthPoint>;
|
||||
}
|
||||
export function MetricsGraph({metrics}: MetricsGraphProps) {
|
||||
if (!metrics || metrics.length === 0) return null;
|
||||
export function MetricsGraph({ metrics }: MetricsGraphProps) {
|
||||
if (!metrics || metrics.length === 0) return null;
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
width={Math.min(window.innerWidth, 900)}
|
||||
height={200}
|
||||
data={metrics}
|
||||
margin={{left: 0, right: 0}}
|
||||
style={{userSelect: "none"}}>
|
||||
<XAxis dataKey="time" tickFormatter={(v) => `${moment(v).format("DD-MMM")}`}/>
|
||||
<Bar dataKey="egress" fill="#ccc"/>
|
||||
<Tooltip formatter={(v) => FormatBytes(v as number)} labelStyle={{color: "#aaa"}} itemStyle={{color: "#eee"}}
|
||||
contentStyle={{backgroundColor: "#111"}}/>
|
||||
</BarChart>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<BarChart
|
||||
width={Math.min(window.innerWidth, 900)}
|
||||
height={200}
|
||||
data={metrics}
|
||||
margin={{ left: 0, right: 0 }}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickFormatter={(v) => `${moment(v).format("DD-MMM")}`}
|
||||
/>
|
||||
<Bar dataKey="egress" fill="#ccc" />
|
||||
<Tooltip
|
||||
formatter={(v) => FormatBytes(v as number)}
|
||||
labelStyle={{ color: "#aaa" }}
|
||||
itemStyle={{ color: "#eee" }}
|
||||
contentStyle={{ backgroundColor: "#111" }}
|
||||
/>
|
||||
</BarChart>
|
||||
);
|
||||
}
|
||||
|
@ -1,83 +1,86 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import moment from "moment";
|
||||
import {ApiKey} from "@void-cat/api";
|
||||
import { ApiKey } from "@void-cat/api";
|
||||
|
||||
import {VoidButton} from "../Shared/VoidButton";
|
||||
import { VoidButton } from "../Shared/VoidButton";
|
||||
import VoidModal from "../Shared/VoidModal";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
|
||||
export default function ApiKeyList() {
|
||||
const Api = useApi();
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||
const [newApiKey, setNewApiKey] = useState<ApiKey>();
|
||||
const DefaultExpiry = 1000 * 60 * 60 * 24 * 90;
|
||||
const Api = useApi();
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||
const [newApiKey, setNewApiKey] = useState<ApiKey>();
|
||||
const DefaultExpiry = 1000 * 60 * 60 * 24 * 90;
|
||||
|
||||
async function loadApiKeys() {
|
||||
try {
|
||||
const keys = await Api.listApiKeys();
|
||||
setApiKeys(keys);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
async function loadApiKeys() {
|
||||
try {
|
||||
const keys = await Api.listApiKeys();
|
||||
setApiKeys(keys);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createApiKey() {
|
||||
try {
|
||||
const rsp = await Api.createApiKey({
|
||||
expiry: new Date(new Date().getTime() + DefaultExpiry)
|
||||
});
|
||||
setNewApiKey(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
async function createApiKey() {
|
||||
try {
|
||||
const rsp = await Api.createApiKey({
|
||||
expiry: new Date(new Date().getTime() + DefaultExpiry),
|
||||
});
|
||||
setNewApiKey(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function openDocs() {
|
||||
window.open("/swagger", "_blank")
|
||||
}
|
||||
function openDocs() {
|
||||
window.open("/swagger", "_blank");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadApiKeys().catch(console.error);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
loadApiKeys().catch(console.error);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-center">
|
||||
<div className="flx-grow">
|
||||
<h1>API Keys</h1>
|
||||
</div>
|
||||
<div>
|
||||
<VoidButton onClick={() => createApiKey()}>+New</VoidButton>
|
||||
<VoidButton onClick={() => openDocs()}>Docs</VoidButton>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Created</th>
|
||||
<th>Expiry</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{apiKeys.map(e => <tr key={e.id}>
|
||||
<td>{e.id}</td>
|
||||
<td>{moment(e.created).fromNow()}</td>
|
||||
<td>{moment(e.expiry).fromNow()}</td>
|
||||
<td>
|
||||
<VoidButton>Delete</VoidButton>
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
{newApiKey &&
|
||||
<VoidModal title="New Api Key" style={{maxWidth: "50vw"}}>
|
||||
Please save this now as it will not be shown again:
|
||||
<pre className="copy">{newApiKey.token}</pre>
|
||||
<VoidButton onClick={() => setNewApiKey(undefined)}>Close</VoidButton>
|
||||
</VoidModal>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-center">
|
||||
<div className="flx-grow">
|
||||
<h1>API Keys</h1>
|
||||
</div>
|
||||
<div>
|
||||
<VoidButton onClick={() => createApiKey()}>+New</VoidButton>
|
||||
<VoidButton onClick={() => openDocs()}>Docs</VoidButton>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Created</th>
|
||||
<th>Expiry</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{apiKeys.map((e) => (
|
||||
<tr key={e.id}>
|
||||
<td>{e.id}</td>
|
||||
<td>{moment(e.created).fromNow()}</td>
|
||||
<td>{moment(e.expiry).fromNow()}</td>
|
||||
<td>
|
||||
<VoidButton>Delete</VoidButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{newApiKey && (
|
||||
<VoidModal title="New Api Key" style={{ maxWidth: "50vw" }}>
|
||||
Please save this now as it will not be shown again:
|
||||
<pre className="copy">{newApiKey.token}</pre>
|
||||
<VoidButton onClick={() => setNewApiKey(undefined)}>Close</VoidButton>
|
||||
</VoidModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,24 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface CountdownProps {
|
||||
to: number | string | Date
|
||||
onEnded?: () => void
|
||||
to: number | string | Date;
|
||||
onEnded?: () => void;
|
||||
}
|
||||
export function Countdown({onEnded, to}: CountdownProps) {
|
||||
const [time, setTime] = useState(0);
|
||||
export function Countdown({ onEnded, to }: CountdownProps) {
|
||||
const [time, setTime] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => {
|
||||
const toDate = new Date(to).getTime();
|
||||
const now = new Date().getTime();
|
||||
const seconds = (toDate - now) / 1000.0;
|
||||
setTime(Math.max(0, seconds));
|
||||
if (seconds <= 0 && typeof onEnded === "function") {
|
||||
onEnded();
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(t);
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => {
|
||||
const toDate = new Date(to).getTime();
|
||||
const now = new Date().getTime();
|
||||
const seconds = (toDate - now) / 1000.0;
|
||||
setTime(Math.max(0, seconds));
|
||||
if (seconds <= 0 && typeof onEnded === "function") {
|
||||
onEnded();
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
return <div>{time.toFixed(1)}s</div>
|
||||
}
|
||||
return <div>{time.toFixed(1)}s</div>;
|
||||
}
|
||||
|
@ -1,100 +1,124 @@
|
||||
import {useDispatch} from "react-redux";
|
||||
import {ReactNode, useEffect, useState} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import {ApiError, PagedRequest, PagedResponse, PagedSortBy, PageSortOrder, VoidFileResponse} from "@void-cat/api";
|
||||
import {
|
||||
ApiError,
|
||||
PagedRequest,
|
||||
PagedResponse,
|
||||
PagedSortBy,
|
||||
PageSortOrder,
|
||||
VoidFileResponse,
|
||||
} from "@void-cat/api";
|
||||
|
||||
import {logout} from "../../LoginState";
|
||||
import {PageSelector} from "./PageSelector";
|
||||
import { logout } from "../../LoginState";
|
||||
import { PageSelector } from "./PageSelector";
|
||||
|
||||
import {FormatBytes} from "Util";
|
||||
import { FormatBytes } from "Util";
|
||||
|
||||
interface FileListProps {
|
||||
actions?: (f: VoidFileResponse) => ReactNode
|
||||
loadPage: (req: PagedRequest) => Promise<PagedResponse<any>>
|
||||
actions?: (f: VoidFileResponse) => ReactNode;
|
||||
loadPage: (req: PagedRequest) => Promise<PagedResponse<any>>;
|
||||
}
|
||||
|
||||
export function FileList(props: FileListProps) {
|
||||
const loadPage = props.loadPage;
|
||||
const actions = props.actions;
|
||||
const dispatch = useDispatch();
|
||||
const [files, setFiles] = useState<PagedResponse<VoidFileResponse>>();
|
||||
const [page, setPage] = useState(0);
|
||||
const pageSize = 20;
|
||||
const [accessDenied, setAccessDenied] = useState<boolean>();
|
||||
const loadPage = props.loadPage;
|
||||
const actions = props.actions;
|
||||
const dispatch = useDispatch();
|
||||
const [files, setFiles] = useState<PagedResponse<VoidFileResponse>>();
|
||||
const [page, setPage] = useState(0);
|
||||
const pageSize = 20;
|
||||
const [accessDenied, setAccessDenied] = useState<boolean>();
|
||||
|
||||
async function loadFileList() {
|
||||
try {
|
||||
const pageReq = {
|
||||
page: page,
|
||||
pageSize,
|
||||
sortBy: PagedSortBy.Date,
|
||||
sortOrder: PageSortOrder.Dsc
|
||||
};
|
||||
const rsp = await loadPage(pageReq);
|
||||
setFiles(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof ApiError) {
|
||||
if (e.statusCode === 401) {
|
||||
dispatch(logout());
|
||||
} else if (e.statusCode === 403) {
|
||||
setAccessDenied(true);
|
||||
}
|
||||
}
|
||||
async function loadFileList() {
|
||||
try {
|
||||
const pageReq = {
|
||||
page: page,
|
||||
pageSize,
|
||||
sortBy: PagedSortBy.Date,
|
||||
sortOrder: PageSortOrder.Dsc,
|
||||
};
|
||||
const rsp = await loadPage(pageReq);
|
||||
setFiles(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof ApiError) {
|
||||
if (e.statusCode === 401) {
|
||||
dispatch(logout());
|
||||
} else if (e.statusCode === 403) {
|
||||
setAccessDenied(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderItem(i: VoidFileResponse) {
|
||||
const meta = i.metadata;
|
||||
const bw = i.bandwidth;
|
||||
|
||||
return (
|
||||
<tr key={i.id}>
|
||||
<td><Link to={`/${i.id}`}>{i.id.substring(0, 4)}..</Link></td>
|
||||
<td>{meta?.name ? (meta?.name.length > 20 ? `${meta?.name.substring(0, 20)}..` : meta?.name) : null}</td>
|
||||
<td>{meta?.uploaded ? moment(meta?.uploaded).fromNow() : null}</td>
|
||||
<td>{meta?.size ? FormatBytes(meta?.size, 2) : null}</td>
|
||||
<td>{bw ? FormatBytes(bw.egress, 2) : null}</td>
|
||||
{actions ? actions(i) : null}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadFileList().catch(console.error)
|
||||
}, [page]);
|
||||
|
||||
if (accessDenied) {
|
||||
return <h3>Access Denied</h3>;
|
||||
}
|
||||
function renderItem(i: VoidFileResponse) {
|
||||
const meta = i.metadata;
|
||||
const bw = i.bandwidth;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Uploaded</th>
|
||||
<th>Size</th>
|
||||
<th>Egress</th>
|
||||
{actions ? <th>Actions</th> : null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files ? files.results.map(a => renderItem(a)) : <tr>
|
||||
<td colSpan={99}>No files</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={999}>
|
||||
{files &&
|
||||
<PageSelector onSelectPage={(x) => setPage(x)} page={page} total={files.totalResults}
|
||||
pageSize={pageSize}/>}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<tr key={i.id}>
|
||||
<td>
|
||||
<Link to={`/${i.id}`}>{i.id.substring(0, 4)}..</Link>
|
||||
</td>
|
||||
<td>
|
||||
{meta?.name
|
||||
? meta?.name.length > 20
|
||||
? `${meta?.name.substring(0, 20)}..`
|
||||
: meta?.name
|
||||
: null}
|
||||
</td>
|
||||
<td>{meta?.uploaded ? moment(meta?.uploaded).fromNow() : null}</td>
|
||||
<td>{meta?.size ? FormatBytes(meta?.size, 2) : null}</td>
|
||||
<td>{bw ? FormatBytes(bw.egress, 2) : null}</td>
|
||||
{actions ? actions(i) : null}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadFileList().catch(console.error);
|
||||
}, [page]);
|
||||
|
||||
if (accessDenied) {
|
||||
return <h3>Access Denied</h3>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Uploaded</th>
|
||||
<th>Size</th>
|
||||
<th>Egress</th>
|
||||
{actions ? <th>Actions</th> : null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files ? (
|
||||
files.results.map((a) => renderItem(a))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={99}>No files</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={999}>
|
||||
{files && (
|
||||
<PageSelector
|
||||
onSelectPage={(x) => setPage(x)}
|
||||
page={page}
|
||||
total={files.totalResults}
|
||||
pageSize={pageSize}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
@ -1,26 +1,27 @@
|
||||
import {useState} from "react";
|
||||
import {RateCalculator} from "./RateCalculator";
|
||||
import { useState } from "react";
|
||||
import { RateCalculator } from "./RateCalculator";
|
||||
|
||||
export function useFileTransfer() {
|
||||
const [speed, setSpeed] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const calc = new RateCalculator();
|
||||
const [speed, setSpeed] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const calc = new RateCalculator();
|
||||
|
||||
return {
|
||||
speed, progress,
|
||||
setFileSize: (size: number) => {
|
||||
calc.SetFileSize(size);
|
||||
},
|
||||
update: (bytes: number) => {
|
||||
calc.ReportProgress(bytes);
|
||||
setSpeed(calc.GetSpeed());
|
||||
setProgress(calc.GetProgress());
|
||||
},
|
||||
loaded: (loaded: number) => {
|
||||
calc.ReportLoaded(loaded);
|
||||
setSpeed(calc.GetSpeed());
|
||||
setProgress(calc.GetProgress());
|
||||
},
|
||||
reset: () => calc.Reset()
|
||||
}
|
||||
}
|
||||
return {
|
||||
speed,
|
||||
progress,
|
||||
setFileSize: (size: number) => {
|
||||
calc.SetFileSize(size);
|
||||
},
|
||||
update: (bytes: number) => {
|
||||
calc.ReportProgress(bytes);
|
||||
setSpeed(calc.GetSpeed());
|
||||
setProgress(calc.GetProgress());
|
||||
},
|
||||
loaded: (loaded: number) => {
|
||||
calc.ReportLoaded(loaded);
|
||||
setSpeed(calc.GetSpeed());
|
||||
setProgress(calc.GetProgress());
|
||||
},
|
||||
reset: () => calc.Reset(),
|
||||
};
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
.header {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
padding: 5px 0;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
padding: 5px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
font-size: 30px;
|
||||
line-height: 2;
|
||||
flex-grow: 1;
|
||||
font-size: 30px;
|
||||
line-height: 2;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header img.logo {
|
||||
height: 80px;
|
||||
}
|
||||
height: 80px;
|
||||
}
|
||||
|
@ -1,65 +1,72 @@
|
||||
import "./Header.css";
|
||||
import VoidCat from "../../image/voidcat.png";
|
||||
|
||||
import {useEffect} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import {InlineProfile} from "./InlineProfile";
|
||||
import {logout, setAuth, setProfile} from "../../LoginState";
|
||||
import {setInfo} from "../../SiteInfoStore";
|
||||
import { InlineProfile } from "./InlineProfile";
|
||||
import { logout, setAuth, setProfile } from "../../LoginState";
|
||||
import { setInfo } from "../../SiteInfoStore";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
import {RootState} from "Store";
|
||||
import { RootState } from "Store";
|
||||
|
||||
export function Header() {
|
||||
const dispatch = useDispatch();
|
||||
const jwt = useSelector((s: RootState) => s.login.jwt);
|
||||
const profile = useSelector((s: RootState) => s.login.profile)
|
||||
const Api = useApi();
|
||||
const dispatch = useDispatch();
|
||||
const jwt = useSelector((s: RootState) => s.login.jwt);
|
||||
const profile = useSelector((s: RootState) => s.login.profile);
|
||||
const Api = useApi();
|
||||
|
||||
async function initProfile() {
|
||||
if (jwt && !profile) {
|
||||
try {
|
||||
const me = await Api.getUser("me");
|
||||
dispatch(setProfile(me));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(logout());
|
||||
}
|
||||
} else if (window.location.pathname === "/login" && window.location.hash.length > 1) {
|
||||
dispatch(setAuth({
|
||||
jwt: window.location.hash.substring(1)
|
||||
}));
|
||||
}
|
||||
async function initProfile() {
|
||||
if (jwt && !profile) {
|
||||
try {
|
||||
const me = await Api.getUser("me");
|
||||
dispatch(setProfile(me));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dispatch(logout());
|
||||
}
|
||||
} else if (
|
||||
window.location.pathname === "/login" &&
|
||||
window.location.hash.length > 1
|
||||
) {
|
||||
dispatch(
|
||||
setAuth({
|
||||
jwt: window.location.hash.substring(1),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
const info = await Api.info();
|
||||
dispatch(setInfo(info));
|
||||
}
|
||||
async function loadStats() {
|
||||
const info = await Api.info();
|
||||
dispatch(setInfo(info));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initProfile().catch(console.error);
|
||||
loadStats().catch(console.error);
|
||||
}, [jwt]);
|
||||
useEffect(() => {
|
||||
initProfile().catch(console.error);
|
||||
loadStats().catch(console.error);
|
||||
}, [jwt]);
|
||||
|
||||
return (
|
||||
<div className="header page">
|
||||
|
||||
<img src={VoidCat} alt="logo" className="logo"/>
|
||||
<div className="title">
|
||||
<Link to="/">
|
||||
{window.location.hostname}
|
||||
</Link>
|
||||
</div>
|
||||
{profile ?
|
||||
<InlineProfile profile={profile} options={{
|
||||
showName: false
|
||||
}}/> :
|
||||
<Link to="/login">
|
||||
<div className="btn">Login</div>
|
||||
</Link>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="header page">
|
||||
<img src={VoidCat} alt="logo" className="logo" />
|
||||
<div className="title">
|
||||
<Link to="/">{window.location.hostname}</Link>
|
||||
</div>
|
||||
{profile ? (
|
||||
<InlineProfile
|
||||
profile={profile}
|
||||
options={{
|
||||
showName: false,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Link to="/login">
|
||||
<div className="btn">Login</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,21 +1,26 @@
|
||||
import { MouseEventHandler } from "react";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
onClick?: MouseEventHandler<SVGSVGElement>;
|
||||
name: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
onClick?: MouseEventHandler<SVGSVGElement>;
|
||||
};
|
||||
|
||||
const Icon = (props: Props) => {
|
||||
const size = props.size || 20;
|
||||
const href = "/icons.svg#" + props.name;
|
||||
const size = props.size || 20;
|
||||
const href = "/icons.svg#" + props.name;
|
||||
|
||||
return (
|
||||
<svg width={size} height={size} className={props.className} onClick={props.onClick}>
|
||||
<use href={href} />
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={props.className}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<use href={href} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
|
@ -1,20 +1,22 @@
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.image-grid > a {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.image-grid img, .image-grid video, .image-grid audio {
|
||||
max-width: stretch;
|
||||
max-height: stretch;
|
||||
}
|
||||
.image-grid img,
|
||||
.image-grid video,
|
||||
.image-grid audio {
|
||||
max-width: stretch;
|
||||
max-height: stretch;
|
||||
}
|
||||
|
@ -1,105 +1,120 @@
|
||||
import "./ImageGrid.css";
|
||||
|
||||
import {ApiError, PagedRequest, PagedResponse, PagedSortBy, PageSortOrder, VoidFileResponse} from "@void-cat/api";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {
|
||||
ApiError,
|
||||
PagedRequest,
|
||||
PagedResponse,
|
||||
PagedSortBy,
|
||||
PageSortOrder,
|
||||
VoidFileResponse,
|
||||
} from "@void-cat/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import {logout} from "../../LoginState";
|
||||
import {PageSelector} from "./PageSelector";
|
||||
import { logout } from "../../LoginState";
|
||||
import { PageSelector } from "./PageSelector";
|
||||
|
||||
interface ImageGridProps {
|
||||
loadPage: (req: PagedRequest) => Promise<PagedResponse<any>>
|
||||
loadPage: (req: PagedRequest) => Promise<PagedResponse<any>>;
|
||||
}
|
||||
|
||||
export default function ImageGrid(props: ImageGridProps) {
|
||||
const loadPage = props.loadPage;
|
||||
const dispatch = useDispatch();
|
||||
const [files, setFiles] = useState<PagedResponse<VoidFileResponse>>();
|
||||
const [page, setPage] = useState(0);
|
||||
const pageSize = 100;
|
||||
const [accessDenied, setAccessDenied] = useState<boolean>();
|
||||
const loadPage = props.loadPage;
|
||||
const dispatch = useDispatch();
|
||||
const [files, setFiles] = useState<PagedResponse<VoidFileResponse>>();
|
||||
const [page, setPage] = useState(0);
|
||||
const pageSize = 100;
|
||||
const [accessDenied, setAccessDenied] = useState<boolean>();
|
||||
|
||||
async function loadFileList() {
|
||||
try {
|
||||
const pageReq = {
|
||||
page: page,
|
||||
pageSize,
|
||||
sortBy: PagedSortBy.Date,
|
||||
sortOrder: PageSortOrder.Dsc
|
||||
};
|
||||
const rsp = await loadPage(pageReq);
|
||||
setFiles(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof ApiError) {
|
||||
if (e.statusCode === 401) {
|
||||
dispatch(logout());
|
||||
} else if (e.statusCode === 403) {
|
||||
setAccessDenied(true);
|
||||
}
|
||||
}
|
||||
async function loadFileList() {
|
||||
try {
|
||||
const pageReq = {
|
||||
page: page,
|
||||
pageSize,
|
||||
sortBy: PagedSortBy.Date,
|
||||
sortOrder: PageSortOrder.Dsc,
|
||||
};
|
||||
const rsp = await loadPage(pageReq);
|
||||
setFiles(rsp);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof ApiError) {
|
||||
if (e.statusCode === 401) {
|
||||
dispatch(logout());
|
||||
} else if (e.statusCode === 403) {
|
||||
setAccessDenied(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadFileList().catch(console.error)
|
||||
}, [page]);
|
||||
useEffect(() => {
|
||||
loadFileList().catch(console.error);
|
||||
}, [page]);
|
||||
|
||||
function renderPreview(info: VoidFileResponse) {
|
||||
const link = `/d/${info.id}`;
|
||||
function renderPreview(info: VoidFileResponse) {
|
||||
const link = `/d/${info.id}`;
|
||||
|
||||
if (info.metadata) {
|
||||
switch (info.metadata.mimeType) {
|
||||
case "image/avif":
|
||||
case "image/bmp":
|
||||
case "image/gif":
|
||||
case "image/svg+xml":
|
||||
case "image/tiff":
|
||||
case "image/webp":
|
||||
case "image/jpg":
|
||||
case "image/jpeg":
|
||||
case "image/png": {
|
||||
return <img src={link} alt={info.metadata.name}/>;
|
||||
}
|
||||
case "audio/aac":
|
||||
case "audio/opus":
|
||||
case "audio/wav":
|
||||
case "audio/webm":
|
||||
case "audio/midi":
|
||||
case "audio/mpeg":
|
||||
case "audio/ogg": {
|
||||
return <audio src={link}/>;
|
||||
}
|
||||
case "video/x-msvideo":
|
||||
case "video/mpeg":
|
||||
case "video/ogg":
|
||||
case "video/mp2t":
|
||||
case "video/mp4":
|
||||
case "video/matroksa":
|
||||
case "video/x-matroska":
|
||||
case "video/webm":
|
||||
case "video/quicktime": {
|
||||
return <video src={link}/>;
|
||||
}
|
||||
default: {
|
||||
return <b>{info.metadata?.name ?? info.id}</b>
|
||||
}
|
||||
}
|
||||
if (info.metadata) {
|
||||
switch (info.metadata.mimeType) {
|
||||
case "image/avif":
|
||||
case "image/bmp":
|
||||
case "image/gif":
|
||||
case "image/svg+xml":
|
||||
case "image/tiff":
|
||||
case "image/webp":
|
||||
case "image/jpg":
|
||||
case "image/jpeg":
|
||||
case "image/png": {
|
||||
return <img src={link} alt={info.metadata.name} />;
|
||||
}
|
||||
case "audio/aac":
|
||||
case "audio/opus":
|
||||
case "audio/wav":
|
||||
case "audio/webm":
|
||||
case "audio/midi":
|
||||
case "audio/mpeg":
|
||||
case "audio/ogg": {
|
||||
return <audio src={link} />;
|
||||
}
|
||||
case "video/x-msvideo":
|
||||
case "video/mpeg":
|
||||
case "video/ogg":
|
||||
case "video/mp2t":
|
||||
case "video/mp4":
|
||||
case "video/matroksa":
|
||||
case "video/x-matroska":
|
||||
case "video/webm":
|
||||
case "video/quicktime": {
|
||||
return <video src={link} />;
|
||||
}
|
||||
default: {
|
||||
return <b>{info.metadata?.name ?? info.id}</b>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (accessDenied) {
|
||||
return <h3>Access Denied</h3>
|
||||
}
|
||||
if (accessDenied) {
|
||||
return <h3>Access Denied</h3>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="image-grid">
|
||||
{files?.results.map(v => <Link key={v.id} to={`/${v.id}`}>
|
||||
{renderPreview(v)}
|
||||
</Link>)}
|
||||
</div>
|
||||
<PageSelector onSelectPage={(x) => setPage(x)} page={page} total={files?.totalResults ?? 0}
|
||||
pageSize={pageSize}/>
|
||||
return (
|
||||
<>
|
||||
<div className="image-grid">
|
||||
{files?.results.map((v) => (
|
||||
<Link key={v.id} to={`/${v.id}`}>
|
||||
{renderPreview(v)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<PageSelector
|
||||
onSelectPage={(x) => setPage(x)}
|
||||
page={page}
|
||||
total={files?.totalResults ?? 0}
|
||||
pageSize={pageSize}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1,19 +1,18 @@
|
||||
|
||||
.small-profile {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.small-profile .avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
border: 2px solid;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.small-profile .name {
|
||||
padding-left: 15px;
|
||||
}
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
@ -1,49 +1,49 @@
|
||||
import "./InlineProfile.css";
|
||||
import {CSSProperties} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Profile} from "@void-cat/api";
|
||||
import { CSSProperties } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Profile } from "@void-cat/api";
|
||||
|
||||
import {DefaultAvatar} from "Const";
|
||||
import { DefaultAvatar } from "Const";
|
||||
|
||||
const DefaultSize = 64;
|
||||
|
||||
interface InlineProfileProps {
|
||||
profile: Profile
|
||||
options?: {
|
||||
size?: number
|
||||
showName?: boolean
|
||||
link?: boolean
|
||||
}
|
||||
profile: Profile;
|
||||
options?: {
|
||||
size?: number;
|
||||
showName?: boolean;
|
||||
link?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function InlineProfile({profile, options}: InlineProfileProps) {
|
||||
options = {
|
||||
size: DefaultSize,
|
||||
showName: true,
|
||||
link: true,
|
||||
...options
|
||||
};
|
||||
export function InlineProfile({ profile, options }: InlineProfileProps) {
|
||||
options = {
|
||||
size: DefaultSize,
|
||||
showName: true,
|
||||
link: true,
|
||||
...options,
|
||||
};
|
||||
|
||||
let avatarUrl = profile.avatar ?? DefaultAvatar;
|
||||
if (!avatarUrl.startsWith("http")) {
|
||||
avatarUrl = `/d/${avatarUrl}`;
|
||||
}
|
||||
let avatarStyles = {
|
||||
backgroundImage: `url(${avatarUrl})`
|
||||
} as CSSProperties;
|
||||
if (options.size !== DefaultSize) {
|
||||
avatarStyles.width = `${options.size}px`;
|
||||
avatarStyles.height = `${options.size}px`;
|
||||
}
|
||||
let avatarUrl = profile.avatar ?? DefaultAvatar;
|
||||
if (!avatarUrl.startsWith("http")) {
|
||||
avatarUrl = `/d/${avatarUrl}`;
|
||||
}
|
||||
let avatarStyles = {
|
||||
backgroundImage: `url(${avatarUrl})`,
|
||||
} as CSSProperties;
|
||||
if (options.size !== DefaultSize) {
|
||||
avatarStyles.width = `${options.size}px`;
|
||||
avatarStyles.height = `${options.size}px`;
|
||||
}
|
||||
|
||||
const elms = (
|
||||
<div className="small-profile">
|
||||
<div className="avatar" style={avatarStyles}/>
|
||||
{options.showName ? <div className="name">{profile.name}</div> : null}
|
||||
</div>
|
||||
);
|
||||
if (options.link === true) {
|
||||
return <Link to={`/u/${profile.id}`}>{elms}</Link>
|
||||
}
|
||||
return elms;
|
||||
}
|
||||
const elms = (
|
||||
<div className="small-profile">
|
||||
<div className="avatar" style={avatarStyles} />
|
||||
{options.showName ? <div className="name">{profile.name}</div> : null}
|
||||
</div>
|
||||
);
|
||||
if (options.link === true) {
|
||||
return <Link to={`/u/${profile.id}`}>{elms}</Link>;
|
||||
}
|
||||
return elms;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
.login .error-msg {
|
||||
color: red;
|
||||
padding: 10px;
|
||||
border: 1px solid red;
|
||||
border-radius: 10px;
|
||||
margin-top: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
color: red;
|
||||
padding: 10px;
|
||||
border: 1px solid red;
|
||||
border-radius: 10px;
|
||||
margin-top: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
@ -1,64 +1,92 @@
|
||||
import {useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import { useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
import "./Login.css";
|
||||
import {setAuth} from "../../LoginState";
|
||||
import {VoidButton} from "./VoidButton";
|
||||
import { setAuth } from "../../LoginState";
|
||||
import { VoidButton } from "./VoidButton";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
import {RootState} from "Store";
|
||||
import { RootState } from "Store";
|
||||
|
||||
export function Login() {
|
||||
const Api = useApi();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [captchaResponse, setCaptchaResponse] = useState("");
|
||||
const captchaKey = useSelector((s: RootState) => s.info.info?.captchaSiteKey);
|
||||
const oAuthProviders = useSelector((s: RootState) => s.info.info?.oAuthProviders);
|
||||
const dispatch = useDispatch();
|
||||
const Api = useApi();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [captchaResponse, setCaptchaResponse] = useState("");
|
||||
const captchaKey = useSelector((s: RootState) => s.info.info?.captchaSiteKey);
|
||||
const oAuthProviders = useSelector(
|
||||
(s: RootState) => s.info.info?.oAuthProviders,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function login(fnLogin: typeof Api.login) {
|
||||
setError("");
|
||||
async function login(fnLogin: typeof Api.login) {
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const rsp = await fnLogin(username, password, captchaResponse);
|
||||
if (rsp.jwt) {
|
||||
dispatch(setAuth({
|
||||
jwt: rsp.jwt,
|
||||
profile: rsp.profile!
|
||||
}));
|
||||
} else {
|
||||
setError(rsp.error!);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const rsp = await fnLogin(username, password, captchaResponse);
|
||||
if (rsp.jwt) {
|
||||
dispatch(
|
||||
setAuth({
|
||||
jwt: rsp.jwt,
|
||||
profile: rsp.profile!,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setError(rsp.error!);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login">
|
||||
<h2>Login</h2>
|
||||
<dl>
|
||||
<dt>Username:</dt>
|
||||
<dd><input type="text" placeholder="user@example.com" onChange={(e) => setUsername(e.target.value)}
|
||||
value={username}/>
|
||||
</dd>
|
||||
<dt>Password:</dt>
|
||||
<dd><input type="password" onChange={(e) => setPassword(e.target.value)} value={password}/></dd>
|
||||
</dl>
|
||||
{captchaKey ? <HCaptcha sitekey={captchaKey} onVerify={v => setCaptchaResponse(v)}/> : null}
|
||||
<VoidButton onClick={() => login(Api.login.bind(Api))}>Login</VoidButton>
|
||||
<VoidButton onClick={() => login(Api.register.bind(Api))}>Register</VoidButton>
|
||||
<br/>
|
||||
{oAuthProviders ?
|
||||
oAuthProviders.map(a => <VoidButton key={a} onClick={() => window.location.href = `/auth/${a}`}>
|
||||
Login with {a}
|
||||
</VoidButton>) : null}
|
||||
{error && <div className="error-msg">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="login">
|
||||
<h2>Login</h2>
|
||||
<dl>
|
||||
<dt>Username:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="user@example.com"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
value={username}
|
||||
/>
|
||||
</dd>
|
||||
<dt>Password:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
value={password}
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
{captchaKey ? (
|
||||
<HCaptcha
|
||||
sitekey={captchaKey}
|
||||
onVerify={(v) => setCaptchaResponse(v)}
|
||||
/>
|
||||
) : null}
|
||||
<VoidButton onClick={() => login(Api.login.bind(Api))}>Login</VoidButton>
|
||||
<VoidButton onClick={() => login(Api.register.bind(Api))}>
|
||||
Register
|
||||
</VoidButton>
|
||||
<br />
|
||||
{oAuthProviders
|
||||
? oAuthProviders.map((a) => (
|
||||
<VoidButton
|
||||
key={a}
|
||||
onClick={() => (window.location.href = `/auth/${a}`)}
|
||||
>
|
||||
Login with {a}
|
||||
</VoidButton>
|
||||
))
|
||||
: null}
|
||||
{error && <div className="error-msg">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,35 +1,34 @@
|
||||
.page-buttons {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
width: min-content;
|
||||
margin-top: 10px;
|
||||
white-space: nowrap;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
width: min-content;
|
||||
margin-top: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.page-buttons > div {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-buttons > div.active {
|
||||
background-color: #333;
|
||||
font-weight: bold;
|
||||
background-color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.page-buttons > div:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.page-buttons > div:last-child {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.page-buttons > small {
|
||||
line-height: 32px;
|
||||
margin-left: 10px;
|
||||
line-height: 32px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
@ -1,43 +1,52 @@
|
||||
import "./PageSelector.css";
|
||||
|
||||
interface PageSelectorProps {
|
||||
total: number
|
||||
pageSize: number
|
||||
page: number
|
||||
onSelectPage?: (v: number) => void
|
||||
options?: {
|
||||
showPages: number
|
||||
}
|
||||
total: number;
|
||||
pageSize: number;
|
||||
page: number;
|
||||
onSelectPage?: (v: number) => void;
|
||||
options?: {
|
||||
showPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function PageSelector(props: PageSelectorProps) {
|
||||
const total = props.total;
|
||||
const pageSize = props.pageSize;
|
||||
const page = props.page;
|
||||
const onSelectPage = props.onSelectPage;
|
||||
const options = {
|
||||
showPages: 3,
|
||||
...props.options
|
||||
};
|
||||
const total = props.total;
|
||||
const pageSize = props.pageSize;
|
||||
const page = props.page;
|
||||
const onSelectPage = props.onSelectPage;
|
||||
const options = {
|
||||
showPages: 3,
|
||||
...props.options,
|
||||
};
|
||||
|
||||
const totalPages = Math.floor(total / pageSize);
|
||||
const first = Math.max(0, page - options.showPages);
|
||||
const firstDiff = page - first;
|
||||
const last = Math.min(totalPages, page + options.showPages + options.showPages - firstDiff);
|
||||
const totalPages = Math.floor(total / pageSize);
|
||||
const first = Math.max(0, page - options.showPages);
|
||||
const firstDiff = page - first;
|
||||
const last = Math.min(
|
||||
totalPages,
|
||||
page + options.showPages + options.showPages - firstDiff,
|
||||
);
|
||||
|
||||
const buttons = [];
|
||||
for (let x = first; x <= last; x++) {
|
||||
buttons.push(<div onClick={() => onSelectPage?.(x)} key={x} className={page === x ? "active" : ""}>
|
||||
{x + 1}
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-buttons">
|
||||
<div onClick={() => onSelectPage?.(0)}><<</div>
|
||||
{buttons}
|
||||
<div onClick={() => onSelectPage?.(totalPages)}>>></div>
|
||||
<small>Total: {total}</small>
|
||||
</div>
|
||||
const buttons = [];
|
||||
for (let x = first; x <= last; x++) {
|
||||
buttons.push(
|
||||
<div
|
||||
onClick={() => onSelectPage?.(x)}
|
||||
key={x}
|
||||
className={page === x ? "active" : ""}
|
||||
>
|
||||
{x + 1}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-buttons">
|
||||
<div onClick={() => onSelectPage?.(0)}><<</div>
|
||||
{buttons}
|
||||
<div onClick={() => onSelectPage?.(totalPages)}>>></div>
|
||||
<small>Total: {total}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,68 +1,68 @@
|
||||
interface RateReport {
|
||||
time: number
|
||||
amount: number
|
||||
time: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export class RateCalculator {
|
||||
#reports: Array<RateReport> = [];
|
||||
#lastLoaded = 0;
|
||||
#progress = 0;
|
||||
#speed = 0;
|
||||
#fileSize = 0;
|
||||
#reports: Array<RateReport> = [];
|
||||
#lastLoaded = 0;
|
||||
#progress = 0;
|
||||
#speed = 0;
|
||||
#fileSize = 0;
|
||||
|
||||
constructor() {
|
||||
this.Reset();
|
||||
constructor() {
|
||||
this.Reset();
|
||||
}
|
||||
|
||||
SetFileSize(size: number) {
|
||||
this.#fileSize = size;
|
||||
}
|
||||
|
||||
GetProgress() {
|
||||
return this.#progress;
|
||||
}
|
||||
|
||||
GetSpeed() {
|
||||
return this.#speed;
|
||||
}
|
||||
|
||||
Reset() {
|
||||
this.#reports = [];
|
||||
this.#lastLoaded = 0;
|
||||
this.#progress = 0;
|
||||
this.#speed = 0;
|
||||
}
|
||||
|
||||
ReportProgress(amount: number) {
|
||||
this.#reports.push({
|
||||
time: new Date().getTime(),
|
||||
amount,
|
||||
});
|
||||
this.#lastLoaded += amount;
|
||||
this.#progress = this.#lastLoaded / this.#fileSize;
|
||||
this.#speed = this.RateWindow(5);
|
||||
}
|
||||
|
||||
ReportLoaded(loaded: number) {
|
||||
this.#reports.push({
|
||||
time: new Date().getTime(),
|
||||
amount: loaded - this.#lastLoaded,
|
||||
});
|
||||
this.#lastLoaded = loaded;
|
||||
this.#progress = this.#lastLoaded / this.#fileSize;
|
||||
this.#speed = this.RateWindow(5);
|
||||
}
|
||||
|
||||
RateWindow(s: number) {
|
||||
let total = 0.0;
|
||||
|
||||
const windowStart = new Date().getTime() - s * 1000;
|
||||
for (let r of this.#reports) {
|
||||
if (r.time >= windowStart) {
|
||||
total += r.amount;
|
||||
}
|
||||
}
|
||||
|
||||
SetFileSize(size: number) {
|
||||
this.#fileSize = size;
|
||||
}
|
||||
|
||||
GetProgress() {
|
||||
return this.#progress;
|
||||
}
|
||||
|
||||
GetSpeed() {
|
||||
return this.#speed;
|
||||
}
|
||||
|
||||
Reset() {
|
||||
this.#reports = [];
|
||||
this.#lastLoaded = 0;
|
||||
this.#progress = 0;
|
||||
this.#speed = 0;
|
||||
}
|
||||
|
||||
ReportProgress(amount: number) {
|
||||
this.#reports.push({
|
||||
time: new Date().getTime(),
|
||||
amount
|
||||
});
|
||||
this.#lastLoaded += amount;
|
||||
this.#progress = this.#lastLoaded / this.#fileSize;
|
||||
this.#speed = this.RateWindow(5);
|
||||
}
|
||||
|
||||
ReportLoaded(loaded: number) {
|
||||
this.#reports.push({
|
||||
time: new Date().getTime(),
|
||||
amount: loaded - this.#lastLoaded
|
||||
});
|
||||
this.#lastLoaded = loaded;
|
||||
this.#progress = this.#lastLoaded / this.#fileSize;
|
||||
this.#speed = this.RateWindow(5);
|
||||
}
|
||||
|
||||
RateWindow(s: number) {
|
||||
let total = 0.0;
|
||||
|
||||
const windowStart = new Date().getTime() - (s * 1000);
|
||||
for (let r of this.#reports) {
|
||||
if (r.time >= windowStart) {
|
||||
total += r.amount;
|
||||
}
|
||||
}
|
||||
|
||||
return total / s;
|
||||
}
|
||||
}
|
||||
return total / s;
|
||||
}
|
||||
}
|
||||
|
@ -1,56 +1,63 @@
|
||||
import React, {MouseEvent, ReactNode, useEffect, useState} from "react";
|
||||
import React, { MouseEvent, ReactNode, useEffect, useState } from "react";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface VoidButtonProps {
|
||||
onClick?: (e: MouseEvent<HTMLDivElement>) => Promise<unknown> | unknown
|
||||
options?: {
|
||||
showSuccess: boolean
|
||||
}
|
||||
children: ReactNode
|
||||
onClick?: (e: MouseEvent<HTMLDivElement>) => Promise<unknown> | unknown;
|
||||
options?: {
|
||||
showSuccess: boolean;
|
||||
};
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function VoidButton(props: VoidButtonProps) {
|
||||
const options = {
|
||||
showSuccess: false,
|
||||
...props.options
|
||||
};
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const options = {
|
||||
showSuccess: false,
|
||||
...props.options,
|
||||
};
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
async function handleClick(e: MouseEvent<HTMLDivElement>) {
|
||||
if (disabled) return;
|
||||
setDisabled(true);
|
||||
async function handleClick(e: MouseEvent<HTMLDivElement>) {
|
||||
if (disabled) return;
|
||||
setDisabled(true);
|
||||
|
||||
let fn = props.onClick;
|
||||
try {
|
||||
if (typeof fn === "function") {
|
||||
const ret = fn(e);
|
||||
if (ret && typeof ret === "object" && "then" in ret) {
|
||||
await (ret as Promise<unknown>);
|
||||
}
|
||||
setSuccess(options.showSuccess);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let fn = props.onClick;
|
||||
try {
|
||||
if (typeof fn === "function") {
|
||||
const ret = fn(e);
|
||||
if (ret && typeof ret === "object" && "then" in ret) {
|
||||
await (ret as Promise<unknown>);
|
||||
}
|
||||
|
||||
setDisabled(false);
|
||||
setSuccess(options.showSuccess);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (success) {
|
||||
setTimeout(() => setSuccess(false), 1000);
|
||||
}
|
||||
}, [success]);
|
||||
setDisabled(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-inline flex-center">
|
||||
<div>
|
||||
<div className={`btn${disabled ? " disabled" : ""}`} onClick={handleClick}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
{success && <div><Icon name="check-circle"/></div>}
|
||||
useEffect(() => {
|
||||
if (success) {
|
||||
setTimeout(() => setSuccess(false), 1000);
|
||||
}
|
||||
}, [success]);
|
||||
|
||||
return (
|
||||
<div className="flex-inline flex-center">
|
||||
<div>
|
||||
<div
|
||||
className={`btn${disabled ? " disabled" : ""}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
{success && (
|
||||
<div>
|
||||
<Icon name="check-circle" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,35 +1,35 @@
|
||||
.modal-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-bg .modal {
|
||||
min-height: 100px;
|
||||
min-width: 300px;
|
||||
background-color: #bbb;
|
||||
color: #000;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
min-height: 100px;
|
||||
min-width: 300px;
|
||||
background-color: #bbb;
|
||||
color: #000;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-bg .modal .modal-header {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid;
|
||||
margin: 0;
|
||||
line-height: 2em;
|
||||
background-color: #222;
|
||||
color: #bbb;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid;
|
||||
margin: 0;
|
||||
line-height: 2em;
|
||||
background-color: #222;
|
||||
color: #bbb;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.modal-bg .modal .modal-body {
|
||||
padding: 10px;
|
||||
}
|
||||
padding: 10px;
|
||||
}
|
||||
|
@ -1,25 +1,21 @@
|
||||
import "./VoidModal.css";
|
||||
import {CSSProperties, ReactNode} from "react";
|
||||
import { CSSProperties, ReactNode } from "react";
|
||||
|
||||
interface VoidModalProps {
|
||||
title?: string
|
||||
style?: CSSProperties
|
||||
children: ReactNode
|
||||
title?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
export default function VoidModal(props: VoidModalProps) {
|
||||
const title = props.title;
|
||||
const style = props.style;
|
||||
|
||||
return (
|
||||
<div className="modal-bg">
|
||||
<div className="modal" style={style}>
|
||||
<div className="modal-header">
|
||||
{title ?? "Unknown modal"}
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{props.children ?? "Missing body"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const title = props.title;
|
||||
const style = props.style;
|
||||
|
||||
return (
|
||||
<div className="modal-bg">
|
||||
<div className="modal" style={style}>
|
||||
<div className="modal-header">{title ?? "Unknown modal"}</div>
|
||||
<div className="modal-body">{props.children ?? "Missing body"}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -35,4 +35,4 @@ export const ZiB = Math.pow(1024, 7);
|
||||
/**
|
||||
* @constant {number} - Size of 1 YiB
|
||||
*/
|
||||
export const YiB = Math.pow(1024, 8);
|
||||
export const YiB = Math.pow(1024, 8);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {useSelector} from "react-redux";
|
||||
import {VoidApi} from "@void-cat/api";
|
||||
import { useSelector } from "react-redux";
|
||||
import { VoidApi } from "@void-cat/api";
|
||||
|
||||
import {RootState} from "Store";
|
||||
import {ApiHost} from "Const";
|
||||
import { RootState } from "Store";
|
||||
import { ApiHost } from "Const";
|
||||
|
||||
export default function useApi() {
|
||||
const auth = useSelector((s: RootState) => s.login.jwt);
|
||||
return new VoidApi(ApiHost, auth);
|
||||
}
|
||||
const auth = useSelector((s: RootState) => s.login.jwt);
|
||||
return new VoidApi(ApiHost, auth ? () => Promise.resolve(`Bearer ${auth}`) : undefined);
|
||||
}
|
||||
|
@ -1,39 +1,39 @@
|
||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
||||
import {Profile} from "@void-cat/api";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Profile } from "@void-cat/api";
|
||||
|
||||
interface LoginStore {
|
||||
jwt?: string
|
||||
profile?: Profile
|
||||
jwt?: string;
|
||||
profile?: Profile;
|
||||
}
|
||||
|
||||
interface SetAuthPayload {
|
||||
jwt: string
|
||||
profile?: Profile
|
||||
jwt: string;
|
||||
profile?: Profile;
|
||||
}
|
||||
|
||||
const LocalStorageKey = "token";
|
||||
export const LoginState = createSlice({
|
||||
name: "Login",
|
||||
initialState: {
|
||||
jwt: window.localStorage.getItem(LocalStorageKey) ?? undefined,
|
||||
profile: undefined
|
||||
} as LoginStore,
|
||||
reducers: {
|
||||
setAuth: (state, action: PayloadAction<SetAuthPayload>) => {
|
||||
state.jwt = action.payload.jwt;
|
||||
state.profile = action.payload.profile;
|
||||
window.localStorage.setItem(LocalStorageKey, state.jwt);
|
||||
},
|
||||
setProfile: (state, action: PayloadAction<Profile>) => {
|
||||
state.profile = action.payload;
|
||||
},
|
||||
logout: (state) => {
|
||||
state.jwt = undefined;
|
||||
state.profile = undefined;
|
||||
window.localStorage.removeItem(LocalStorageKey);
|
||||
}
|
||||
}
|
||||
name: "Login",
|
||||
initialState: {
|
||||
jwt: window.localStorage.getItem(LocalStorageKey) ?? undefined,
|
||||
profile: undefined,
|
||||
} as LoginStore,
|
||||
reducers: {
|
||||
setAuth: (state, action: PayloadAction<SetAuthPayload>) => {
|
||||
state.jwt = action.payload.jwt;
|
||||
state.profile = action.payload.profile;
|
||||
window.localStorage.setItem(LocalStorageKey, state.jwt);
|
||||
},
|
||||
setProfile: (state, action: PayloadAction<Profile>) => {
|
||||
state.profile = action.payload;
|
||||
},
|
||||
logout: (state) => {
|
||||
state.jwt = undefined;
|
||||
state.profile = undefined;
|
||||
window.localStorage.removeItem(LocalStorageKey);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {setAuth, setProfile, logout} = LoginState.actions;
|
||||
export default LoginState.reducer;
|
||||
export const { setAuth, setProfile, logout } = LoginState.actions;
|
||||
export default LoginState.reducer;
|
||||
|
@ -1,3 +1,2 @@
|
||||
.donate {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,50 @@
|
||||
import "./Donate.css"
|
||||
import {useState} from "react";
|
||||
import "./Donate.css";
|
||||
import { useState } from "react";
|
||||
|
||||
export function Donate() {
|
||||
const Hostname = "pay.v0l.io";
|
||||
const StoreId = "GdRya8MAvZYhyviA4ypFgijBknNoDEkg12ro8efLcZp5";
|
||||
const Hostname = "pay.v0l.io";
|
||||
const StoreId = "GdRya8MAvZYhyviA4ypFgijBknNoDEkg12ro8efLcZp5";
|
||||
|
||||
const [currency, setCurrency] = useState("USD");
|
||||
const [price, setPrice] = useState(1);
|
||||
const [currency, setCurrency] = useState("USD");
|
||||
const [price, setPrice] = useState(1);
|
||||
|
||||
return (
|
||||
<div className="page donate">
|
||||
<h2>Donate with Bitcoin</h2>
|
||||
<form method="POST" action={`https://${Hostname}/api/v1/invoices`} className="flex">
|
||||
<input type="hidden" name="storeId" value={StoreId}/>
|
||||
<input type="hidden" name="checkoutDesc" value="Donation"/>
|
||||
<div className="flex">
|
||||
<input name="price" type="number" min="1" step="1" value={price}
|
||||
onChange={(e) => setPrice(parseFloat(e.target.value))}/>
|
||||
<select name="currency" value={currency} onChange={(e) => setCurrency(e.target.value)}>
|
||||
<option>USD</option>
|
||||
<option>GBP</option>
|
||||
<option>EUR</option>
|
||||
<option>BTC</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="image"
|
||||
name="submit"
|
||||
src={`https://${Hostname}/img/paybutton/pay.svg`}
|
||||
alt="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor"/>
|
||||
</form>
|
||||
return (
|
||||
<div className="page donate">
|
||||
<h2>Donate with Bitcoin</h2>
|
||||
<form
|
||||
method="POST"
|
||||
action={`https://${Hostname}/api/v1/invoices`}
|
||||
className="flex"
|
||||
>
|
||||
<input type="hidden" name="storeId" value={StoreId} />
|
||||
<input type="hidden" name="checkoutDesc" value="Donation" />
|
||||
<div className="flex">
|
||||
<input
|
||||
name="price"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(parseFloat(e.target.value))}
|
||||
/>
|
||||
<select
|
||||
name="currency"
|
||||
value={currency}
|
||||
onChange={(e) => setCurrency(e.target.value)}
|
||||
>
|
||||
<option>USD</option>
|
||||
<option>GBP</option>
|
||||
<option>EUR</option>
|
||||
<option>BTC</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<input
|
||||
type="image"
|
||||
name="submit"
|
||||
src={`https://${Hostname}/img/paybutton/pay.svg`}
|
||||
alt="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,38 +1,41 @@
|
||||
.preview {
|
||||
margin-top: 2vh;
|
||||
margin-top: 2vh;
|
||||
}
|
||||
|
||||
.preview img, .preview video, .preview object, .preview audio {
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
.preview img,
|
||||
.preview video,
|
||||
.preview object,
|
||||
.preview audio {
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.preview .file-stats {
|
||||
line-height: 32px;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview .file-stats svg {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.preview .virus-warning {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid red;
|
||||
margin-bottom: 5px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid red;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.preview .encrypted {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #bbbbbb;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #bbbbbb;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
color: red;
|
||||
}
|
||||
|
@ -1,285 +1,329 @@
|
||||
import "./FilePreview.css";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {Helmet} from "react-helmet";
|
||||
import {PaymentOrder, VoidFileResponse, StreamEncryption} from "@void-cat/api";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
PaymentOrder,
|
||||
VoidFileResponse,
|
||||
StreamEncryption,
|
||||
} from "@void-cat/api";
|
||||
|
||||
import {TextPreview} from "../Components/FilePreview/TextPreview";
|
||||
import {FileEdit} from "../Components/FileEdit/FileEdit";
|
||||
import {FilePayment} from "../Components/FilePreview/FilePayment";
|
||||
import {InlineProfile} from "../Components/Shared/InlineProfile";
|
||||
import {VoidButton} from "../Components/Shared/VoidButton";
|
||||
import {useFileTransfer} from "../Components/Shared/FileTransferHook";
|
||||
import { TextPreview } from "../Components/FilePreview/TextPreview";
|
||||
import { FileEdit } from "../Components/FileEdit/FileEdit";
|
||||
import { FilePayment } from "../Components/FilePreview/FilePayment";
|
||||
import { InlineProfile } from "../Components/Shared/InlineProfile";
|
||||
import { VoidButton } from "../Components/Shared/VoidButton";
|
||||
import { useFileTransfer } from "../Components/Shared/FileTransferHook";
|
||||
import Icon from "../Components/Shared/Icon";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
import {FormatBytes} from "Util";
|
||||
import {ApiHost} from "Const";
|
||||
import { FormatBytes } from "Util";
|
||||
import { ApiHost } from "Const";
|
||||
|
||||
export function FilePreview() {
|
||||
const Api = useApi();
|
||||
const params = useParams();
|
||||
const [info, setInfo] = useState<VoidFileResponse>();
|
||||
const [order, setOrder] = useState<PaymentOrder>();
|
||||
const [link, setLink] = useState("#");
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const {speed, progress, update, setFileSize} = useFileTransfer();
|
||||
const Api = useApi();
|
||||
const params = useParams();
|
||||
const [info, setInfo] = useState<VoidFileResponse>();
|
||||
const [order, setOrder] = useState<PaymentOrder>();
|
||||
const [link, setLink] = useState("#");
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const { speed, progress, update, setFileSize } = useFileTransfer();
|
||||
|
||||
async function loadInfo() {
|
||||
if (params.id) {
|
||||
const i = await Api.fileInfo(params.id);
|
||||
setInfo(i);
|
||||
}
|
||||
async function loadInfo() {
|
||||
if (params.id) {
|
||||
const i = await Api.fileInfo(params.id);
|
||||
setInfo(i);
|
||||
}
|
||||
}
|
||||
|
||||
function isFileEncrypted() {
|
||||
return "string" === typeof info?.metadata?.encryptionParams
|
||||
function isFileEncrypted() {
|
||||
return "string" === typeof info?.metadata?.encryptionParams;
|
||||
}
|
||||
|
||||
function isDecrypted() {
|
||||
return link.startsWith("blob:");
|
||||
}
|
||||
|
||||
function isPaymentRequired() {
|
||||
return info?.payment?.required === true && !order;
|
||||
}
|
||||
|
||||
function canAccessFile() {
|
||||
if (isPaymentRequired()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDecrypted() {
|
||||
return link.startsWith("blob:");
|
||||
if (isFileEncrypted() && !isDecrypted()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isPaymentRequired() {
|
||||
return info?.payment?.required === true && !order;
|
||||
}
|
||||
async function decryptFile() {
|
||||
if (!info) return;
|
||||
|
||||
function canAccessFile() {
|
||||
if (isPaymentRequired()) {
|
||||
return false;
|
||||
}
|
||||
if (isFileEncrypted() && !isDecrypted()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function decryptFile() {
|
||||
if (!info) return;
|
||||
|
||||
try {
|
||||
let hashKey = key.match(/([0-9a-z]{32}):([0-9a-z]{24})/);
|
||||
if (hashKey?.length === 3) {
|
||||
let [key, iv] = [hashKey[1], hashKey[2]];
|
||||
let enc = new StreamEncryption(key, iv, info.metadata?.encryptionParams);
|
||||
|
||||
let rsp = await fetch(link);
|
||||
if (rsp.ok) {
|
||||
const reader = rsp.body?.pipeThrough(enc.getDecryptionTransform())
|
||||
.pipeThrough(decryptionProgressTransform());
|
||||
const newResponse = new Response(reader);
|
||||
setLink(window.URL.createObjectURL(await newResponse.blob()));
|
||||
}
|
||||
} else {
|
||||
setError("Invalid encryption key format");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decryptionProgressTransform() {
|
||||
return new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
update(chunk.length);
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderEncryptedDownload() {
|
||||
if (!isFileEncrypted() || isDecrypted() || isPaymentRequired()) return;
|
||||
return (
|
||||
<div className="encrypted">
|
||||
<h3>This file is encrypted, please enter the encryption key:</h3>
|
||||
<input type="password" placeholder="Encryption key" value={key}
|
||||
onChange={(e) => setKey(e.target.value)}/>
|
||||
<VoidButton onClick={() => decryptFile()}>Decrypt</VoidButton>
|
||||
{progress > 0 && `${(100 * progress).toFixed(0)}% (${FormatBytes(speed)}/s)`}
|
||||
{error && <h4 className="error">{error}</h4>}
|
||||
</div>
|
||||
try {
|
||||
let hashKey = key.match(/([0-9a-z]{32}):([0-9a-z]{24})/);
|
||||
if (hashKey?.length === 3) {
|
||||
let [key, iv] = [hashKey[1], hashKey[2]];
|
||||
let enc = new StreamEncryption(
|
||||
key,
|
||||
iv,
|
||||
info.metadata?.encryptionParams,
|
||||
);
|
||||
}
|
||||
|
||||
function renderPayment() {
|
||||
if (!info) return;
|
||||
|
||||
if (info.payment && info.payment.service !== 0 && !order) {
|
||||
return <FilePayment file={info} onPaid={loadInfo}/>;
|
||||
let rsp = await fetch(link);
|
||||
if (rsp.ok) {
|
||||
const reader = rsp.body
|
||||
?.pipeThrough(enc.getDecryptionTransform())
|
||||
.pipeThrough(decryptionProgressTransform());
|
||||
const newResponse = new Response(reader);
|
||||
setLink(window.URL.createObjectURL(await newResponse.blob()));
|
||||
}
|
||||
} else {
|
||||
setError("Invalid encryption key format");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError("Unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview() {
|
||||
if (!canAccessFile() || !info) return;
|
||||
|
||||
if (info.metadata) {
|
||||
switch (info.metadata.mimeType) {
|
||||
case "image/avif":
|
||||
case "image/bmp":
|
||||
case "image/gif":
|
||||
case "image/svg+xml":
|
||||
case "image/tiff":
|
||||
case "image/webp":
|
||||
case "image/jpg":
|
||||
case "image/jpeg":
|
||||
case "image/png": {
|
||||
return <img src={link} alt={info.metadata.name}/>;
|
||||
}
|
||||
case "audio/aac":
|
||||
case "audio/opus":
|
||||
case "audio/wav":
|
||||
case "audio/webm":
|
||||
case "audio/midi":
|
||||
case "audio/mpeg":
|
||||
case "audio/ogg": {
|
||||
return <audio src={link} controls/>;
|
||||
}
|
||||
case "video/x-msvideo":
|
||||
case "video/mpeg":
|
||||
case "video/ogg":
|
||||
case "video/mp2t":
|
||||
case "video/mp4":
|
||||
case "video/matroksa":
|
||||
case "video/x-matroska":
|
||||
case "video/webm":
|
||||
case "video/quicktime": {
|
||||
return <video src={link} controls/>;
|
||||
}
|
||||
case "application/json":
|
||||
case "text/javascript":
|
||||
case "text/html":
|
||||
case "text/csv":
|
||||
case "text/css":
|
||||
case "text/plain": {
|
||||
return <TextPreview link={link}/>;
|
||||
}
|
||||
case "application/pdf": {
|
||||
return <object data={link}/>;
|
||||
}
|
||||
default: {
|
||||
return <h3>{info.metadata?.name ?? info.id}</h3>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderOpenGraphTags() {
|
||||
const tags = [
|
||||
<meta key="og-site_name" property={"og:site_name"} content={"void.cat"}/>,
|
||||
<meta key="og-title" property={"og:title"} content={info?.metadata?.name}/>,
|
||||
<meta key="og-description" property={"og:description"} content={info?.metadata?.description}/>,
|
||||
<meta key="og-url" property={"og:url"} content={`https://${window.location.host}/${info?.id}`}/>
|
||||
];
|
||||
|
||||
const mime = info?.metadata?.mimeType;
|
||||
if (mime?.startsWith("image/")) {
|
||||
tags.push(<meta key="og-image" property={"og:image"} content={link}/>);
|
||||
tags.push(<meta key="og-image-type" property={"og:image:type"} content={mime}/>);
|
||||
} else if (mime?.startsWith("video/")) {
|
||||
tags.push(<meta key="og-video" property={"og:video"} content={link}/>);
|
||||
tags.push(<meta key="og-video-type" property={"og:video:type"} content={mime}/>);
|
||||
} else if (mime?.startsWith("audio/")) {
|
||||
tags.push(<meta key="og-audio" property={"og:audio"} content={link}/>);
|
||||
tags.push(<meta key="og-audio-type" property={"og:audio:type"} content={mime}/>);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
function renderVirusWarning() {
|
||||
if (info?.virusScan?.isVirus === true) {
|
||||
let scanResult = info.virusScan;
|
||||
return (
|
||||
<div className="virus-warning">
|
||||
<p>
|
||||
This file apears to be a virus, take care when downloading this file.
|
||||
</p>
|
||||
Detected as:
|
||||
<pre>
|
||||
{scanResult.names}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadInfo().catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (info) {
|
||||
const fileLink = info.metadata?.url ?? `${ApiHost}/d/${info.id}`;
|
||||
setFileSize(info.metadata?.size ?? 0);
|
||||
|
||||
const order = window.localStorage.getItem(`payment-${info.id}`);
|
||||
if (order) {
|
||||
const orderObj = JSON.parse(order);
|
||||
setOrder(orderObj);
|
||||
setLink(`${fileLink}?orderId=${orderObj.id}`);
|
||||
} else {
|
||||
setLink(fileLink);
|
||||
}
|
||||
}
|
||||
}, [info]);
|
||||
function decryptionProgressTransform() {
|
||||
return new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
update(chunk.length);
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderEncryptedDownload() {
|
||||
if (!isFileEncrypted() || isDecrypted() || isPaymentRequired()) return;
|
||||
return (
|
||||
<div className="preview page">
|
||||
{info ? (
|
||||
<Fragment>
|
||||
<Helmet>
|
||||
<title>void.cat - {info.metadata?.name ?? info.id}</title>
|
||||
{info.metadata?.description ?
|
||||
<meta name="description" content={info.metadata?.description}/> : null}
|
||||
{renderOpenGraphTags()}
|
||||
</Helmet>
|
||||
{renderVirusWarning()}
|
||||
<div className="flex flex-center">
|
||||
<div className="flx-grow">
|
||||
{info.uploader ? <InlineProfile profile={info.uploader}/> : null}
|
||||
</div>
|
||||
<div>
|
||||
{canAccessFile() &&
|
||||
<>
|
||||
|
||||
<a className="btn" href={info?.metadata?.magnetLink}>
|
||||
<Icon name="link" size={14} className="mr10"/>
|
||||
Magnet
|
||||
</a>
|
||||
<a className="btn" href={`${link}.torrent`}
|
||||
download={info.metadata?.name ?? info.id}>
|
||||
<Icon name="file" size={14} className="mr10"/>
|
||||
Torrent
|
||||
</a>
|
||||
<a className="btn" href={link}
|
||||
download={info.metadata?.name ?? info.id}>
|
||||
<Icon name="download" size={14} className="mr10"/>
|
||||
Direct Download
|
||||
</a>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
{renderPayment()}
|
||||
{renderPreview()}
|
||||
{renderEncryptedDownload()}
|
||||
<div className="file-stats">
|
||||
<div>
|
||||
<Icon name="download-cloud"/>
|
||||
{FormatBytes(info?.bandwidth?.egress ?? 0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="save"/>
|
||||
{FormatBytes(info?.metadata?.size ?? 0, 2)}
|
||||
</div>
|
||||
</div>
|
||||
<FileEdit file={info}/>
|
||||
</Fragment>
|
||||
) : "Not Found"}
|
||||
</div>
|
||||
<div className="encrypted">
|
||||
<h3>This file is encrypted, please enter the encryption key:</h3>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Encryption key"
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
/>
|
||||
<VoidButton onClick={() => decryptFile()}>Decrypt</VoidButton>
|
||||
{progress > 0 &&
|
||||
`${(100 * progress).toFixed(0)}% (${FormatBytes(speed)}/s)`}
|
||||
{error && <h4 className="error">{error}</h4>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPayment() {
|
||||
if (!info) return;
|
||||
|
||||
if (info.payment && info.payment.service !== 0 && !order) {
|
||||
return <FilePayment file={info} onPaid={loadInfo} />;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview() {
|
||||
if (!canAccessFile() || !info) return;
|
||||
|
||||
if (info.metadata) {
|
||||
switch (info.metadata.mimeType) {
|
||||
case "image/avif":
|
||||
case "image/bmp":
|
||||
case "image/gif":
|
||||
case "image/svg+xml":
|
||||
case "image/tiff":
|
||||
case "image/webp":
|
||||
case "image/jpg":
|
||||
case "image/jpeg":
|
||||
case "image/png": {
|
||||
return <img src={link} alt={info.metadata.name} />;
|
||||
}
|
||||
case "audio/aac":
|
||||
case "audio/opus":
|
||||
case "audio/wav":
|
||||
case "audio/webm":
|
||||
case "audio/midi":
|
||||
case "audio/mpeg":
|
||||
case "audio/ogg": {
|
||||
return <audio src={link} controls />;
|
||||
}
|
||||
case "video/x-msvideo":
|
||||
case "video/mpeg":
|
||||
case "video/ogg":
|
||||
case "video/mp2t":
|
||||
case "video/mp4":
|
||||
case "video/matroksa":
|
||||
case "video/x-matroska":
|
||||
case "video/webm":
|
||||
case "video/quicktime": {
|
||||
return <video src={link} controls />;
|
||||
}
|
||||
case "application/json":
|
||||
case "text/javascript":
|
||||
case "text/html":
|
||||
case "text/csv":
|
||||
case "text/css":
|
||||
case "text/plain": {
|
||||
return <TextPreview link={link} />;
|
||||
}
|
||||
case "application/pdf": {
|
||||
return <object data={link} />;
|
||||
}
|
||||
default: {
|
||||
return <h3>{info.metadata?.name ?? info.id}</h3>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderOpenGraphTags() {
|
||||
const tags = [
|
||||
<meta
|
||||
key="og-site_name"
|
||||
property={"og:site_name"}
|
||||
content={"void.cat"}
|
||||
/>,
|
||||
<meta
|
||||
key="og-title"
|
||||
property={"og:title"}
|
||||
content={info?.metadata?.name}
|
||||
/>,
|
||||
<meta
|
||||
key="og-description"
|
||||
property={"og:description"}
|
||||
content={info?.metadata?.description}
|
||||
/>,
|
||||
<meta
|
||||
key="og-url"
|
||||
property={"og:url"}
|
||||
content={`https://${window.location.host}/${info?.id}`}
|
||||
/>,
|
||||
];
|
||||
|
||||
const mime = info?.metadata?.mimeType;
|
||||
if (mime?.startsWith("image/")) {
|
||||
tags.push(<meta key="og-image" property={"og:image"} content={link} />);
|
||||
tags.push(
|
||||
<meta key="og-image-type" property={"og:image:type"} content={mime} />,
|
||||
);
|
||||
} else if (mime?.startsWith("video/")) {
|
||||
tags.push(<meta key="og-video" property={"og:video"} content={link} />);
|
||||
tags.push(
|
||||
<meta key="og-video-type" property={"og:video:type"} content={mime} />,
|
||||
);
|
||||
} else if (mime?.startsWith("audio/")) {
|
||||
tags.push(<meta key="og-audio" property={"og:audio"} content={link} />);
|
||||
tags.push(
|
||||
<meta key="og-audio-type" property={"og:audio:type"} content={mime} />,
|
||||
);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
function renderVirusWarning() {
|
||||
if (info?.virusScan?.isVirus === true) {
|
||||
let scanResult = info.virusScan;
|
||||
return (
|
||||
<div className="virus-warning">
|
||||
<p>
|
||||
This file apears to be a virus, take care when downloading this
|
||||
file.
|
||||
</p>
|
||||
Detected as:
|
||||
<pre>{scanResult.names}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadInfo().catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (info) {
|
||||
const fileLink = info.metadata?.url ?? `${ApiHost}/d/${info.id}`;
|
||||
setFileSize(info.metadata?.size ?? 0);
|
||||
|
||||
const order = window.localStorage.getItem(`payment-${info.id}`);
|
||||
if (order) {
|
||||
const orderObj = JSON.parse(order);
|
||||
setOrder(orderObj);
|
||||
setLink(`${fileLink}?orderId=${orderObj.id}`);
|
||||
} else {
|
||||
setLink(fileLink);
|
||||
}
|
||||
}
|
||||
}, [info]);
|
||||
|
||||
return (
|
||||
<div className="preview page">
|
||||
{info ? (
|
||||
<Fragment>
|
||||
<Helmet>
|
||||
<title>void.cat - {info.metadata?.name ?? info.id}</title>
|
||||
{info.metadata?.description ? (
|
||||
<meta name="description" content={info.metadata?.description} />
|
||||
) : null}
|
||||
{renderOpenGraphTags()}
|
||||
</Helmet>
|
||||
{renderVirusWarning()}
|
||||
<div className="flex flex-center">
|
||||
<div className="flx-grow">
|
||||
{info.uploader ? <InlineProfile profile={info.uploader} /> : null}
|
||||
</div>
|
||||
<div>
|
||||
{canAccessFile() && (
|
||||
<>
|
||||
<a className="btn" href={info?.metadata?.magnetLink}>
|
||||
<Icon name="link" size={14} className="mr10" />
|
||||
Magnet
|
||||
</a>
|
||||
<a
|
||||
className="btn"
|
||||
href={`${link}.torrent`}
|
||||
download={info.metadata?.name ?? info.id}
|
||||
>
|
||||
<Icon name="file" size={14} className="mr10" />
|
||||
Torrent
|
||||
</a>
|
||||
<a
|
||||
className="btn"
|
||||
href={link}
|
||||
download={info.metadata?.name ?? info.id}
|
||||
>
|
||||
<Icon name="download" size={14} className="mr10" />
|
||||
Direct Download
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderPayment()}
|
||||
{renderPreview()}
|
||||
{renderEncryptedDownload()}
|
||||
<div className="file-stats">
|
||||
<div>
|
||||
<Icon name="download-cloud" />
|
||||
{FormatBytes(info?.bandwidth?.egress ?? 0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="save" />
|
||||
{FormatBytes(info?.metadata?.size ?? 0, 2)}
|
||||
</div>
|
||||
</div>
|
||||
<FileEdit file={info} />
|
||||
</Fragment>
|
||||
) : (
|
||||
"Not Found"
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
import {useSelector} from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {Dropzone} from "../Components/FileUpload/Dropzone";
|
||||
import {GlobalStats} from "../Components/HomePage/GlobalStats";
|
||||
import {FooterLinks} from "../Components/HomePage/FooterLinks";
|
||||
import {MetricsGraph} from "../Components/HomePage/MetricsGraph";
|
||||
import { Dropzone } from "../Components/FileUpload/Dropzone";
|
||||
import { GlobalStats } from "../Components/HomePage/GlobalStats";
|
||||
import { FooterLinks } from "../Components/HomePage/FooterLinks";
|
||||
import { MetricsGraph } from "../Components/HomePage/MetricsGraph";
|
||||
|
||||
import {RootState} from "Store";
|
||||
import { RootState } from "Store";
|
||||
|
||||
export function HomePage() {
|
||||
const metrics = useSelector((s: RootState) => s.info.info);
|
||||
return (
|
||||
<div className="page">
|
||||
<Dropzone/>
|
||||
<GlobalStats/>
|
||||
<MetricsGraph metrics={metrics?.timeSeriesMetrics}/>
|
||||
<FooterLinks/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const metrics = useSelector((s: RootState) => s.info.info);
|
||||
return (
|
||||
<div className="page">
|
||||
<Dropzone />
|
||||
<GlobalStats />
|
||||
<MetricsGraph metrics={metrics?.timeSeriesMetrics} />
|
||||
<FooterLinks />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,49 +1,48 @@
|
||||
.profile {
|
||||
|
||||
}
|
||||
|
||||
.profile .name {
|
||||
font-size: 30px;
|
||||
margin: 10px 0;
|
||||
font-size: 30px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.profile .name input {
|
||||
background: unset;
|
||||
color: white;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
border: unset;
|
||||
background: unset;
|
||||
color: white;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
border: unset;
|
||||
}
|
||||
|
||||
.profile .avatar {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
border-radius: 40px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
border: 1px solid;
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
border-radius: 40px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.profile .avatar .edit-avatar {
|
||||
opacity: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile .avatar .edit-avatar:hover {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.profile .roles > span {
|
||||
margin-right: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.profile dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -1,198 +1,242 @@
|
||||
import "./Profile.css";
|
||||
import {Fragment, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {default as moment} from "moment";
|
||||
import {useLoaderData} from "react-router-dom";
|
||||
import {Profile} from "@void-cat/api";
|
||||
import { Fragment, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { default as moment } from "moment";
|
||||
import { useLoaderData } from "react-router-dom";
|
||||
import { Profile } from "@void-cat/api";
|
||||
|
||||
import useApi from "Hooks/UseApi";
|
||||
import {RootState} from "Store";
|
||||
import {DefaultAvatar} from "Const";
|
||||
import { RootState } from "Store";
|
||||
import { DefaultAvatar } from "Const";
|
||||
|
||||
import {logout, setProfile as setGlobalProfile} from "../LoginState";
|
||||
import {FileList} from "../Components/Shared/FileList";
|
||||
import {VoidButton} from "../Components/Shared/VoidButton";
|
||||
import { logout, setProfile as setGlobalProfile } from "../LoginState";
|
||||
import { FileList } from "../Components/Shared/FileList";
|
||||
import { VoidButton } from "../Components/Shared/VoidButton";
|
||||
import ApiKeyList from "../Components/Profile/ApiKeyList";
|
||||
|
||||
export function ProfilePage() {
|
||||
const dispatch = useDispatch();
|
||||
const loader = useLoaderData();
|
||||
const Api = useApi();
|
||||
const dispatch = useDispatch();
|
||||
const loader = useLoaderData();
|
||||
const Api = useApi();
|
||||
|
||||
const [profile, setProfile] = useState(loader as Profile | null);
|
||||
const [emailCode, setEmailCode] = useState("");
|
||||
const [emailCodeError, setEmailCodeError] = useState("");
|
||||
const [newCodeSent, setNewCodeSent] = useState(false);
|
||||
const [profile, setProfile] = useState(loader as Profile | null);
|
||||
const [emailCode, setEmailCode] = useState("");
|
||||
const [emailCodeError, setEmailCodeError] = useState("");
|
||||
const [newCodeSent, setNewCodeSent] = useState(false);
|
||||
|
||||
const localProfile = useSelector((s: RootState) => s.login.profile);
|
||||
const localProfile = useSelector((s: RootState) => s.login.profile);
|
||||
|
||||
const canEdit = localProfile?.id === profile?.id;
|
||||
const needsEmailVerify = canEdit && profile?.needsVerification === true;
|
||||
const cantEditProfile = canEdit && !needsEmailVerify;
|
||||
const canEdit = localProfile?.id === profile?.id;
|
||||
const needsEmailVerify = canEdit && profile?.needsVerification === true;
|
||||
const cantEditProfile = canEdit && !needsEmailVerify;
|
||||
|
||||
async function changeAvatar() {
|
||||
const res = await new Promise<Array<File>>((resolve) => {
|
||||
let i = document.createElement('input');
|
||||
i.setAttribute('type', 'file');
|
||||
i.setAttribute('multiple', '');
|
||||
i.addEventListener('change', async function (evt) {
|
||||
resolve((evt.target as any).files);
|
||||
});
|
||||
i.click();
|
||||
});
|
||||
|
||||
const file = res[0];
|
||||
const uploader = Api.getUploader(file);
|
||||
const rsp = await uploader.upload();
|
||||
if (rsp.ok) {
|
||||
setProfile({
|
||||
...profile,
|
||||
avatar: rsp.file?.id
|
||||
} as Profile);
|
||||
}
|
||||
async function changeAvatar() {
|
||||
const res = await new Promise<Array<File>>((resolve) => {
|
||||
let i = document.createElement("input");
|
||||
i.setAttribute("type", "file");
|
||||
i.setAttribute("multiple", "");
|
||||
i.addEventListener("change", async function (evt) {
|
||||
resolve((evt.target as any).files);
|
||||
});
|
||||
i.click();
|
||||
});
|
||||
|
||||
const file = res[0];
|
||||
const uploader = Api.getUploader(file);
|
||||
const rsp = await uploader.upload();
|
||||
if (rsp.ok) {
|
||||
setProfile({
|
||||
...profile,
|
||||
avatar: rsp.file?.id,
|
||||
} as Profile);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUser(p: Profile) {
|
||||
try {
|
||||
await Api.updateUser(p);
|
||||
dispatch(setGlobalProfile(p));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
async function saveUser(p: Profile) {
|
||||
try {
|
||||
await Api.updateUser(p);
|
||||
dispatch(setGlobalProfile(p));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCode(id: string, code: string) {
|
||||
try {
|
||||
await Api.submitVerifyCode(id, code);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setEmailCodeError("Invalid or expired code.");
|
||||
}
|
||||
async function submitCode(id: string, code: string) {
|
||||
try {
|
||||
await Api.submitVerifyCode(id, code);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setEmailCodeError("Invalid or expired code.");
|
||||
}
|
||||
}
|
||||
|
||||
async function sendNewCode(id: string) {
|
||||
setNewCodeSent(true);
|
||||
try {
|
||||
await Api.sendNewCode(id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setNewCodeSent(false);
|
||||
}
|
||||
async function sendNewCode(id: string) {
|
||||
setNewCodeSent(true);
|
||||
try {
|
||||
await Api.sendNewCode(id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setNewCodeSent(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmailVerify() {
|
||||
if (!profile) return;
|
||||
function renderEmailVerify() {
|
||||
if (!profile) return;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<h2>Please enter email verification code</h2>
|
||||
<small>Your account will automatically be deleted in 7 days if you do not verify your email
|
||||
address.</small>
|
||||
<br/>
|
||||
<input type="text" placeholder="Verification code" value={emailCode}
|
||||
onChange={(e) => setEmailCode(e.target.value)}/>
|
||||
<VoidButton onClick={() => submitCode(profile.id, emailCode)}>Submit</VoidButton>
|
||||
<VoidButton onClick={() => {
|
||||
dispatch(logout());
|
||||
}}>Logout</VoidButton>
|
||||
<br/>
|
||||
{emailCodeError && <b>{emailCodeError}</b>}
|
||||
{(emailCodeError && !newCodeSent) &&
|
||||
<a onClick={() => sendNewCode(profile.id)}>Send verification email</a>}
|
||||
</Fragment>
|
||||
);
|
||||
return (
|
||||
<Fragment>
|
||||
<h2>Please enter email verification code</h2>
|
||||
<small>
|
||||
Your account will automatically be deleted in 7 days if you do not
|
||||
verify your email address.
|
||||
</small>
|
||||
<br />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Verification code"
|
||||
value={emailCode}
|
||||
onChange={(e) => setEmailCode(e.target.value)}
|
||||
/>
|
||||
<VoidButton onClick={() => submitCode(profile.id, emailCode)}>
|
||||
Submit
|
||||
</VoidButton>
|
||||
<VoidButton
|
||||
onClick={() => {
|
||||
dispatch(logout());
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</VoidButton>
|
||||
<br />
|
||||
{emailCodeError && <b>{emailCodeError}</b>}
|
||||
{emailCodeError && !newCodeSent && (
|
||||
<a onClick={() => sendNewCode(profile.id)}>Send verification email</a>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function renderProfileEdit() {
|
||||
if (!profile) return;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<dl>
|
||||
<dt>Public Profile:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={profile.publicProfile}
|
||||
onChange={(e) =>
|
||||
setProfile({
|
||||
...profile,
|
||||
publicProfile: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</dd>
|
||||
<dt>Public Uploads:</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={profile.publicUploads}
|
||||
onChange={(e) =>
|
||||
setProfile({
|
||||
...profile,
|
||||
publicUploads: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
<div className="flex flex-center">
|
||||
<div>
|
||||
<VoidButton
|
||||
onClick={() => saveUser(profile)}
|
||||
options={{ showSuccess: true }}
|
||||
>
|
||||
Save
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div>
|
||||
<VoidButton
|
||||
onClick={() => {
|
||||
dispatch(logout());
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</VoidButton>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
let avatarUrl = profile.avatar ?? DefaultAvatar;
|
||||
if (!avatarUrl.startsWith("http")) {
|
||||
// assume void-cat hosted avatar
|
||||
avatarUrl = `/d/${avatarUrl}`;
|
||||
}
|
||||
|
||||
function renderProfileEdit() {
|
||||
if (!profile) return;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<dl>
|
||||
<dt>Public Profile:</dt>
|
||||
<dd>
|
||||
<input type="checkbox" checked={profile.publicProfile}
|
||||
onChange={(e) => setProfile({
|
||||
...profile,
|
||||
publicProfile: e.target.checked
|
||||
})}/>
|
||||
</dd>
|
||||
<dt>Public Uploads:</dt>
|
||||
<dd>
|
||||
<input type="checkbox" checked={profile.publicUploads}
|
||||
onChange={(e) => setProfile({
|
||||
...profile,
|
||||
publicUploads: e.target.checked
|
||||
})}/>
|
||||
</dd>
|
||||
|
||||
</dl>
|
||||
<div className="flex flex-center">
|
||||
<div>
|
||||
<VoidButton onClick={() => saveUser(profile)} options={{showSuccess: true}}>Save</VoidButton>
|
||||
</div>
|
||||
<div>
|
||||
<VoidButton onClick={() => {
|
||||
dispatch(logout());
|
||||
}}>Logout</VoidButton>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
let avatarUrl = profile.avatar ?? DefaultAvatar;
|
||||
if (!avatarUrl.startsWith("http")) {
|
||||
// assume void-cat hosted avatar
|
||||
avatarUrl = `/d/${avatarUrl}`;
|
||||
}
|
||||
let avatarStyles = {
|
||||
backgroundImage: `url(${avatarUrl})`
|
||||
};
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="profile">
|
||||
<div className="name">
|
||||
{cantEditProfile ?
|
||||
<input value={profile.name}
|
||||
onChange={(e) => setProfile({
|
||||
...profile,
|
||||
name: e.target.value
|
||||
})}/>
|
||||
: profile.name}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flx-1">
|
||||
<div className="avatar" style={avatarStyles}>
|
||||
{cantEditProfile ? <div className="edit-avatar" onClick={() => changeAvatar()}>
|
||||
<h3>Edit</h3>
|
||||
</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flx-1">
|
||||
<dl>
|
||||
<dt>Created</dt>
|
||||
<dd>{moment(profile.created).fromNow()}</dd>
|
||||
<dt>Roles</dt>
|
||||
<dd>{profile.roles.map(a => <span key={a} className="btn">{a}</span>)}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{cantEditProfile ? renderProfileEdit() : null}
|
||||
{needsEmailVerify ? renderEmailVerify() : null}
|
||||
<h1>Uploads</h1>
|
||||
<FileList loadPage={(req) => Api.listUserFiles(profile.id, req)}/>
|
||||
{cantEditProfile ? <ApiKeyList/> : null}
|
||||
</div>
|
||||
let avatarStyles = {
|
||||
backgroundImage: `url(${avatarUrl})`,
|
||||
};
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="profile">
|
||||
<div className="name">
|
||||
{cantEditProfile ? (
|
||||
<input
|
||||
value={profile.name}
|
||||
onChange={(e) =>
|
||||
setProfile({
|
||||
...profile,
|
||||
name: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
profile.name
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flx-1">
|
||||
<div className="avatar" style={avatarStyles}>
|
||||
{cantEditProfile ? (
|
||||
<div className="edit-avatar" onClick={() => changeAvatar()}>
|
||||
<h3>Edit</h3>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="page">
|
||||
<h1>Loading..</h1>
|
||||
<div className="flx-1">
|
||||
<dl>
|
||||
<dt>Created</dt>
|
||||
<dd>{moment(profile.created).fromNow()}</dd>
|
||||
<dt>Roles</dt>
|
||||
<dd>
|
||||
{profile.roles.map((a) => (
|
||||
<span key={a} className="btn">
|
||||
{a}
|
||||
</span>
|
||||
))}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
{cantEditProfile ? renderProfileEdit() : null}
|
||||
{needsEmailVerify ? renderEmailVerify() : null}
|
||||
<h1>Uploads</h1>
|
||||
<FileList loadPage={(req) => Api.listUserFiles(profile.id, req)} />
|
||||
{cantEditProfile ? <ApiKeyList /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="page">
|
||||
<h1>Loading..</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
import {useSelector} from "react-redux";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useEffect} from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import {Login} from "../Components/Shared/Login";
|
||||
import {RootState} from "Store";
|
||||
import { Login } from "../Components/Shared/Login";
|
||||
import { RootState } from "Store";
|
||||
|
||||
export function UserLogin() {
|
||||
const auth = useSelector((s: RootState) => s.login.jwt);
|
||||
const navigate = useNavigate();
|
||||
const auth = useSelector((s: RootState) => s.login.jwt);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (auth) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [auth, navigate]);
|
||||
useEffect(() => {
|
||||
if (auth) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [auth, navigate]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<Login/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="page">
|
||||
<Login />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
||||
import {SiteInfoResponse} from "@void-cat/api";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { SiteInfoResponse } from "@void-cat/api";
|
||||
|
||||
export const SiteInfoState = createSlice({
|
||||
name: "SiteInfo",
|
||||
initialState: {
|
||||
info: null as SiteInfoResponse | null
|
||||
name: "SiteInfo",
|
||||
initialState: {
|
||||
info: null as SiteInfoResponse | null,
|
||||
},
|
||||
reducers: {
|
||||
setInfo: (state, action: PayloadAction<SiteInfoResponse>) => {
|
||||
state.info = action.payload;
|
||||
},
|
||||
reducers: {
|
||||
setInfo: (state, action: PayloadAction<SiteInfoResponse>) => {
|
||||
state.info = action.payload;
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const {setInfo} = SiteInfoState.actions;
|
||||
export default SiteInfoState.reducer;
|
||||
export const { setInfo } = SiteInfoState.actions;
|
||||
export default SiteInfoState.reducer;
|
||||
|
@ -1,14 +1,14 @@
|
||||
import {configureStore} from "@reduxjs/toolkit";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import loginReducer from "./LoginState";
|
||||
import siteInfoReducer from "./SiteInfoStore";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
login: loginReducer,
|
||||
info: siteInfoReducer
|
||||
}
|
||||
reducer: {
|
||||
login: loginReducer,
|
||||
info: siteInfoReducer,
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export default store;
|
||||
export default store;
|
||||
|
@ -7,69 +7,63 @@ import * as Const from "Const";
|
||||
* @returns Bytes formatted in binary notation
|
||||
*/
|
||||
export function FormatBytes(b: number, f?: number) {
|
||||
f ??= 2;
|
||||
if (b >= Const.YiB)
|
||||
return (b / Const.YiB).toFixed(f) + ' YiB';
|
||||
if (b >= Const.ZiB)
|
||||
return (b / Const.ZiB).toFixed(f) + ' ZiB';
|
||||
if (b >= Const.EiB)
|
||||
return (b / Const.EiB).toFixed(f) + ' EiB';
|
||||
if (b >= Const.PiB)
|
||||
return (b / Const.PiB).toFixed(f) + ' PiB';
|
||||
if (b >= Const.TiB)
|
||||
return (b / Const.TiB).toFixed(f) + ' TiB';
|
||||
if (b >= Const.GiB)
|
||||
return (b / Const.GiB).toFixed(f) + ' GiB';
|
||||
if (b >= Const.MiB)
|
||||
return (b / Const.MiB).toFixed(f) + ' MiB';
|
||||
if (b >= Const.kiB)
|
||||
return (b / Const.kiB).toFixed(f) + ' KiB';
|
||||
return b.toFixed(f) + ' B'
|
||||
f ??= 2;
|
||||
if (b >= Const.YiB) return (b / Const.YiB).toFixed(f) + " YiB";
|
||||
if (b >= Const.ZiB) return (b / Const.ZiB).toFixed(f) + " ZiB";
|
||||
if (b >= Const.EiB) return (b / Const.EiB).toFixed(f) + " EiB";
|
||||
if (b >= Const.PiB) return (b / Const.PiB).toFixed(f) + " PiB";
|
||||
if (b >= Const.TiB) return (b / Const.TiB).toFixed(f) + " TiB";
|
||||
if (b >= Const.GiB) return (b / Const.GiB).toFixed(f) + " GiB";
|
||||
if (b >= Const.MiB) return (b / Const.MiB).toFixed(f) + " MiB";
|
||||
if (b >= Const.kiB) return (b / Const.kiB).toFixed(f) + " KiB";
|
||||
return b.toFixed(f) + " B";
|
||||
}
|
||||
|
||||
export function buf2hex(buffer: number[] | ArrayBuffer) {
|
||||
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
|
||||
return [...new Uint8Array(buffer)]
|
||||
.map((x) => x.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function ConstName(type: object, val: any) {
|
||||
for (let [k, v] of Object.entries(type)) {
|
||||
if (v === val) {
|
||||
return k;
|
||||
}
|
||||
for (let [k, v] of Object.entries(type)) {
|
||||
if (v === val) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function FormatCurrency(value: number, currency: string | number) {
|
||||
switch (currency) {
|
||||
case 0:
|
||||
case "BTC": {
|
||||
let hasDecimals = (value % 1) > 0;
|
||||
return `₿${value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: hasDecimals ? 8 : 0, // Sats
|
||||
maximumFractionDigits: 11 // MSats
|
||||
})}`;
|
||||
}
|
||||
case 1:
|
||||
case "USD": {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
});
|
||||
}
|
||||
case 2:
|
||||
case "EUR": {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "EUR"
|
||||
});
|
||||
}
|
||||
case 3:
|
||||
case "GBP": {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "GBP"
|
||||
});
|
||||
}
|
||||
switch (currency) {
|
||||
case 0:
|
||||
case "BTC": {
|
||||
let hasDecimals = value % 1 > 0;
|
||||
return `₿${value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: hasDecimals ? 8 : 0, // Sats
|
||||
maximumFractionDigits: 11, // MSats
|
||||
})}`;
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
case 1:
|
||||
case "USD": {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
});
|
||||
}
|
||||
case 2:
|
||||
case "EUR": {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
});
|
||||
}
|
||||
case 3:
|
||||
case "GBP": {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "GBP",
|
||||
});
|
||||
}
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
@ -1,104 +1,109 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap');
|
||||
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap");
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: black;
|
||||
color: white;
|
||||
margin: 0;
|
||||
font-family: "Source Code Pro", monospace;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
line-height: 1.1;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
border-radius: 10px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 10px 20px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
line-height: 1.1;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
border-radius: 10px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 10px 20px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.btn.disabled {
|
||||
background-color: #666;
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-inline {
|
||||
display: inline-flex;
|
||||
.flex-inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.flx-1 {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flx-2 {
|
||||
flex: 2;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.flx-grow {
|
||||
flex-grow: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="number"], input[type="password"], input[type="datetime-local"], input[type="checkbox"], select {
|
||||
display: inline-block;
|
||||
line-height: 1.1;
|
||||
border-radius: 10px;
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
border: 0;
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
input[type="datetime-local"],
|
||||
input[type="checkbox"],
|
||||
select {
|
||||
display: inline-block;
|
||||
line-height: 1.1;
|
||||
border-radius: 10px;
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
word-break: keep-all;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
word-break: keep-all;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table tr:nth-child(2n) {
|
||||
background-color: #111;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #222;
|
||||
text-align: start;
|
||||
background-color: #222;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
pre.copy {
|
||||
user-select: all;
|
||||
width: fit-content;
|
||||
border-radius: 4px;
|
||||
border: 1px solid;
|
||||
padding: 5px;
|
||||
user-select: all;
|
||||
width: fit-content;
|
||||
border-radius: 4px;
|
||||
border: 1px solid;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.mr10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement,
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
@ -1,18 +1,18 @@
|
||||
const {createProxyMiddleware} = require('http-proxy-middleware');
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
const settings = require("../package.json");
|
||||
|
||||
module.exports = function (app) {
|
||||
const proxy = createProxyMiddleware({
|
||||
target: settings.proxy,
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
});
|
||||
const proxy = createProxyMiddleware({
|
||||
target: settings.proxy,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
});
|
||||
|
||||
app.use('/admin', proxy);
|
||||
app.use('/d', proxy);
|
||||
app.use('/info', proxy);
|
||||
app.use('/upload', proxy);
|
||||
app.use('/auth', proxy);
|
||||
app.use('/swagger', proxy);
|
||||
app.use('/user', proxy);
|
||||
};
|
||||
app.use("/admin", proxy);
|
||||
app.use("/d", proxy);
|
||||
app.use("/info", proxy);
|
||||
app.use("/upload", proxy);
|
||||
app.use("/auth", proxy);
|
||||
app.use("/swagger", proxy);
|
||||
app.use("/user", proxy);
|
||||
};
|
||||
|
@ -1841,6 +1841,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:8.51.0":
|
||||
version: 8.51.0
|
||||
resolution: "@eslint/js@npm:8.51.0"
|
||||
checksum: 0228bf1e1e0414843e56d9ff362a2a72d579c078f93174666f29315690e9e30a8633ad72c923297f7fd7182381b5a476805ff04dac8debe638953eb1ded3ac73
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:^8.47.0":
|
||||
version: 8.47.0
|
||||
resolution: "@eslint/js@npm:8.47.0"
|
||||
@ -1871,6 +1878,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@humanwhocodes/config-array@npm:^0.11.11":
|
||||
version: 0.11.11
|
||||
resolution: "@humanwhocodes/config-array@npm:0.11.11"
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema": ^1.2.1
|
||||
debug: ^4.1.1
|
||||
minimatch: ^3.0.5
|
||||
checksum: db84507375ab77b8ffdd24f498a5b49ad6b64391d30dd2ac56885501d03964d29637e05b1ed5aefa09d57ac667e28028bc22d2da872bfcd619652fbdb5f4ca19
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@humanwhocodes/config-array@npm:^0.11.8":
|
||||
version: 0.11.8
|
||||
resolution: "@humanwhocodes/config-array@npm:0.11.8"
|
||||
@ -6422,6 +6440,53 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:^8.51.0":
|
||||
version: 8.51.0
|
||||
resolution: "eslint@npm:8.51.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": ^4.2.0
|
||||
"@eslint-community/regexpp": ^4.6.1
|
||||
"@eslint/eslintrc": ^2.1.2
|
||||
"@eslint/js": 8.51.0
|
||||
"@humanwhocodes/config-array": ^0.11.11
|
||||
"@humanwhocodes/module-importer": ^1.0.1
|
||||
"@nodelib/fs.walk": ^1.2.8
|
||||
ajv: ^6.12.4
|
||||
chalk: ^4.0.0
|
||||
cross-spawn: ^7.0.2
|
||||
debug: ^4.3.2
|
||||
doctrine: ^3.0.0
|
||||
escape-string-regexp: ^4.0.0
|
||||
eslint-scope: ^7.2.2
|
||||
eslint-visitor-keys: ^3.4.3
|
||||
espree: ^9.6.1
|
||||
esquery: ^1.4.2
|
||||
esutils: ^2.0.2
|
||||
fast-deep-equal: ^3.1.3
|
||||
file-entry-cache: ^6.0.1
|
||||
find-up: ^5.0.0
|
||||
glob-parent: ^6.0.2
|
||||
globals: ^13.19.0
|
||||
graphemer: ^1.4.0
|
||||
ignore: ^5.2.0
|
||||
imurmurhash: ^0.1.4
|
||||
is-glob: ^4.0.0
|
||||
is-path-inside: ^3.0.3
|
||||
js-yaml: ^4.1.0
|
||||
json-stable-stringify-without-jsonify: ^1.0.1
|
||||
levn: ^0.4.1
|
||||
lodash.merge: ^4.6.2
|
||||
minimatch: ^3.1.2
|
||||
natural-compare: ^1.4.0
|
||||
optionator: ^0.9.3
|
||||
strip-ansi: ^6.0.1
|
||||
text-table: ^0.2.0
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 214fa5d1fcb67af1b8992ce9584ccd85e1aa7a482f8b8ea5b96edc28fa838a18a3b69456db45fc1ed3ef95f1e9efa9714f737292dc681e572d471d02fda9649c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"espree@npm:^9.5.2":
|
||||
version: 9.5.2
|
||||
resolution: "espree@npm:9.5.2"
|
||||
@ -10975,6 +11040,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prettier@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "prettier@npm:3.0.3"
|
||||
bin:
|
||||
prettier: bin/prettier.cjs
|
||||
checksum: e10b9af02b281f6c617362ebd2571b1d7fc9fb8a3bd17e371754428cda992e5e8d8b7a046e8f7d3e2da1dcd21aa001e2e3c797402ebb6111b5cd19609dd228e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pretty-bytes@npm:^5.3.0, pretty-bytes@npm:^5.4.1":
|
||||
version: 5.6.0
|
||||
resolution: "pretty-bytes@npm:5.6.0"
|
||||
@ -11893,6 +11967,10 @@ __metadata:
|
||||
"root-workspace-0b6124@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "root-workspace-0b6124@workspace:."
|
||||
dependencies:
|
||||
eslint: ^8.51.0
|
||||
prettier: ^3.0.3
|
||||
typescript: ^5.2.2
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -13195,6 +13273,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:^5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "typescript@npm:5.2.2"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@^5.0.4#~builtin<compat/typescript>":
|
||||
version: 5.0.4
|
||||
resolution: "typescript@patch:typescript@npm%3A5.0.4#~builtin<compat/typescript>::version=5.0.4&hash=b5f058"
|
||||
@ -13215,6 +13303,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@^5.2.2#~builtin<compat/typescript>":
|
||||
version: 5.2.2
|
||||
resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin<compat/typescript>::version=5.2.2&hash=f3b441"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 0f4da2f15e6f1245e49db15801dbee52f2bbfb267e1c39225afdab5afee1a72839cd86000e65ee9d7e4dfaff12239d28beaf5ee431357fcced15fb08583d72ca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unbox-primitive@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "unbox-primitive@npm:1.0.2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user