mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 05:26:27 -08:00
refactor: 💩 monokit
This commit is contained in:
parent
f441e06a21
commit
d120378e75
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -3,4 +3,5 @@ node_modules
|
||||||
.data
|
.data
|
||||||
out
|
out
|
||||||
dist
|
dist
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
.svelte-kit
|
||||||
|
|
12
config.json
12
config.json
|
@ -1,15 +1,13 @@
|
||||||
{
|
{
|
||||||
"maxDiscordFiles": 1000,
|
"maxDiscordFiles": 500,
|
||||||
"maxDiscordFileSize": 10485760,
|
"maxDiscordFileSize": 10485760,
|
||||||
"targetGuild": "906767804575928390",
|
"targetChannel": "1160783463696302182",
|
||||||
"targetChannel": "1024080525993971913",
|
"requestTimeout": 1800000,
|
||||||
"requestTimeout": 3600000,
|
|
||||||
"maxUploadIdLength": 30,
|
"maxUploadIdLength": 30,
|
||||||
"accounts": {
|
"accounts": {
|
||||||
"registrationEnabled": true,
|
"registrationEnabled": true,
|
||||||
"requiredForUpload": false
|
"requiredForUpload": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"mail": {
|
"mail": {
|
||||||
"transport": {
|
"transport": {
|
||||||
"host": "smtp.fastmail.com",
|
"host": "smtp.fastmail.com",
|
||||||
|
@ -22,4 +20,4 @@
|
||||||
},
|
},
|
||||||
"trustProxy": true,
|
"trustProxy": true,
|
||||||
"forceSSL": false
|
"forceSSL": false
|
||||||
}
|
}
|
||||||
|
|
26
package.json
26
package.json
|
@ -5,12 +5,17 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./out/server/index.js",
|
"dev": "vite dev",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"build": "vite build",
|
||||||
"dev": "vite",
|
"preview": "vite preview",
|
||||||
"build": "tsc --build src/server && vite build",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||||
"preview": "vite preview"
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||||
|
"lint": "prettier --check .",
|
||||||
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"last 1 version"
|
||||||
|
],
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Etcetera (https://cetera.uk)",
|
"author": "Etcetera (https://cetera.uk)",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
|
@ -31,14 +36,17 @@
|
||||||
"dotenv": "^16.0.2",
|
"dotenv": "^16.0.2",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"formidable": "^3.5.1",
|
"formidable": "^3.5.1",
|
||||||
"hono": "^4.0.10",
|
"hono": "4.0.10",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.4.6",
|
"@hono/vite-dev-server": "^0.10.0",
|
||||||
|
"@sveltejs/adapter-auto": "^3.2.0",
|
||||||
|
"@sveltejs/kit": "^2.5.7",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||||
"@tsconfig/svelte": "^4.0.1",
|
"@tsconfig/svelte": "^4.0.1",
|
||||||
"@types/bytes": "^3.1.1",
|
"@types/bytes": "^3.1.1",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
|
@ -46,9 +54,9 @@
|
||||||
"@types/range-parser": "^1.2.6",
|
"@types/range-parser": "^1.2.6",
|
||||||
"discord-api-types": "^0.37.61",
|
"discord-api-types": "^0.37.61",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"svelte": "^3.55.1",
|
"svelte": "4",
|
||||||
"svelte-preprocess": "^5.1.3",
|
"svelte-preprocess": "^5.1.3",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"vite": "^4.5.0"
|
"vite": "5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
655
pnpm-lock.yaml
655
pnpm-lock.yaml
|
@ -6,8 +6,8 @@ settings:
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@hono/node-server':
|
'@hono/node-server':
|
||||||
specifier: ^1.2.0
|
specifier: ^1.8.2
|
||||||
version: 1.2.0
|
version: 1.8.2
|
||||||
'@types/body-parser':
|
'@types/body-parser':
|
||||||
specifier: ^1.19.2
|
specifier: ^1.19.2
|
||||||
version: 1.19.3
|
version: 1.19.3
|
||||||
|
@ -29,24 +29,30 @@ dependencies:
|
||||||
bytes:
|
bytes:
|
||||||
specifier: ^3.1.2
|
specifier: ^3.1.2
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
|
commander:
|
||||||
|
specifier: ^11.1.0
|
||||||
|
version: 11.1.0
|
||||||
cookie-parser:
|
cookie-parser:
|
||||||
specifier: ^1.4.6
|
specifier: ^1.4.6
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
discord.js:
|
|
||||||
specifier: ^14.7.1
|
|
||||||
version: 14.13.0
|
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.0.2
|
specifier: ^16.0.2
|
||||||
version: 16.3.1
|
version: 16.3.1
|
||||||
express:
|
express:
|
||||||
specifier: ^4.18.1
|
specifier: ^4.18.1
|
||||||
version: 4.18.2
|
version: 4.18.2
|
||||||
|
formidable:
|
||||||
|
specifier: ^3.5.1
|
||||||
|
version: 3.5.1
|
||||||
hono:
|
hono:
|
||||||
specifier: ^3.8.3
|
specifier: ^4.0.10
|
||||||
version: 3.8.3
|
version: 4.0.10
|
||||||
multer:
|
multer:
|
||||||
specifier: ^1.4.5-lts.1
|
specifier: ^1.4.5-lts.1
|
||||||
version: 1.4.5-lts.1
|
version: 1.4.5-lts.1
|
||||||
|
node-fetch:
|
||||||
|
specifier: ^3.3.2
|
||||||
|
version: 3.3.2
|
||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^6.9.3
|
specifier: ^6.9.3
|
||||||
version: 6.9.5
|
version: 6.9.5
|
||||||
|
@ -55,92 +61,99 @@ dependencies:
|
||||||
version: 5.2.2
|
version: 5.2.2
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@hono/vite-dev-server':
|
||||||
|
specifier: ^0.10.0
|
||||||
|
version: 0.10.0(hono@4.0.10)
|
||||||
'@sveltejs/vite-plugin-svelte':
|
'@sveltejs/vite-plugin-svelte':
|
||||||
specifier: ^2.4.6
|
specifier: ^2.4.6
|
||||||
version: 2.4.6(svelte@3.59.2)(vite@4.5.0)
|
version: 2.4.6(svelte@3.59.2)(vite@4.5.0)
|
||||||
|
'@tsconfig/svelte':
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.1
|
||||||
'@types/bytes':
|
'@types/bytes':
|
||||||
specifier: ^3.1.1
|
specifier: ^3.1.1
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
'@types/cookie-parser':
|
'@types/cookie-parser':
|
||||||
specifier: ^1.4.3
|
specifier: ^1.4.3
|
||||||
version: 1.4.4
|
version: 1.4.4
|
||||||
|
'@types/formidable':
|
||||||
|
specifier: ^3.4.5
|
||||||
|
version: 3.4.5
|
||||||
'@types/range-parser':
|
'@types/range-parser':
|
||||||
specifier: ^1.2.6
|
specifier: ^1.2.6
|
||||||
version: 1.2.6
|
version: 1.2.6
|
||||||
|
discord-api-types:
|
||||||
|
specifier: ^0.37.61
|
||||||
|
version: 0.37.71
|
||||||
sass:
|
sass:
|
||||||
specifier: ^1.57.1
|
specifier: ^1.57.1
|
||||||
version: 1.69.0
|
version: 1.69.0
|
||||||
svelte:
|
svelte:
|
||||||
specifier: ^3.55.1
|
specifier: ^3.55.1
|
||||||
version: 3.59.2
|
version: 3.59.2
|
||||||
|
svelte-preprocess:
|
||||||
|
specifier: ^5.1.3
|
||||||
|
version: 5.1.3(sass@1.69.0)(svelte@3.59.2)(typescript@5.2.2)
|
||||||
|
tslib:
|
||||||
|
specifier: ^2.6.2
|
||||||
|
version: 2.6.2
|
||||||
vite:
|
vite:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0(sass@1.69.0)
|
version: 4.5.0(sass@1.69.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
/@discordjs/builders@1.6.5:
|
/@cloudflare/workerd-darwin-64@1.20240320.1:
|
||||||
resolution: {integrity: sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==}
|
resolution: {integrity: sha512-ioG5k2M17xyiAlK/k3L21NZLMVeSHMjwlmGtZyCyzSLL5/zGINcgZ5yPLV0UuWiysw07/6Jjzm5Sx94hzMVybg==}
|
||||||
engines: {node: '>=16.11.0'}
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cloudflare/workerd-darwin-arm64@1.20240320.1:
|
||||||
|
resolution: {integrity: sha512-Ga6RDdnFEIsN4WuWsaP9bLGvK9K7pEIVoSIgmw6vweVlD8UK/a2MPGrsF1ogwdeCTCOMY8wUh9poL/Yu48IPpg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cloudflare/workerd-linux-64@1.20240320.1:
|
||||||
|
resolution: {integrity: sha512-KFof5H8eU0NXv+pUAU7Lk/OLtOmfsioTJqu0v6kPL7QsTGsgzj5sEQNcQ8DONSze549Yflu5W00qpA2cPz9eWQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cloudflare/workerd-linux-arm64@1.20240320.1:
|
||||||
|
resolution: {integrity: sha512-t+kGc6dGdkKvVMGcHCPhlCsUZF5dj8xbAFvLB7DAJ8T79ys30rmY2Lu/C8vKlhjH9TJhbzgKmPaJ0wC/K4euvw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cloudflare/workerd-windows-64@1.20240320.1:
|
||||||
|
resolution: {integrity: sha512-9xDylCOsuzWqGuANkuUByiJ5RHeMqgw37FiI7rn8I6zdGAc/alOB9B4Bh7B73WC2uEpFL+XCEjcHZ6NmsO4NaQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cspotcode/source-map-support@0.8.1:
|
||||||
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@discordjs/formatters': 0.3.2
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
'@discordjs/util': 1.0.1
|
dev: true
|
||||||
'@sapphire/shapeshift': 3.9.2
|
|
||||||
discord-api-types: 0.37.50
|
|
||||||
fast-deep-equal: 3.1.3
|
|
||||||
ts-mixer: 6.0.3
|
|
||||||
tslib: 2.6.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@discordjs/collection@1.5.3:
|
|
||||||
resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==}
|
|
||||||
engines: {node: '>=16.11.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@discordjs/formatters@0.3.2:
|
|
||||||
resolution: {integrity: sha512-lE++JZK8LSSDRM5nLjhuvWhGuKiXqu+JZ/DsOR89DVVia3z9fdCJVcHF2W/1Zxgq0re7kCzmAJlCMMX3tetKpA==}
|
|
||||||
engines: {node: '>=16.11.0'}
|
|
||||||
dependencies:
|
|
||||||
discord-api-types: 0.37.50
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@discordjs/rest@2.0.1:
|
|
||||||
resolution: {integrity: sha512-/eWAdDRvwX/rIE2tuQUmKaxmWeHmGealttIzGzlYfI4+a7y9b6ZoMp8BG/jaohs8D8iEnCNYaZiOFLVFLQb8Zg==}
|
|
||||||
engines: {node: '>=16.11.0'}
|
|
||||||
dependencies:
|
|
||||||
'@discordjs/collection': 1.5.3
|
|
||||||
'@discordjs/util': 1.0.1
|
|
||||||
'@sapphire/async-queue': 1.5.0
|
|
||||||
'@sapphire/snowflake': 3.5.1
|
|
||||||
'@vladfrangu/async_event_emitter': 2.2.2
|
|
||||||
discord-api-types: 0.37.50
|
|
||||||
magic-bytes.js: 1.5.0
|
|
||||||
tslib: 2.6.2
|
|
||||||
undici: 5.22.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@discordjs/util@1.0.1:
|
|
||||||
resolution: {integrity: sha512-d0N2yCxB8r4bn00/hvFZwM7goDcUhtViC5un4hPj73Ba4yrChLSJD8fy7Ps5jpTLg1fE9n4K0xBLc1y9WGwSsA==}
|
|
||||||
engines: {node: '>=16.11.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@discordjs/ws@1.0.1:
|
|
||||||
resolution: {integrity: sha512-avvAolBqN3yrSvdBPcJ/0j2g42ABzrv3PEL76e3YTp2WYMGH7cuspkjfSyNWaqYl1J+669dlLp+YFMxSVQyS5g==}
|
|
||||||
engines: {node: '>=16.11.0'}
|
|
||||||
dependencies:
|
|
||||||
'@discordjs/collection': 1.5.3
|
|
||||||
'@discordjs/rest': 2.0.1
|
|
||||||
'@discordjs/util': 1.0.1
|
|
||||||
'@sapphire/async-queue': 1.5.0
|
|
||||||
'@types/ws': 8.5.6
|
|
||||||
'@vladfrangu/async_event_emitter': 2.2.2
|
|
||||||
discord-api-types: 0.37.50
|
|
||||||
tslib: 2.6.2
|
|
||||||
ws: 8.14.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- utf-8-validate
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@esbuild/android-arm64@0.18.20:
|
/@esbuild/android-arm64@0.18.20:
|
||||||
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||||
|
@ -340,32 +353,46 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@hono/node-server@1.2.0:
|
/@fastify/busboy@2.1.1:
|
||||||
resolution: {integrity: sha512-aHT8lDMLpd7ioXJ1/057+h+oE/k7rCOWmjklYDsE0jE4CoNB9XzG4f8dRHvw4s5HJFocaYDiGgYM/V0kYbQ0ww==}
|
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=14'}
|
||||||
dev: false
|
dev: true
|
||||||
|
|
||||||
|
/@hono/node-server@1.8.2:
|
||||||
|
resolution: {integrity: sha512-h8l2TBLCPHZBUrrkosZ6L5CpBLj6zdESyF4B+zngiCDF7aZFQJ0alVbLx7jn8PCVi9EyoFf8a4hOZFi1tD95EA==}
|
||||||
|
engines: {node: '>=18.14.1'}
|
||||||
|
|
||||||
|
/@hono/vite-dev-server@0.10.0(hono@4.0.10):
|
||||||
|
resolution: {integrity: sha512-JWqdgH59x/PKDrwVCS5EW4eOL4fV+JOuzlKgaHk5eQUgE9vkPwyWwmf8f8rXjsXt5zxOKS3XMlf8sZeglFg3hw==}
|
||||||
|
engines: {node: '>=18.14.1'}
|
||||||
|
peerDependencies:
|
||||||
|
hono: '*'
|
||||||
|
dependencies:
|
||||||
|
'@hono/node-server': 1.8.2
|
||||||
|
hono: 4.0.10
|
||||||
|
miniflare: 3.20240320.1
|
||||||
|
minimatch: 9.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@jridgewell/resolve-uri@3.1.2:
|
||||||
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@jridgewell/sourcemap-codec@1.4.15:
|
/@jridgewell/sourcemap-codec@1.4.15:
|
||||||
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@sapphire/async-queue@1.5.0:
|
/@jridgewell/trace-mapping@0.3.9:
|
||||||
resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@sapphire/shapeshift@3.9.2:
|
|
||||||
resolution: {integrity: sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==}
|
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
lodash: 4.17.21
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
dev: false
|
dev: true
|
||||||
|
|
||||||
/@sapphire/snowflake@3.5.1:
|
|
||||||
resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==}
|
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@3.59.2)(vite@4.5.0):
|
/@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@3.59.2)(vite@4.5.0):
|
||||||
resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==}
|
resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==}
|
||||||
|
@ -403,6 +430,10 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@tsconfig/svelte@4.0.1:
|
||||||
|
resolution: {integrity: sha512-B+XlGpmuAQzJqDoBATNCvEPqQg0HkO7S8pM14QDI5NsmtymzRexQ1N+nX2H6RTtFbuFgaZD4I8AAi8voGg0GLg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/body-parser@1.19.3:
|
/@types/body-parser@1.19.3:
|
||||||
resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==}
|
resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -440,6 +471,12 @@ packages:
|
||||||
'@types/qs': 6.9.8
|
'@types/qs': 6.9.8
|
||||||
'@types/serve-static': 1.15.3
|
'@types/serve-static': 1.15.3
|
||||||
|
|
||||||
|
/@types/formidable@3.4.5:
|
||||||
|
resolution: {integrity: sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.8.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/http-errors@2.0.2:
|
/@types/http-errors@2.0.2:
|
||||||
resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==}
|
resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==}
|
||||||
|
|
||||||
|
@ -464,6 +501,10 @@ packages:
|
||||||
'@types/node': 20.8.3
|
'@types/node': 20.8.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/pug@2.0.10:
|
||||||
|
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/qs@6.9.8:
|
/@types/qs@6.9.8:
|
||||||
resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==}
|
resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==}
|
||||||
|
|
||||||
|
@ -483,17 +524,6 @@ packages:
|
||||||
'@types/mime': 3.0.2
|
'@types/mime': 3.0.2
|
||||||
'@types/node': 20.8.3
|
'@types/node': 20.8.3
|
||||||
|
|
||||||
/@types/ws@8.5.6:
|
|
||||||
resolution: {integrity: sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==}
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 20.8.3
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@vladfrangu/async_event_emitter@2.2.2:
|
|
||||||
resolution: {integrity: sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==}
|
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/accepts@1.3.8:
|
/accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -502,6 +532,17 @@ packages:
|
||||||
negotiator: 0.6.3
|
negotiator: 0.6.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/acorn-walk@8.3.2:
|
||||||
|
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/acorn@8.11.3:
|
||||||
|
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/anymatch@3.1.3:
|
/anymatch@3.1.3:
|
||||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
@ -518,6 +559,16 @@ packages:
|
||||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/as-table@1.0.55:
|
||||||
|
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||||
|
dependencies:
|
||||||
|
printable-characters: 1.0.42
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/asap@2.0.6:
|
||||||
|
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/asynckit@0.4.0:
|
/asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -531,6 +582,10 @@ packages:
|
||||||
- debug
|
- debug
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/balanced-match@1.0.2:
|
||||||
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/binary-extensions@2.2.0:
|
/binary-extensions@2.2.0:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -576,6 +631,19 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/brace-expansion@1.1.11:
|
||||||
|
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
concat-map: 0.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/brace-expansion@2.0.1:
|
||||||
|
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/braces@3.0.2:
|
/braces@3.0.2:
|
||||||
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
|
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -583,6 +651,10 @@ packages:
|
||||||
fill-range: 7.0.1
|
fill-range: 7.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/buffer-crc32@0.2.13:
|
||||||
|
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/buffer-from@1.1.2:
|
/buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -606,6 +678,15 @@ packages:
|
||||||
get-intrinsic: 1.2.1
|
get-intrinsic: 1.2.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/capnp-ts@0.7.0:
|
||||||
|
resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
|
||||||
|
dependencies:
|
||||||
|
debug: 4.3.4
|
||||||
|
tslib: 2.6.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/chokidar@3.5.3:
|
/chokidar@3.5.3:
|
||||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
@ -628,6 +709,15 @@ packages:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/commander@11.1.0:
|
||||||
|
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/concat-map@0.0.1:
|
||||||
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/concat-stream@1.6.2:
|
/concat-stream@1.6.2:
|
||||||
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
||||||
engines: {'0': node >= 0.8}
|
engines: {'0': node >= 0.8}
|
||||||
|
@ -670,12 +760,20 @@ packages:
|
||||||
/cookie@0.5.0:
|
/cookie@0.5.0:
|
||||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/core-util-is@1.0.3:
|
/core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/data-uri-to-buffer@2.0.2:
|
||||||
|
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/data-uri-to-buffer@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/debug@2.6.9:
|
/debug@2.6.9:
|
||||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -719,32 +817,21 @@ packages:
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/discord-api-types@0.37.50:
|
/detect-indent@6.1.0:
|
||||||
resolution: {integrity: sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==}
|
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/dezalgo@1.0.4:
|
||||||
|
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||||
|
dependencies:
|
||||||
|
asap: 2.0.6
|
||||||
|
wrappy: 1.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/discord.js@14.13.0:
|
/discord-api-types@0.37.71:
|
||||||
resolution: {integrity: sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==}
|
resolution: {integrity: sha512-oYDVWoiQdblr9DpwOgpi5d78dVhPcoN9YZCCqYZf2T0v9+iICs7k2bYGumoHuYMtaIitpp5aQNs+2guVkgjbOA==}
|
||||||
engines: {node: '>=16.11.0'}
|
dev: true
|
||||||
dependencies:
|
|
||||||
'@discordjs/builders': 1.6.5
|
|
||||||
'@discordjs/collection': 1.5.3
|
|
||||||
'@discordjs/formatters': 0.3.2
|
|
||||||
'@discordjs/rest': 2.0.1
|
|
||||||
'@discordjs/util': 1.0.1
|
|
||||||
'@discordjs/ws': 1.0.1
|
|
||||||
'@sapphire/snowflake': 3.5.1
|
|
||||||
'@types/ws': 8.5.6
|
|
||||||
discord-api-types: 0.37.50
|
|
||||||
fast-deep-equal: 3.1.3
|
|
||||||
lodash.snakecase: 4.1.1
|
|
||||||
tslib: 2.6.2
|
|
||||||
undici: 5.22.1
|
|
||||||
ws: 8.14.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- utf-8-validate
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/dotenv@16.3.1:
|
/dotenv@16.3.1:
|
||||||
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
|
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
|
||||||
|
@ -760,6 +847,10 @@ packages:
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/es6-promise@3.3.1:
|
||||||
|
resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/esbuild@0.18.20:
|
/esbuild@0.18.20:
|
||||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -799,6 +890,11 @@ packages:
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/exit-hook@2.2.1:
|
||||||
|
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/express@4.18.2:
|
/express@4.18.2:
|
||||||
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
@ -838,8 +934,12 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/fast-deep-equal@3.1.3:
|
/fetch-blob@3.2.0:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||||
|
engines: {node: ^12.20 || >= 14.13}
|
||||||
|
dependencies:
|
||||||
|
node-domexception: 1.0.0
|
||||||
|
web-streams-polyfill: 3.3.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/fill-range@7.0.1:
|
/fill-range@7.0.1:
|
||||||
|
@ -883,6 +983,21 @@ packages:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/formdata-polyfill@4.0.10:
|
||||||
|
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
dependencies:
|
||||||
|
fetch-blob: 3.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/formidable@3.5.1:
|
||||||
|
resolution: {integrity: sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==}
|
||||||
|
dependencies:
|
||||||
|
dezalgo: 1.0.4
|
||||||
|
hexoid: 1.0.0
|
||||||
|
once: 1.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/forwarded@0.2.0:
|
/forwarded@0.2.0:
|
||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -893,6 +1008,10 @@ packages:
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/fs.realpath@1.0.0:
|
||||||
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fsevents@2.3.3:
|
/fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
@ -914,6 +1033,13 @@ packages:
|
||||||
has-symbols: 1.0.3
|
has-symbols: 1.0.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/get-source@2.0.12:
|
||||||
|
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
|
||||||
|
dependencies:
|
||||||
|
data-uri-to-buffer: 2.0.2
|
||||||
|
source-map: 0.6.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/glob-parent@5.1.2:
|
/glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -921,6 +1047,25 @@ packages:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/glob-to-regexp@0.4.1:
|
||||||
|
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/glob@7.2.3:
|
||||||
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
|
dependencies:
|
||||||
|
fs.realpath: 1.0.0
|
||||||
|
inflight: 1.0.6
|
||||||
|
inherits: 2.0.4
|
||||||
|
minimatch: 3.1.2
|
||||||
|
once: 1.4.0
|
||||||
|
path-is-absolute: 1.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/graceful-fs@4.2.11:
|
||||||
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/has-proto@1.0.1:
|
/has-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
|
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -936,11 +1081,15 @@ packages:
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/hono@3.8.3:
|
/hexoid@1.0.0:
|
||||||
resolution: {integrity: sha512-NLJgUCKKMvijBy+V+U1FQTsNwHk2bD1KGlWJA9+qaCNWgx5St9bhfQwxrpcTGvG2Gi2naemTWCzBavDNXOqO6Q==}
|
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/hono@4.0.10:
|
||||||
|
resolution: {integrity: sha512-sq0RFAC3Ij+bkhZu90EGAQnVI1EhohRsjo9BU+BjXLbC71GSy41JjsFqCeg8MRpO2Gdu0A4MXF5licO89tn/rw==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
/http-errors@2.0.0:
|
/http-errors@2.0.0:
|
||||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
@ -963,9 +1112,15 @@ packages:
|
||||||
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
|
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/inflight@1.0.6:
|
||||||
|
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
wrappy: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/inherits@2.0.4:
|
/inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ipaddr.js@1.9.1:
|
/ipaddr.js@1.9.1:
|
||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
|
@ -1005,18 +1160,6 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/lodash.snakecase@4.1.1:
|
|
||||||
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/lodash@4.17.21:
|
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/magic-bytes.js@1.5.0:
|
|
||||||
resolution: {integrity: sha512-wJkXvutRbNWcc37tt5j1HyOK1nosspdh3dj6LUYYAvF6JYNqs53IfRvK9oEpcwiDA1NdoIi64yAMfdivPeVAyw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/magic-string@0.30.5:
|
/magic-string@0.30.5:
|
||||||
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
|
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -1056,16 +1199,55 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/min-indent@1.0.1:
|
||||||
|
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/miniflare@3.20240320.1:
|
||||||
|
resolution: {integrity: sha512-MoHhT+XaFPQtplNIkJc5NtWOi5u/7VkmBUWyyxDH7ehHk4xRT2PDkMCvVOUIcaqbHNIBzigyoYegdYmZcYtdCg==}
|
||||||
|
engines: {node: '>=16.13'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
|
acorn: 8.11.3
|
||||||
|
acorn-walk: 8.3.2
|
||||||
|
capnp-ts: 0.7.0
|
||||||
|
exit-hook: 2.2.1
|
||||||
|
glob-to-regexp: 0.4.1
|
||||||
|
stoppable: 1.1.0
|
||||||
|
undici: 5.28.3
|
||||||
|
workerd: 1.20240320.1
|
||||||
|
ws: 8.16.0
|
||||||
|
youch: 3.3.3
|
||||||
|
zod: 3.22.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/minimatch@3.1.2:
|
||||||
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 1.1.11
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/minimatch@9.0.4:
|
||||||
|
resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 2.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/minimist@1.2.8:
|
/minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mkdirp@0.5.6:
|
/mkdirp@0.5.6:
|
||||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ms@2.0.0:
|
/ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
@ -1092,6 +1274,11 @@ packages:
|
||||||
xtend: 4.0.2
|
xtend: 4.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/mustache@4.2.0:
|
||||||
|
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/nanoid@3.3.6:
|
/nanoid@3.3.6:
|
||||||
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
@ -1103,6 +1290,20 @@ packages:
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/node-domexception@1.0.0:
|
||||||
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
|
engines: {node: '>=10.5.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/node-fetch@3.3.2:
|
||||||
|
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
dependencies:
|
||||||
|
data-uri-to-buffer: 4.0.1
|
||||||
|
fetch-blob: 3.2.0
|
||||||
|
formdata-polyfill: 4.0.10
|
||||||
|
dev: false
|
||||||
|
|
||||||
/nodemailer@6.9.5:
|
/nodemailer@6.9.5:
|
||||||
resolution: {integrity: sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==}
|
resolution: {integrity: sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
@ -1129,11 +1330,21 @@ packages:
|
||||||
ee-first: 1.1.1
|
ee-first: 1.1.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/once@1.4.0:
|
||||||
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
dependencies:
|
||||||
|
wrappy: 1.0.2
|
||||||
|
|
||||||
/parseurl@1.3.3:
|
/parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/path-is-absolute@1.0.1:
|
||||||
|
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/path-to-regexp@0.1.7:
|
/path-to-regexp@0.1.7:
|
||||||
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -1156,6 +1367,10 @@ packages:
|
||||||
source-map-js: 1.0.2
|
source-map-js: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/printable-characters@1.0.42:
|
||||||
|
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/process-nextick-args@2.0.1:
|
/process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -1219,6 +1434,13 @@ packages:
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/rimraf@2.7.1:
|
||||||
|
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
glob: 7.2.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/rollup@3.29.4:
|
/rollup@3.29.4:
|
||||||
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
|
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
|
||||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||||
|
@ -1239,6 +1461,15 @@ packages:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/sander@0.5.1:
|
||||||
|
resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
|
||||||
|
dependencies:
|
||||||
|
es6-promise: 3.3.1
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
mkdirp: 0.5.6
|
||||||
|
rimraf: 2.7.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sass@1.69.0:
|
/sass@1.69.0:
|
||||||
resolution: {integrity: sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==}
|
resolution: {integrity: sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
@ -1294,16 +1525,43 @@ packages:
|
||||||
object-inspect: 1.12.3
|
object-inspect: 1.12.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/sorcery@0.11.0:
|
||||||
|
resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
buffer-crc32: 0.2.13
|
||||||
|
minimist: 1.2.8
|
||||||
|
sander: 0.5.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/source-map-js@1.0.2:
|
/source-map-js@1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/source-map@0.6.1:
|
||||||
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/stacktracey@2.1.8:
|
||||||
|
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
|
||||||
|
dependencies:
|
||||||
|
as-table: 1.0.55
|
||||||
|
get-source: 2.0.12
|
||||||
|
dev: true
|
||||||
|
|
||||||
/statuses@2.0.1:
|
/statuses@2.0.1:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/stoppable@1.1.0:
|
||||||
|
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
|
||||||
|
engines: {node: '>=4', npm: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/streamsearch@1.1.0:
|
/streamsearch@1.1.0:
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
@ -1315,6 +1573,13 @@ packages:
|
||||||
safe-buffer: 5.1.2
|
safe-buffer: 5.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/strip-indent@3.0.0:
|
||||||
|
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
min-indent: 1.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/svelte-hmr@0.15.3(svelte@3.59.2):
|
/svelte-hmr@0.15.3(svelte@3.59.2):
|
||||||
resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
|
resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
|
||||||
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
||||||
|
@ -1324,6 +1589,54 @@ packages:
|
||||||
svelte: 3.59.2
|
svelte: 3.59.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/svelte-preprocess@5.1.3(sass@1.69.0)(svelte@3.59.2)(typescript@5.2.2):
|
||||||
|
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
|
||||||
|
engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
|
||||||
|
requiresBuild: true
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.10.2
|
||||||
|
coffeescript: ^2.5.1
|
||||||
|
less: ^3.11.3 || ^4.0.0
|
||||||
|
postcss: ^7 || ^8
|
||||||
|
postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
|
||||||
|
pug: ^3.0.0
|
||||||
|
sass: ^1.26.8
|
||||||
|
stylus: ^0.55.0
|
||||||
|
sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
|
||||||
|
svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0
|
||||||
|
typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@babel/core':
|
||||||
|
optional: true
|
||||||
|
coffeescript:
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
postcss:
|
||||||
|
optional: true
|
||||||
|
postcss-load-config:
|
||||||
|
optional: true
|
||||||
|
pug:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/pug': 2.0.10
|
||||||
|
detect-indent: 6.1.0
|
||||||
|
magic-string: 0.30.5
|
||||||
|
sass: 1.69.0
|
||||||
|
sorcery: 0.11.0
|
||||||
|
strip-indent: 3.0.0
|
||||||
|
svelte: 3.59.2
|
||||||
|
typescript: 5.2.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/svelte@3.59.2:
|
/svelte@3.59.2:
|
||||||
resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==}
|
resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
@ -1341,13 +1654,9 @@ packages:
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/ts-mixer@6.0.3:
|
|
||||||
resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tslib@2.6.2:
|
/tslib@2.6.2:
|
||||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
dev: false
|
dev: true
|
||||||
|
|
||||||
/type-is@1.6.18:
|
/type-is@1.6.18:
|
||||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||||
|
@ -1365,14 +1674,13 @@ packages:
|
||||||
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
|
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
|
||||||
|
|
||||||
/undici@5.22.1:
|
/undici@5.28.3:
|
||||||
resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==}
|
resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==}
|
||||||
engines: {node: '>=14.0'}
|
engines: {node: '>=14.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
busboy: 1.6.0
|
'@fastify/busboy': 2.1.1
|
||||||
dev: false
|
dev: true
|
||||||
|
|
||||||
/unpipe@1.0.0:
|
/unpipe@1.0.0:
|
||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
|
@ -1440,8 +1748,29 @@ packages:
|
||||||
vite: 4.5.0(sass@1.69.0)
|
vite: 4.5.0(sass@1.69.0)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ws@8.14.2:
|
/web-streams-polyfill@3.3.3:
|
||||||
resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==}
|
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/workerd@1.20240320.1:
|
||||||
|
resolution: {integrity: sha512-nuavAGGjh0qqM6RF5zxTHyUwEqdLCHchodbrpbh/xlJpFGnJVY5C1YgSi2S9aLkJJoa0/25Ta/+EzXEbApA/3w==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
hasBin: true
|
||||||
|
requiresBuild: true
|
||||||
|
optionalDependencies:
|
||||||
|
'@cloudflare/workerd-darwin-64': 1.20240320.1
|
||||||
|
'@cloudflare/workerd-darwin-arm64': 1.20240320.1
|
||||||
|
'@cloudflare/workerd-linux-64': 1.20240320.1
|
||||||
|
'@cloudflare/workerd-linux-arm64': 1.20240320.1
|
||||||
|
'@cloudflare/workerd-windows-64': 1.20240320.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/wrappy@1.0.2:
|
||||||
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
/ws@8.16.0:
|
||||||
|
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
|
@ -1451,9 +1780,21 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
dev: false
|
dev: true
|
||||||
|
|
||||||
/xtend@4.0.2:
|
/xtend@4.0.2:
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/youch@3.3.3:
|
||||||
|
resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==}
|
||||||
|
dependencies:
|
||||||
|
cookie: 0.5.0
|
||||||
|
mustache: 4.2.0
|
||||||
|
stacktracey: 2.1.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/zod@3.22.4:
|
||||||
|
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||||
|
dev: true
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
import * as Accounts from "../../lib/accounts.js"
|
||||||
import * as auth from "../../../lib/auth.js"
|
import * as auth from "../../lib/auth.js"
|
||||||
import { writeFile } from "fs/promises"
|
import { writeFile } from "fs/promises"
|
||||||
import { sendMail } from "../../../lib/mail.js"
|
import { sendMail } from "../../lib/mail.js"
|
||||||
import {
|
import {
|
||||||
getAccount,
|
getAccount,
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
requiresAdmin,
|
requiresAdmin,
|
||||||
requiresPermissions,
|
requiresPermissions,
|
||||||
} from "../../../lib/middleware.js"
|
} from "../../lib/middleware.js"
|
||||||
import Files from "../../../lib/files.js"
|
import Files from "../../lib/files.js"
|
||||||
|
|
||||||
export let adminRoutes = new Hono<{
|
export let adminRoutes = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
|
@ -1,24 +1,24 @@
|
||||||
import { Hono, Handler } from "hono"
|
import { Hono, Handler } from "hono"
|
||||||
import { getCookie, setCookie } from "hono/cookie"
|
import { getCookie, setCookie } from "hono/cookie"
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
import * as Accounts from "../../lib/accounts.js"
|
||||||
import * as auth from "../../../lib/auth.js"
|
import * as auth from "../../lib/auth.js"
|
||||||
import { sendMail } from "../../../lib/mail.js"
|
import { sendMail } from "../../lib/mail.js"
|
||||||
import {
|
import {
|
||||||
getAccount,
|
getAccount,
|
||||||
noAPIAccess,
|
noAPIAccess,
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
requiresPermissions,
|
requiresPermissions,
|
||||||
} from "../../../lib/middleware.js"
|
} from "../../lib/middleware.js"
|
||||||
import { accountRatelimit } from "../../../lib/ratelimit.js"
|
import { accountRatelimit } from "../../lib/ratelimit.js"
|
||||||
|
|
||||||
import ServeError from "../../../lib/errors.js"
|
import ServeError from "../../lib/errors.js"
|
||||||
import Files, {
|
import Files, {
|
||||||
FileVisibility,
|
FileVisibility,
|
||||||
generateFileId,
|
generateFileId,
|
||||||
id_check_regex,
|
id_check_regex,
|
||||||
} from "../../../lib/files.js"
|
} from "../../lib/files.js"
|
||||||
|
|
||||||
import { writeFile } from "fs/promises"
|
import { writeFile, readFile } from "fs/promises"
|
||||||
|
|
||||||
export let authRoutes = new Hono<{
|
export let authRoutes = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
||||||
|
@ -26,50 +26,16 @@ export let authRoutes = new Hono<{
|
||||||
}
|
}
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
import config from "../../../../../config.json" assert {type:"json"}
|
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
|
||||||
|
const config = JSON.parse(
|
||||||
|
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
|
||||||
|
// Also if this were NixOS you can't modify the built folder after build
|
||||||
|
await readFile(process.cwd() + "/config.json", "utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
authRoutes.all("*", getAccount)
|
authRoutes.all("*", getAccount)
|
||||||
|
|
||||||
export default function (files: Files) {
|
export default function (files: Files) {
|
||||||
authRoutes.post("/login", async (ctx) => {
|
|
||||||
const body = await ctx.req.json()
|
|
||||||
if (
|
|
||||||
typeof body.username != "string" ||
|
|
||||||
typeof body.password != "string"
|
|
||||||
) {
|
|
||||||
return ServeError(ctx, 400, "please provide a username or password")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.validate(getCookie(ctx, "auth")!))
|
|
||||||
return ctx.text("You are already authed")
|
|
||||||
|
|
||||||
/*
|
|
||||||
check if account exists
|
|
||||||
*/
|
|
||||||
|
|
||||||
let acc = Accounts.getFromUsername(body.username)
|
|
||||||
|
|
||||||
if (!acc) {
|
|
||||||
return ServeError(ctx, 401, "username or password incorrect")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Accounts.password.check(acc.id, body.password)) {
|
|
||||||
return ServeError(ctx, 401, "username or password incorrect")
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
assign token
|
|
||||||
*/
|
|
||||||
|
|
||||||
setCookie(ctx, "auth", auth.create(acc.id, 3 * 24 * 60 * 60 * 1000), {
|
|
||||||
path: "/",
|
|
||||||
sameSite: "Strict",
|
|
||||||
secure: true,
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 3 * 24 * 60 * 60 * 1000,
|
|
||||||
})
|
|
||||||
return ctx.text("")
|
|
||||||
})
|
|
||||||
|
|
||||||
authRoutes.post("/create", async (ctx) => {
|
authRoutes.post("/create", async (ctx) => {
|
||||||
if (!config.accounts.registrationEnabled) {
|
if (!config.accounts.registrationEnabled) {
|
||||||
return ServeError(ctx, 403, "account registration disabled")
|
return ServeError(ctx, 403, "account registration disabled")
|
|
@ -1,12 +1,12 @@
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
import * as Accounts from "../../lib/accounts.js"
|
||||||
import { writeFile } from "fs/promises"
|
import { writeFile } from "fs/promises"
|
||||||
import Files from "../../../lib/files.js"
|
import Files from "../../lib/files.js"
|
||||||
import {
|
import {
|
||||||
getAccount,
|
getAccount,
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
requiresPermissions,
|
requiresPermissions,
|
||||||
} from "../../../lib/middleware.js"
|
} from "../../lib/middleware.js"
|
||||||
|
|
||||||
export let fileApiRoutes = new Hono<{
|
export let fileApiRoutes = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
15
src/api/v0/index.ts
Normal file
15
src/api/v0/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import type Files from "../../lib/files.js"
|
||||||
|
import primaryApi from "./primaryApi.js"
|
||||||
|
import adminRoutes from "./adminRoutes.js"
|
||||||
|
import authRoutes from "./authRoutes.js"
|
||||||
|
import fileApiRoutes from "./fileApiRoutes.js"
|
||||||
|
|
||||||
|
export default function install(root: Hono, files: Files) {
|
||||||
|
const router = new Hono()
|
||||||
|
router.route("/", primaryApi(files, root))
|
||||||
|
router.route("/admin", adminRoutes(files))
|
||||||
|
router.route("/auth", authRoutes(files))
|
||||||
|
router.route("/files", fileApiRoutes(files))
|
||||||
|
root.route("/", router)
|
||||||
|
}
|
47
src/api/v0/primaryApi.ts
Normal file
47
src/api/v0/primaryApi.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import * as Accounts from "../../lib/accounts.js"
|
||||||
|
import * as auth from "../../lib/auth.js"
|
||||||
|
import RangeParser, { type Range } from "range-parser"
|
||||||
|
import ServeError from "../../lib/errors.js"
|
||||||
|
import Files, { WebError } from "../../lib/files.js"
|
||||||
|
import { getAccount, requiresPermissions } from "../../lib/middleware.js"
|
||||||
|
import { Readable } from "node:stream"
|
||||||
|
import type { ReadableStream as StreamWebReadable } from "node:stream/web"
|
||||||
|
import formidable from "formidable"
|
||||||
|
import { HttpBindings } from "@hono/node-server"
|
||||||
|
import { type StatusCode } from "hono/utils/http-status"
|
||||||
|
export let primaryApi = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
account: Accounts.Account
|
||||||
|
}
|
||||||
|
Bindings: HttpBindings
|
||||||
|
}>()
|
||||||
|
|
||||||
|
primaryApi.all("*", getAccount)
|
||||||
|
|
||||||
|
export default function (files: Files, apiRoot: Hono) {
|
||||||
|
primaryApi.get("/file/:fileId", async (ctx) =>
|
||||||
|
apiRoot.fetch(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`/api/v1/file/${ctx.req.param("fileId")}`,
|
||||||
|
ctx.req.raw.url
|
||||||
|
).href,
|
||||||
|
ctx.req.raw
|
||||||
|
),
|
||||||
|
ctx.env
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
primaryApi.post("/upload", async (ctx) =>
|
||||||
|
apiRoot.fetch(
|
||||||
|
new Request(new URL(`/api/v1/file`, ctx.req.raw.url).href, {
|
||||||
|
...ctx.req.raw,
|
||||||
|
method: "PUT",
|
||||||
|
}),
|
||||||
|
ctx.env
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return primaryApi
|
||||||
|
}
|
462
src/api/v1/account.ts
Normal file
462
src/api/v1/account.ts
Normal file
|
@ -0,0 +1,462 @@
|
||||||
|
// Modules
|
||||||
|
|
||||||
|
import { type Context, Hono } from "hono"
|
||||||
|
import { getCookie, setCookie } from "hono/cookie"
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
|
||||||
|
import Files, { id_check_regex } from "../../lib/files.js"
|
||||||
|
import * as Accounts from "../../lib/accounts.js"
|
||||||
|
import * as auth from "../../lib/auth.js"
|
||||||
|
import {
|
||||||
|
assertAPI,
|
||||||
|
getAccount,
|
||||||
|
login,
|
||||||
|
noAPIAccess,
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions,
|
||||||
|
} from "../../lib/middleware.js"
|
||||||
|
import ServeError from "../../lib/errors.js"
|
||||||
|
import { CodeMgr, sendMail } from "../../lib/mail.js"
|
||||||
|
import { readFile } from "fs/promises"
|
||||||
|
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
|
||||||
|
const config = JSON.parse(
|
||||||
|
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
|
||||||
|
// Also if this were NixOS you can't modify the built folder after build
|
||||||
|
await readFile(process.cwd() + "/config.json", "utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
account: Accounts.Account
|
||||||
|
target: Accounts.Account
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type UserUpdateParameters = Partial<
|
||||||
|
Omit<Accounts.Account, "password"> & {
|
||||||
|
password: string
|
||||||
|
currentPassword?: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
type Message = [200 | 400 | 401 | 403 | 429 | 501, string]
|
||||||
|
|
||||||
|
// there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
|
||||||
|
// @Jack5079 make typings better if possible
|
||||||
|
|
||||||
|
type Validator<
|
||||||
|
T extends keyof Partial<Accounts.Account>,
|
||||||
|
ValueNotNull extends boolean
|
||||||
|
> =
|
||||||
|
/**
|
||||||
|
* @param actor The account performing this action
|
||||||
|
* @param target The target account for this action
|
||||||
|
* @param params Changes being patched in by the user
|
||||||
|
*/
|
||||||
|
(
|
||||||
|
actor: Accounts.Account,
|
||||||
|
target: Accounts.Account,
|
||||||
|
params: UserUpdateParameters &
|
||||||
|
(ValueNotNull extends true
|
||||||
|
? {
|
||||||
|
[K in keyof Pick<
|
||||||
|
UserUpdateParameters,
|
||||||
|
T
|
||||||
|
>]-?: UserUpdateParameters[K]
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
ctx: Context
|
||||||
|
) => Accounts.Account[T] | Message
|
||||||
|
|
||||||
|
// this type is so stupid stg
|
||||||
|
type ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> =
|
||||||
|
| {
|
||||||
|
acceptsNull: true
|
||||||
|
validator: Validator<T, false>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
acceptsNull?: false
|
||||||
|
validator: Validator<T, true>
|
||||||
|
}
|
||||||
|
|
||||||
|
const validators: {
|
||||||
|
[T in keyof Partial<Accounts.Account>]:
|
||||||
|
| Validator<T, true>
|
||||||
|
| ValidatorWithSettings<T>
|
||||||
|
} = {
|
||||||
|
defaultFileVisibility(actor, target, params) {
|
||||||
|
if (
|
||||||
|
["public", "private", "anonymous"].includes(
|
||||||
|
params.defaultFileVisibility
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return params.defaultFileVisibility
|
||||||
|
else return [400, "invalid file visibility"]
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
acceptsNull: true,
|
||||||
|
validator: (actor, target, params, ctx) => {
|
||||||
|
if (
|
||||||
|
!params.currentPassword || // actor on purpose here to allow admins
|
||||||
|
(params.currentPassword &&
|
||||||
|
Accounts.password.check(actor.id, params.currentPassword))
|
||||||
|
)
|
||||||
|
return [401, "current password incorrect"]
|
||||||
|
|
||||||
|
if (!params.email) {
|
||||||
|
if (target.email) {
|
||||||
|
sendMail(
|
||||||
|
target.email,
|
||||||
|
`Email disconnected`,
|
||||||
|
`<b>Hello there!</b> Your email address (<span code>${target.email}</span>) has been disconnected from the monofile account <span username>${target.username}</span>. Thank you for using monofile.`
|
||||||
|
).catch()
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params.email !== "string")
|
||||||
|
return [400, "email must be string"]
|
||||||
|
if (actor.admin) return params.email
|
||||||
|
|
||||||
|
// send verification email
|
||||||
|
|
||||||
|
if (
|
||||||
|
(CodeMgr.codes.verifyEmail.byUser.get(target.id)?.length ||
|
||||||
|
0) >= 2
|
||||||
|
)
|
||||||
|
return [429, "you have too many active codes"]
|
||||||
|
|
||||||
|
let code = new CodeMgr.Code("verifyEmail", target.id, params.email)
|
||||||
|
|
||||||
|
sendMail(
|
||||||
|
params.email,
|
||||||
|
`Hey there, ${target.username} - let's connect your email`,
|
||||||
|
`<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${
|
||||||
|
params.email.split("@")[0]
|
||||||
|
}<span style="opacity:0.5">@${
|
||||||
|
params.email.split("@")[1]
|
||||||
|
}</span></span>, to your account, <span username>${
|
||||||
|
target.username
|
||||||
|
}</span>. If you would like to continue, please <a href="https://${ctx.req.header(
|
||||||
|
"Host"
|
||||||
|
)}/go/verify/${
|
||||||
|
code.id
|
||||||
|
}"><span code>click here</span></a>, or go to https://${ctx.req.header(
|
||||||
|
"Host"
|
||||||
|
)}/go/verify/${code.id}.`
|
||||||
|
)
|
||||||
|
|
||||||
|
return [200, "please check your inbox"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
password(actor, target, params) {
|
||||||
|
if (
|
||||||
|
!params.currentPassword || // actor on purpose here to allow admins
|
||||||
|
(params.currentPassword &&
|
||||||
|
Accounts.password.check(actor.id, params.currentPassword))
|
||||||
|
)
|
||||||
|
return [401, "current password incorrect"]
|
||||||
|
|
||||||
|
if (typeof params.password != "string" || params.password.length < 8)
|
||||||
|
return [400, "password must be 8 characters or longer"]
|
||||||
|
|
||||||
|
if (target.email) {
|
||||||
|
sendMail(
|
||||||
|
target.email,
|
||||||
|
`Your login details have been updated`,
|
||||||
|
`<b>Hello there!</b> Your password on your account, <span username>${target.username}</span>, has been updated` +
|
||||||
|
`${
|
||||||
|
actor != target
|
||||||
|
? ` by <span username>${actor.username}</span>`
|
||||||
|
: ""
|
||||||
|
}. ` +
|
||||||
|
`Please update your saved login details accordingly.`
|
||||||
|
).catch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Accounts.password.hash(params.password)
|
||||||
|
},
|
||||||
|
username(actor, target, params) {
|
||||||
|
if (
|
||||||
|
!params.currentPassword || // actor on purpose here to allow admins
|
||||||
|
(params.currentPassword &&
|
||||||
|
Accounts.password.check(actor.id, params.currentPassword))
|
||||||
|
)
|
||||||
|
return [401, "current password incorrect"]
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof params.username != "string" ||
|
||||||
|
params.username.length < 3 ||
|
||||||
|
params.username.length > 20
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
400,
|
||||||
|
"username must be between 3 and 20 characters in length",
|
||||||
|
]
|
||||||
|
|
||||||
|
if (Accounts.getFromUsername(params.username))
|
||||||
|
return [400, "account with this username already exists"]
|
||||||
|
|
||||||
|
if (
|
||||||
|
(params.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] !=
|
||||||
|
params.username
|
||||||
|
)
|
||||||
|
return [400, "username has invalid characters"]
|
||||||
|
|
||||||
|
if (target.email) {
|
||||||
|
sendMail(
|
||||||
|
target.email,
|
||||||
|
`Your login details have been updated`,
|
||||||
|
`<b>Hello there!</b> Your username on your account, <span username>${target.username}</span>, has been updated` +
|
||||||
|
`${
|
||||||
|
actor != target
|
||||||
|
? ` by <span username>${actor.username}</span>`
|
||||||
|
: ""
|
||||||
|
} to <span username>${params.username}</span>. ` +
|
||||||
|
`Please update your saved login details accordingly.`
|
||||||
|
).catch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.username
|
||||||
|
},
|
||||||
|
customCSS: {
|
||||||
|
acceptsNull: true,
|
||||||
|
validator: (actor, target, params) => {
|
||||||
|
if (
|
||||||
|
!params.customCSS ||
|
||||||
|
(params.customCSS.match(id_check_regex)?.[0] ==
|
||||||
|
params.customCSS &&
|
||||||
|
params.customCSS.length <= config.maxUploadIdLength)
|
||||||
|
)
|
||||||
|
return params.customCSS
|
||||||
|
else return [400, "bad file id"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
embed(actor, target, params) {
|
||||||
|
if (typeof params.embed !== "object")
|
||||||
|
return [400, "must use an object for embed"]
|
||||||
|
if (params.embed.color === undefined) {
|
||||||
|
params.embed.color = target.embed?.color
|
||||||
|
} else if (
|
||||||
|
!(
|
||||||
|
(params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
|
||||||
|
params.embed.color.toLowerCase() &&
|
||||||
|
params.embed.color.length == 6) ||
|
||||||
|
params.embed.color == null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return [400, "bad embed color"]
|
||||||
|
|
||||||
|
if (params.embed.largeImage === undefined) {
|
||||||
|
params.embed.largeImage = target.embed?.largeImage
|
||||||
|
} else params.embed.largeImage = Boolean(params.embed.largeImage)
|
||||||
|
|
||||||
|
return params.embed
|
||||||
|
},
|
||||||
|
admin(actor, target, params) {
|
||||||
|
if (actor.admin && !target.admin) return params.admin
|
||||||
|
else if (!actor.admin) return [400, "cannot promote yourself"]
|
||||||
|
else return [400, "cannot demote an admin"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
router.use(getAccount)
|
||||||
|
router.all("/:user", async (ctx, next) => {
|
||||||
|
let acc =
|
||||||
|
ctx.req.param("user") == "me"
|
||||||
|
? ctx.get("account")
|
||||||
|
: ctx.req.param("user").startsWith("@")
|
||||||
|
? Accounts.getFromUsername(ctx.req.param("user").slice(1))
|
||||||
|
: Accounts.getFromId(ctx.req.param("user"))
|
||||||
|
if (acc != ctx.get("account") && !ctx.get("account")?.admin)
|
||||||
|
return ServeError(ctx, 403, "you cannot manage this user")
|
||||||
|
if (!acc) return ServeError(ctx, 404, "account does not exist")
|
||||||
|
|
||||||
|
ctx.set("target", acc)
|
||||||
|
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
|
||||||
|
function isMessage(object: any): object is Message {
|
||||||
|
return (
|
||||||
|
Array.isArray(object) &&
|
||||||
|
object.length == 2 &&
|
||||||
|
typeof object[0] == "number" &&
|
||||||
|
typeof object[1] == "string"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (files: Files) {
|
||||||
|
router.post("/", async (ctx) => {
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (!config.accounts.registrationEnabled) {
|
||||||
|
return ServeError(ctx, 403, "account registration disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.validate(getCookie(ctx, "auth")!)) {
|
||||||
|
return ServeError(ctx, 400, "you are already logged in")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Accounts.getFromUsername(body.username)) {
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"account with this username already exists"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.username.length < 3 || body.username.length > 20) {
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
||||||
|
) {
|
||||||
|
return ServeError(ctx, 400, "username contains invalid characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.password.length < 8) {
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"password must be 8 characters or longer"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Accounts.create(body.username, body.password)
|
||||||
|
.then((account) => {
|
||||||
|
login(ctx, account)
|
||||||
|
return ctx.text("logged in")
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return ServeError(ctx, 500, "internal server error")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/:user",
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions("manage"),
|
||||||
|
async (ctx) => {
|
||||||
|
const body = (await ctx.req.json()) as UserUpdateParameters
|
||||||
|
const actor = ctx.get("account")!
|
||||||
|
const target = ctx.get("target")!
|
||||||
|
if (Array.isArray(body)) return ServeError(ctx, 400, "invalid body")
|
||||||
|
|
||||||
|
let results: (
|
||||||
|
| [
|
||||||
|
keyof Accounts.Account,
|
||||||
|
Accounts.Account[keyof Accounts.Account]
|
||||||
|
]
|
||||||
|
| Message
|
||||||
|
)[] = (
|
||||||
|
Object.entries(body).filter(
|
||||||
|
(e) => e[0] !== "currentPassword"
|
||||||
|
) as [
|
||||||
|
keyof Accounts.Account,
|
||||||
|
UserUpdateParameters[keyof Accounts.Account]
|
||||||
|
][]
|
||||||
|
).map(([x, v]) => {
|
||||||
|
if (!validators[x])
|
||||||
|
return [
|
||||||
|
400,
|
||||||
|
`the ${x} parameter cannot be set or is not a valid parameter`,
|
||||||
|
] as Message
|
||||||
|
|
||||||
|
let validator = (
|
||||||
|
typeof validators[x] == "object"
|
||||||
|
? validators[x]
|
||||||
|
: {
|
||||||
|
validator: validators[x] as Validator<
|
||||||
|
typeof x,
|
||||||
|
false
|
||||||
|
>,
|
||||||
|
acceptsNull: false,
|
||||||
|
}
|
||||||
|
) as ValidatorWithSettings<typeof x>
|
||||||
|
|
||||||
|
if (!validator.acceptsNull && !v)
|
||||||
|
return [
|
||||||
|
400,
|
||||||
|
`the ${x} validator does not accept null values`,
|
||||||
|
] as Message
|
||||||
|
|
||||||
|
return [
|
||||||
|
x,
|
||||||
|
validator.validator(actor, target, body as any, ctx),
|
||||||
|
] as [
|
||||||
|
keyof Accounts.Account,
|
||||||
|
Accounts.Account[keyof Accounts.Account]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
let allMsgs = results.map((v) => {
|
||||||
|
if (isMessage(v)) return v
|
||||||
|
target[v[0]] = v[1] as never // lol
|
||||||
|
return [200, "OK"] as Message
|
||||||
|
})
|
||||||
|
|
||||||
|
await Accounts.save()
|
||||||
|
|
||||||
|
if (allMsgs.length == 1)
|
||||||
|
return ctx.text(
|
||||||
|
...(allMsgs[0]!.reverse() as [Message[1], Message[0]])
|
||||||
|
) // im sorry
|
||||||
|
else return ctx.json(allMsgs)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
router.delete("/:user", requiresAccount, noAPIAccess, async (ctx) => {
|
||||||
|
let acc = ctx.get("target")
|
||||||
|
|
||||||
|
auth.AuthTokens.filter((e) => e.account == acc?.id).forEach((token) => {
|
||||||
|
auth.invalidate(token.token)
|
||||||
|
})
|
||||||
|
|
||||||
|
await Accounts.deleteAccount(acc.id)
|
||||||
|
|
||||||
|
if (acc.email) {
|
||||||
|
await sendMail(
|
||||||
|
acc.email,
|
||||||
|
"Notice of account deletion",
|
||||||
|
`Your account, <span username>${acc.username}</span>, has been removed. Thank you for using monofile.`
|
||||||
|
).catch()
|
||||||
|
return ctx.text("OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.text("account deleted")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get("/:user", requiresAccount, async (ctx) => {
|
||||||
|
let acc = ctx.get("target")
|
||||||
|
let sessionToken = auth.tokenFor(ctx)!
|
||||||
|
|
||||||
|
return ctx.json({
|
||||||
|
...acc,
|
||||||
|
password: undefined,
|
||||||
|
email:
|
||||||
|
auth.getType(sessionToken) == "User" ||
|
||||||
|
auth.getPermissions(sessionToken)?.includes("email")
|
||||||
|
? acc.email
|
||||||
|
: undefined,
|
||||||
|
activeSessions: auth.AuthTokens.filter(
|
||||||
|
(e) =>
|
||||||
|
e.type != "App" &&
|
||||||
|
e.account == acc.id &&
|
||||||
|
(e.expire > Date.now() || !e.expire)
|
||||||
|
).length,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get("/css", async (ctx) => {
|
||||||
|
let acc = ctx.get("account")
|
||||||
|
if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`)
|
||||||
|
else return ctx.text("")
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
186
src/api/v1/file/index.ts
Normal file
186
src/api/v1/file/index.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import * as Accounts from "../../../lib/accounts.js"
|
||||||
|
import * as auth from "../../../lib/auth.js"
|
||||||
|
import RangeParser, { type Range } from "range-parser"
|
||||||
|
import ServeError from "../../../lib/errors.js"
|
||||||
|
import Files, { WebError } from "../../../lib/files.js"
|
||||||
|
import { getAccount, requiresPermissions } from "../../../lib/middleware.js"
|
||||||
|
import { Readable } from "node:stream"
|
||||||
|
import type { ReadableStream as StreamWebReadable } from "node:stream/web"
|
||||||
|
import formidable from "formidable"
|
||||||
|
import { HttpBindings } from "@hono/node-server"
|
||||||
|
import { type StatusCode } from "hono/utils/http-status"
|
||||||
|
|
||||||
|
declare const MONOFILE_VERSION: string // see vite.config.js
|
||||||
|
|
||||||
|
const router = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
account: Accounts.Account
|
||||||
|
}
|
||||||
|
Bindings: HttpBindings
|
||||||
|
}>()
|
||||||
|
router.all("*", getAccount)
|
||||||
|
|
||||||
|
export default function (files: Files) {
|
||||||
|
router.on(["PUT", "POST"], "/", requiresPermissions("upload"), (ctx) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
ctx.env.incoming.removeAllListeners("data") // remove hono's buffering
|
||||||
|
|
||||||
|
let errEscalated = false
|
||||||
|
function escalate(err: Error) {
|
||||||
|
if (errEscalated) return
|
||||||
|
errEscalated = true
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
if ("httpCode" in err) ctx.status(err.httpCode as StatusCode)
|
||||||
|
else if (err instanceof WebError)
|
||||||
|
ctx.status(err.statusCode as StatusCode)
|
||||||
|
else ctx.status(400)
|
||||||
|
resolve(ctx.body(err.message))
|
||||||
|
}
|
||||||
|
|
||||||
|
let acc = ctx.get("account") as Accounts.Account | undefined
|
||||||
|
|
||||||
|
if (
|
||||||
|
!ctx.req
|
||||||
|
.header("Content-Type")
|
||||||
|
?.startsWith("multipart/form-data")
|
||||||
|
)
|
||||||
|
return resolve(ctx.body("must be multipart/form-data", 400))
|
||||||
|
|
||||||
|
if (!ctx.req.raw.body)
|
||||||
|
return resolve(ctx.body("body must be supplied", 400))
|
||||||
|
|
||||||
|
let file = files.createWriteStream(acc?.id)
|
||||||
|
|
||||||
|
file.on("error", escalate).on("finish", async () => {
|
||||||
|
if (!ctx.env.incoming.readableEnded)
|
||||||
|
await new Promise((res) =>
|
||||||
|
ctx.env.incoming.once("end", res)
|
||||||
|
)
|
||||||
|
file.commit()
|
||||||
|
.then((id) => resolve(ctx.body(id!)))
|
||||||
|
.catch(escalate)
|
||||||
|
})
|
||||||
|
|
||||||
|
let parser = formidable({
|
||||||
|
maxFieldsSize: 65536,
|
||||||
|
maxFileSize:
|
||||||
|
files.config.maxDiscordFileSize *
|
||||||
|
files.config.maxDiscordFiles,
|
||||||
|
maxFiles: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
let acceptNewData = true
|
||||||
|
|
||||||
|
parser.onPart = function (part) {
|
||||||
|
if (!part.originalFilename || !part.mimetype) {
|
||||||
|
parser._handlePart(part)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// lol
|
||||||
|
if (part.name == "file") {
|
||||||
|
if (!acceptNewData || file.writableEnded)
|
||||||
|
return part.emit(
|
||||||
|
"error",
|
||||||
|
new WebError(
|
||||||
|
400,
|
||||||
|
"cannot set file after previously setting up another upload"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
acceptNewData = false
|
||||||
|
file.setName(part.originalFilename || "")
|
||||||
|
file.setType(part.mimetype || "")
|
||||||
|
|
||||||
|
file.on("drain", () => ctx.env.incoming.resume())
|
||||||
|
file.on("error", (err) => part.emit("error", err))
|
||||||
|
|
||||||
|
part.on("data", (data: Buffer) => {
|
||||||
|
if (!file.write(data)) ctx.env.incoming.pause()
|
||||||
|
})
|
||||||
|
part.on("end", () => file.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.on("field", async (k, v) => {
|
||||||
|
if (k == "uploadId") {
|
||||||
|
if (files.files[v] && ctx.req.method == "POST")
|
||||||
|
return file.destroy(
|
||||||
|
new WebError(409, "file already exists")
|
||||||
|
)
|
||||||
|
file.setUploadId(v)
|
||||||
|
// I'M GONNA KILL MYSELF!!!!
|
||||||
|
} else if (k == "file") {
|
||||||
|
if (!acceptNewData || file.writableEnded)
|
||||||
|
return file.destroy(
|
||||||
|
new WebError(
|
||||||
|
400,
|
||||||
|
"cannot set file after previously setting up another upload"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
acceptNewData = false
|
||||||
|
|
||||||
|
let res = await fetch(v, {
|
||||||
|
headers: {
|
||||||
|
"user-agent": `monofile ${
|
||||||
|
MONOFILE_VERSION
|
||||||
|
} (+https://${ctx.req.header("Host")})`,
|
||||||
|
},
|
||||||
|
}).catch(escalate)
|
||||||
|
|
||||||
|
if (!res) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
!file.setName(
|
||||||
|
res.headers
|
||||||
|
.get("Content-Disposition")
|
||||||
|
?.match(/filename="(.*)"/)?.[1] ||
|
||||||
|
v.split("/")[v.split("/").length - 1] ||
|
||||||
|
"generic"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (res.headers.has("Content-Type"))
|
||||||
|
if (!file.setType(res.headers.get("Content-Type")!))
|
||||||
|
return
|
||||||
|
|
||||||
|
if (!res.ok)
|
||||||
|
return file.destroy(
|
||||||
|
new WebError(
|
||||||
|
500,
|
||||||
|
`got ${res.status} ${res.statusText}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (!res.body)
|
||||||
|
return file.destroy(
|
||||||
|
new WebError(500, `Internal Server Error`)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
res.headers.has("Content-Length") &&
|
||||||
|
!Number.isNaN(
|
||||||
|
parseInt(res.headers.get("Content-Length")!, 10)
|
||||||
|
) &&
|
||||||
|
parseInt(res.headers.get("Content-Length")!, 10) >
|
||||||
|
files.config.maxDiscordFileSize *
|
||||||
|
files.config.maxDiscordFiles
|
||||||
|
)
|
||||||
|
return file.destroy(
|
||||||
|
new WebError(413, `file reports to be too large`)
|
||||||
|
)
|
||||||
|
|
||||||
|
Readable.fromWeb(res.body as StreamWebReadable).pipe(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
parser.parse(ctx.env.incoming).catch((e) => console.error(e))
|
||||||
|
|
||||||
|
parser.on("error", (err) => {
|
||||||
|
escalate(err)
|
||||||
|
if (!file.destroyed) file.destroy(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
186
src/api/v1/file/individual.ts
Normal file
186
src/api/v1/file/individual.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import * as Accounts from "../../../lib/accounts.js"
|
||||||
|
import * as auth from "../../../lib/auth.js"
|
||||||
|
import RangeParser, { type Range } from "range-parser"
|
||||||
|
import ServeError from "../../../lib/errors.js"
|
||||||
|
import Files, { WebError } from "../../../lib/files.js"
|
||||||
|
import { getAccount, requiresPermissions } from "../../../lib/middleware.js"
|
||||||
|
import { Readable } from "node:stream"
|
||||||
|
import type { ReadableStream as StreamWebReadable } from "node:stream/web"
|
||||||
|
import formidable from "formidable"
|
||||||
|
import { HttpBindings } from "@hono/node-server"
|
||||||
|
import { type StatusCode } from "hono/utils/http-status"
|
||||||
|
|
||||||
|
const router = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
account: Accounts.Account
|
||||||
|
}
|
||||||
|
Bindings: HttpBindings
|
||||||
|
}>()
|
||||||
|
router.all("*", getAccount)
|
||||||
|
|
||||||
|
export default function (files: Files, apiRoot: Hono) {
|
||||||
|
router.get("/:id", async (ctx) => {
|
||||||
|
const fileId = ctx.req.param("id")
|
||||||
|
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
|
||||||
|
let file = files.files[fileId]
|
||||||
|
ctx.header("Accept-Ranges", "bytes")
|
||||||
|
ctx.header("Access-Control-Allow-Origin", "*")
|
||||||
|
ctx.header("Content-Security-Policy", "sandbox allow-scripts")
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
ctx.header(
|
||||||
|
"Content-Disposition",
|
||||||
|
`${
|
||||||
|
ctx.req.query("attachment") == "1" ? "attachment" : "inline"
|
||||||
|
}; filename="${encodeURI(
|
||||||
|
file.filename.replaceAll("\n", "\\n")
|
||||||
|
)}"`
|
||||||
|
)
|
||||||
|
ctx.header("ETag", file.md5)
|
||||||
|
if (file.lastModified) {
|
||||||
|
let lm = new Date(file.lastModified)
|
||||||
|
// TERRIFYING
|
||||||
|
ctx.header(
|
||||||
|
"Last-Modified",
|
||||||
|
`${
|
||||||
|
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][
|
||||||
|
lm.getUTCDay()
|
||||||
|
]
|
||||||
|
}, ${lm.getUTCDate()} ` +
|
||||||
|
`${
|
||||||
|
[
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
][lm.getUTCMonth()]
|
||||||
|
}` +
|
||||||
|
` ${lm.getUTCFullYear()} ${lm
|
||||||
|
.getUTCHours()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}` +
|
||||||
|
`:${lm.getUTCMinutes().toString().padStart(2, "0")}:${lm
|
||||||
|
.getUTCSeconds()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")} GMT`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.visibility == "private") {
|
||||||
|
if (acc?.id != file.owner) {
|
||||||
|
return ServeError(ctx, 403, "you do not own this file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
auth.getType(auth.tokenFor(ctx)!) == "App" &&
|
||||||
|
auth
|
||||||
|
.getPermissions(auth.tokenFor(ctx)!)
|
||||||
|
?.includes("private")
|
||||||
|
) {
|
||||||
|
return ServeError(ctx, 403, "insufficient permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let range: Range | undefined
|
||||||
|
|
||||||
|
ctx.header("Content-Type", file.mime)
|
||||||
|
if (file.sizeInBytes) {
|
||||||
|
ctx.header("Content-Length", file.sizeInBytes.toString())
|
||||||
|
|
||||||
|
if (file.chunkSize && ctx.req.header("Range")) {
|
||||||
|
let ranges = RangeParser(
|
||||||
|
file.sizeInBytes,
|
||||||
|
ctx.req.header("Range") || ""
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ranges) {
|
||||||
|
if (typeof ranges == "number")
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
ranges == -1 ? 416 : 400,
|
||||||
|
ranges == -1
|
||||||
|
? "unsatisfiable ranges"
|
||||||
|
: "invalid ranges"
|
||||||
|
)
|
||||||
|
if (ranges.length > 1)
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"multiple ranges not supported"
|
||||||
|
)
|
||||||
|
range = ranges[0]
|
||||||
|
|
||||||
|
ctx.status(206)
|
||||||
|
ctx.header(
|
||||||
|
"Content-Length",
|
||||||
|
(range.end - range.start + 1).toString()
|
||||||
|
)
|
||||||
|
ctx.header(
|
||||||
|
"Content-Range",
|
||||||
|
`bytes ${range.start}-${range.end}/${file.sizeInBytes}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.req.method == "HEAD") return ctx.body(null)
|
||||||
|
|
||||||
|
return files
|
||||||
|
.readFileStream(fileId, range)
|
||||||
|
.then(async (stream) => {
|
||||||
|
let rs = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
stream.once("end", () => controller.close())
|
||||||
|
stream.once("error", (err) => controller.error(err))
|
||||||
|
},
|
||||||
|
cancel(reason) {
|
||||||
|
stream.destroy(
|
||||||
|
reason instanceof Error
|
||||||
|
? reason
|
||||||
|
: new Error(reason)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
stream.pipe(ctx.env.outgoing)
|
||||||
|
return new Response(rs, ctx.body(null))
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return ServeError(ctx, err.status, err.message)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return ServeError(ctx, 404, "file not found")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.on(["PUT", "POST"], "/:id", async (ctx) => {
|
||||||
|
ctx.env.incoming.push(
|
||||||
|
`--${
|
||||||
|
ctx.req.header("content-type")?.match(/boundary=(\S+)/)?.[1]
|
||||||
|
}\r\n` +
|
||||||
|
`Content-Disposition: form-data; name="uploadId"\r\n\r\n` +
|
||||||
|
ctx.req.param("id") +
|
||||||
|
"\r\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
return apiRoot.fetch(
|
||||||
|
new Request(
|
||||||
|
new URL(`/api/v1/file`, ctx.req.raw.url).href,
|
||||||
|
ctx.req.raw
|
||||||
|
),
|
||||||
|
ctx.env
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
15
src/api/v1/index.ts
Normal file
15
src/api/v1/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import account from "./account.js"
|
||||||
|
import session from "./session.js"
|
||||||
|
import file from "./file/index.js"
|
||||||
|
import individual from "./file/individual.js"
|
||||||
|
import type Files from "../../lib/files.js"
|
||||||
|
|
||||||
|
export default function install(root: Hono, files: Files) {
|
||||||
|
const router = new Hono()
|
||||||
|
router.route("/account", account(files))
|
||||||
|
router.route("/session", session(files))
|
||||||
|
router.route("/file", file(files)) // /file API
|
||||||
|
router.route("/file", individual(files, root)) // /file/:id
|
||||||
|
root.route("/api/v1", router)
|
||||||
|
}
|
|
@ -1,20 +1,15 @@
|
||||||
// Modules
|
// Modules
|
||||||
|
|
||||||
|
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { getCookie, setCookie } from "hono/cookie"
|
import { getCookie, setCookie } from "hono/cookie"
|
||||||
|
|
||||||
// Libs
|
// Libs
|
||||||
|
|
||||||
import Files, { id_check_regex } from "../../../lib/files.js"
|
import Files, { id_check_regex } from "../../lib/files.js"
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
import * as Accounts from "../../lib/accounts.js"
|
||||||
import * as auth from "../../../lib/auth.js"
|
import * as auth from "../../lib/auth.js"
|
||||||
import {
|
import { getAccount, login, requiresAccount } from "../../lib/middleware.js"
|
||||||
getAccount,
|
import ServeError from "../../lib/errors.js"
|
||||||
login,
|
|
||||||
requiresAccount
|
|
||||||
} from "../../../lib/middleware.js"
|
|
||||||
import ServeError from "../../../lib/errors.js"
|
|
||||||
|
|
||||||
const router = new Hono<{
|
const router = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
||||||
|
@ -51,12 +46,11 @@ export default function (files: Files) {
|
||||||
return ctx.text("logged in")
|
return ctx.text("logged in")
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get("/", requiresAccount, ctx => {
|
router.get("/", requiresAccount, (ctx) => {
|
||||||
let sessionToken = auth.tokenFor(ctx)
|
let sessionToken = auth.tokenFor(ctx)
|
||||||
return ctx.json({
|
return ctx.json({
|
||||||
expiry: auth.AuthTokens.find(
|
expiry: auth.AuthTokens.find((e) => e.token == sessionToken)
|
||||||
(e) => e.token == sessionToken
|
?.expire,
|
||||||
)?.expire,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import bytes from "bytes"
|
import bytes from "bytes"
|
||||||
import ServeError from "../../../lib/errors.js"
|
import ServeError from "../../lib/errors.js"
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
import * as Accounts from "../../lib/accounts.js"
|
||||||
import type Files from "../../../lib/files.js"
|
import type Files from "../../lib/files.js"
|
||||||
import pkg from "../../../../../package.json" assert {type:"json"}
|
import { CodeMgr } from "../../lib/mail.js"
|
||||||
import { CodeMgr } from "../../../lib/mail.js"
|
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { getAccount, login } from "../../../lib/middleware.js"
|
import { getAccount, login } from "../../lib/middleware.js"
|
||||||
export let router = new Hono<{
|
export let router = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
||||||
account: Accounts.Account
|
account: Accounts.Account
|
||||||
|
@ -20,7 +19,11 @@ export default function (files: Files) {
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
if (currentAccount != undefined && !code.check(currentAccount.id)) {
|
if (currentAccount != undefined && !code.check(currentAccount.id)) {
|
||||||
return ServeError(ctx, 403, "you are logged in on a different account")
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
403,
|
||||||
|
"you are logged in on a different account"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentAccount) {
|
if (!currentAccount) {
|
||||||
|
@ -32,10 +35,10 @@ export default function (files: Files) {
|
||||||
|
|
||||||
currentAccount.email = code.data
|
currentAccount.email = code.data
|
||||||
await Accounts.save()
|
await Accounts.save()
|
||||||
|
|
||||||
return ctx.redirect('/')
|
return ctx.redirect("/")
|
||||||
} else return ServeError(ctx, 404, "code not found")
|
} else return ServeError(ctx, 404, "code not found")
|
||||||
})
|
})
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
8
src/api/web/index.ts
Normal file
8
src/api/web/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import type Files from "../../lib/files.js"
|
||||||
|
import go from "./go.js"
|
||||||
|
export default function install(root: Hono, files: Files) {
|
||||||
|
const router = new Hono()
|
||||||
|
router.route("/go", go(files))
|
||||||
|
root.route("/api/v1", router)
|
||||||
|
}
|
13
src/app.html
Normal file
13
src/app.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.svg">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="/api/v1/account/me/css">
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -4,8 +4,7 @@ import Files from "./lib/files.js"
|
||||||
import { program } from "commander"
|
import { program } from "commander"
|
||||||
import { basename } from "path"
|
import { basename } from "path"
|
||||||
import { Writable } from "node:stream"
|
import { Writable } from "node:stream"
|
||||||
import pkg from "../../package.json" assert { type: "json" }
|
import pkg from "../package.json" assert { type: "json" }
|
||||||
import config from "../../config.json" assert { type: "json" }
|
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
import { dirname } from "path"
|
import { dirname } from "path"
|
||||||
|
|
||||||
|
@ -15,6 +14,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
if (!fs.existsSync(__dirname + "/../../.data/"))
|
if (!fs.existsSync(__dirname + "/../../.data/"))
|
||||||
fs.mkdirSync(__dirname + "/../../.data/")
|
fs.mkdirSync(__dirname + "/../../.data/")
|
||||||
|
|
||||||
|
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
|
||||||
|
const config = JSON.parse(
|
||||||
|
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
|
||||||
|
// Also if this were NixOS you can't modify the built folder after build
|
||||||
|
fs.readFileSync(process.cwd() + "/config.json", "utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
// discord
|
// discord
|
||||||
let files = new Files(config)
|
let files = new Files(config)
|
||||||
|
|
||||||
|
@ -23,65 +29,65 @@ program
|
||||||
.description("Quickly run monofile to execute a query or so")
|
.description("Quickly run monofile to execute a query or so")
|
||||||
.version(pkg.version)
|
.version(pkg.version)
|
||||||
|
|
||||||
program.command("list")
|
program
|
||||||
|
.command("list")
|
||||||
.alias("ls")
|
.alias("ls")
|
||||||
.description("List files in the database")
|
.description("List files in the database")
|
||||||
.action(() => {
|
.action(() => {
|
||||||
Object.keys(files.files).forEach(e => console.log(e))
|
Object.keys(files.files).forEach((e) => console.log(e))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
program
|
||||||
program.command("download")
|
.command("download")
|
||||||
.alias("dl")
|
.alias("dl")
|
||||||
.description("Download a file from the database")
|
.description("Download a file from the database")
|
||||||
.argument("<id>", "ID of the file you'd like to download")
|
.argument("<id>", "ID of the file you'd like to download")
|
||||||
.option("-o, --output <path>", 'Folder or filename to output to')
|
.option("-o, --output <path>", "Folder or filename to output to")
|
||||||
.action(async (id, options) => {
|
.action(async (id, options) => {
|
||||||
|
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000))
|
||||||
await (new Promise<void>(resolve => setTimeout(() => resolve(), 1000)))
|
|
||||||
|
|
||||||
let fp = files.files[id]
|
let fp = files.files[id]
|
||||||
|
|
||||||
if (!fp)
|
if (!fp) throw `file ${id} not found`
|
||||||
throw `file ${id} not found`
|
|
||||||
|
let out = (options.output as string) || `./`
|
||||||
let out = options.output as string || `./`
|
|
||||||
|
|
||||||
if (fs.existsSync(out) && (await stat(out)).isDirectory())
|
if (fs.existsSync(out) && (await stat(out)).isDirectory())
|
||||||
out = `${out.replace(/\/+$/, "")}/${fp.filename}`
|
out = `${out.replace(/\/+$/, "")}/${fp.filename}`
|
||||||
|
|
||||||
let filestream = await files.readFileStream(id)
|
let filestream = await files.readFileStream(id)
|
||||||
|
|
||||||
let prog=0
|
let prog = 0
|
||||||
filestream.on("data", dt => {
|
filestream.on("data", (dt) => {
|
||||||
prog+=dt.byteLength
|
prog += dt.byteLength
|
||||||
console.log(`Downloading ${fp.filename}: ${Math.floor(prog/(fp.sizeInBytes??0)*10000)/100}% (${Math.floor(prog/(1024*1024))}MiB/${Math.floor((fp.sizeInBytes??0)/(1024*1024))}MiB)`)
|
console.log(
|
||||||
|
`Downloading ${fp.filename}: ${
|
||||||
|
Math.floor((prog / (fp.sizeInBytes ?? 0)) * 10000) / 100
|
||||||
|
}% (${Math.floor(prog / (1024 * 1024))}MiB/${Math.floor(
|
||||||
|
(fp.sizeInBytes ?? 0) / (1024 * 1024)
|
||||||
|
)}MiB)`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
filestream.pipe(
|
filestream.pipe(fs.createWriteStream(out))
|
||||||
fs.createWriteStream(out)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
program
|
||||||
program.command("upload")
|
.command("upload")
|
||||||
.alias("up")
|
.alias("up")
|
||||||
.description("Upload a file to the instance")
|
.description("Upload a file to the instance")
|
||||||
.argument("<file>", "Path to the file you'd like to upload")
|
.argument("<file>", "Path to the file you'd like to upload")
|
||||||
.option("-id, --fileid <id>", 'Custom file ID to use')
|
.option("-id, --fileid <id>", "Custom file ID to use")
|
||||||
.action(async (file, options) => {
|
.action(async (file, options) => {
|
||||||
|
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000))
|
||||||
await (new Promise<void>(resolve => setTimeout(() => resolve(), 1000)))
|
|
||||||
|
|
||||||
if (!(fs.existsSync(file) && (await stat(file)).isFile()))
|
if (!(fs.existsSync(file) && (await stat(file)).isFile()))
|
||||||
throw `${file} is not a file`
|
throw `${file} is not a file`
|
||||||
|
|
||||||
let writable = files.createWriteStream()
|
let writable = files.createWriteStream()
|
||||||
|
|
||||||
writable
|
writable.setName(basename(file))?.setType("application/octet-stream")
|
||||||
.setName(basename(file))
|
|
||||||
?.setType("application/octet-stream")
|
|
||||||
|
|
||||||
if (options.id) writable.setUploadId(options.id)
|
if (options.id) writable.setUploadId(options.id)
|
||||||
|
|
||||||
if (!(writable instanceof Writable))
|
if (!(writable instanceof Writable))
|
||||||
|
@ -90,7 +96,7 @@ program.command("upload")
|
||||||
console.log(`started: ${file}`)
|
console.log(`started: ${file}`)
|
||||||
|
|
||||||
writable.on("drain", () => {
|
writable.on("drain", () => {
|
||||||
console.log("Drained");
|
console.log("Drained")
|
||||||
})
|
})
|
||||||
|
|
||||||
writable.on("finish", async () => {
|
writable.on("finish", async () => {
|
||||||
|
@ -108,11 +114,8 @@ program.command("upload")
|
||||||
|
|
||||||
writable.on("close", () => {
|
writable.on("close", () => {
|
||||||
console.log("Closed.")
|
console.log("Closed.")
|
||||||
});
|
})
|
||||||
|
;(await fs.createReadStream(file)).pipe(writable)
|
||||||
;(await fs.createReadStream(file)).pipe(
|
|
||||||
writable
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
program.parse()
|
program.parse()
|
1
src/consts.d.ts
vendored
Normal file
1
src/consts.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
declare const MONOFILE_VERSION: string
|
|
@ -1,54 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>$FileId</title>
|
|
||||||
|
|
||||||
<!--metaTags-->
|
|
||||||
|
|
||||||
<meta name="og:site_name" content="$Uploader">
|
|
||||||
<meta name="title" content="$FileName">
|
|
||||||
<meta name="description" content="$FileSize file on monofile $Version, the Discord-based file sharing service">
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="./style/downloads.scss"
|
|
||||||
>
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="/api/v1/account/me/css"
|
|
||||||
>
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/svg"
|
|
||||||
href="/static/assets/icons/file_icon.svg"
|
|
||||||
>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="appContent">
|
|
||||||
<div id="uploadWindow">
|
|
||||||
<h1>
|
|
||||||
$FileName
|
|
||||||
</h1>
|
|
||||||
<p style="color:#999999">
|
|
||||||
<span class="number">$FileSize</span> — uploaded by <span class="number">$Uploader</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!--preview-->
|
|
||||||
|
|
||||||
<button style="position:relative;width:100%;top:10px;">
|
|
||||||
<a id="dlbtn" href="/file/$FileId" download="$FileName" style="position:absolute;left:0px;top:0px;height:100%;width:100%;"></a>
|
|
||||||
download
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div style="min-height:15px" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,41 +1,21 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="stylesheet" href="./style/error.scss" />
|
||||||
<link
|
<link rel="icon" type="image/svg" href="/assets/icons/error.svg" />
|
||||||
rel="stylesheet"
|
<link rel="stylesheet" href="/api/v1/account/me/css" />
|
||||||
href="./style/error.scss"
|
<meta
|
||||||
>
|
name="viewport"
|
||||||
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/svg"
|
|
||||||
href="/static/assets/icons/error.svg"
|
|
||||||
>
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="/api/v1/account/me/css"
|
|
||||||
>
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0, user-scalable=0"
|
content="width=device-width, initial-scale=1.0, user-scalable=0"
|
||||||
>
|
/>
|
||||||
|
<title>%sveltekit.status%</title>
|
||||||
<title>$code</title>
|
<meta name="theme-color" content="rgb(30, 33, 36)" />
|
||||||
|
|
||||||
<meta name="theme-color" content="rgb(30, 33, 36)">
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<p class="error">
|
<p class="error">
|
||||||
<span class="code">$code</span>
|
<span class="code">%sveltekit.status%</span>
|
||||||
$text
|
%sveltekit.error.message%
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
<!--
|
|
||||||
for some reason (don't know why)
|
|
||||||
certain things break
|
|
||||||
when not in quirks mode
|
|
||||||
so i'm not adding in the
|
|
||||||
doctype html
|
|
||||||
-->
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="./style/app.scss" />
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/svg"
|
|
||||||
href="/static/assets/icons/icon.svg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/api/v1/account/me/css" />
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0, user-scalable=0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<script type="module" src="./svelte/index.ts"></script>
|
|
||||||
|
|
||||||
<title>monofile</title>
|
|
||||||
|
|
||||||
<meta name="title" content="monofile" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="The open-source Discord-based file sharing service"
|
|
||||||
/>
|
|
||||||
<meta name="theme-color" content="rgb(30, 33, 36)" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
30
src/lib/errors.ts
Normal file
30
src/lib/errors.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { readFile } from "fs/promises"
|
||||||
|
import type { Context } from "hono"
|
||||||
|
import type { StatusCode } from "hono/utils/http-status"
|
||||||
|
|
||||||
|
import errorPage from "../error.html?raw"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Serves an error as a response to a request with an error page attached
|
||||||
|
* @param ctx Express response object
|
||||||
|
* @param code Error code
|
||||||
|
* @param reason Error reason
|
||||||
|
*/
|
||||||
|
export default async function ServeError(
|
||||||
|
ctx: Context,
|
||||||
|
code: number = 500,
|
||||||
|
reason: string
|
||||||
|
) {
|
||||||
|
// serve error
|
||||||
|
return ctx.req.header("accept")?.includes("text/html")
|
||||||
|
? ctx.html(
|
||||||
|
errorPage
|
||||||
|
.replaceAll("%sveltekit.status%", code.toString())
|
||||||
|
.replaceAll("%sveltekit.error.message%", reason),
|
||||||
|
code as StatusCode /*,
|
||||||
|
{
|
||||||
|
"x-backup-status-message": reason, // glitch default nginx configuration
|
||||||
|
}*/
|
||||||
|
)
|
||||||
|
: ctx.text(reason, code as StatusCode)
|
||||||
|
}
|
|
@ -3,8 +3,9 @@ import { Readable, Writable } from "node:stream"
|
||||||
import crypto from "node:crypto"
|
import crypto from "node:crypto"
|
||||||
import { files } from "./accounts.js"
|
import { files } from "./accounts.js"
|
||||||
import { Client as API } from "./DiscordAPI/index.js"
|
import { Client as API } from "./DiscordAPI/index.js"
|
||||||
import type {APIAttachment} from "discord-api-types/v10"
|
import type { APIAttachment } from "discord-api-types/v10"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
|
import { readFileSync } from "node:fs"
|
||||||
|
|
||||||
import * as Accounts from "./accounts.js"
|
import * as Accounts from "./accounts.js"
|
||||||
|
|
||||||
|
@ -32,10 +33,12 @@ export function generateFileId(length: number = 5) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Assert multiple conditions... this exists out of pure laziness
|
* @description Assert multiple conditions... this exists out of pure laziness
|
||||||
* @param conditions
|
* @param conditions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function multiAssert(conditions: Map<boolean, { message: string, status: number }>) {
|
function multiAssert(
|
||||||
|
conditions: Map<boolean, { message: string; status: number }>
|
||||||
|
) {
|
||||||
for (let [cond, err] of conditions.entries()) {
|
for (let [cond, err] of conditions.entries()) {
|
||||||
if (cond) return err
|
if (cond) return err
|
||||||
}
|
}
|
||||||
|
@ -80,18 +83,15 @@ export interface StatusCodeError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebError extends Error {
|
export class WebError extends Error {
|
||||||
|
|
||||||
readonly statusCode: number = 500
|
readonly statusCode: number = 500
|
||||||
|
|
||||||
constructor(status: number, message: string) {
|
constructor(status: number, message: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.statusCode = status
|
this.statusCode = status
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReadStream extends Readable {
|
export class ReadStream extends Readable {
|
||||||
|
|
||||||
files: Files
|
files: Files
|
||||||
pointer: FilePointer
|
pointer: FilePointer
|
||||||
|
|
||||||
|
@ -100,52 +100,60 @@ export class ReadStream extends Readable {
|
||||||
position: number = 0
|
position: number = 0
|
||||||
|
|
||||||
ranges: {
|
ranges: {
|
||||||
useRanges: boolean,
|
useRanges: boolean
|
||||||
byteStart: number,
|
byteStart: number
|
||||||
byteEnd: number
|
byteEnd: number
|
||||||
scan_msg_begin: number,
|
scan_msg_begin: number
|
||||||
scan_msg_end: number,
|
scan_msg_end: number
|
||||||
scan_files_begin: number,
|
scan_files_begin: number
|
||||||
scan_files_end: number
|
scan_files_end: number
|
||||||
}
|
}
|
||||||
|
|
||||||
id: number = Math.random()
|
id: number = Math.random()
|
||||||
aborter?: AbortController
|
aborter?: AbortController
|
||||||
|
|
||||||
constructor(files: Files, pointer: FilePointer, range?: {start: number, end: number}) {
|
constructor(
|
||||||
|
files: Files,
|
||||||
|
pointer: FilePointer,
|
||||||
|
range?: { start: number; end: number }
|
||||||
|
) {
|
||||||
super()
|
super()
|
||||||
console.log(this.id, range)
|
console.log(this.id, range)
|
||||||
this.files = files
|
this.files = files
|
||||||
this.pointer = pointer
|
this.pointer = pointer
|
||||||
|
|
||||||
let useRanges =
|
let useRanges = Boolean(
|
||||||
Boolean(range && pointer.chunkSize && pointer.sizeInBytes)
|
range && pointer.chunkSize && pointer.sizeInBytes
|
||||||
|
)
|
||||||
|
|
||||||
this.ranges = {
|
this.ranges = {
|
||||||
useRanges,
|
useRanges,
|
||||||
scan_msg_begin: 0,
|
scan_msg_begin: 0,
|
||||||
scan_msg_end: pointer.messageids.length - 1,
|
scan_msg_end: pointer.messageids.length - 1,
|
||||||
scan_files_begin:
|
scan_files_begin: useRanges
|
||||||
useRanges
|
|
||||||
? Math.floor(range!.start / pointer.chunkSize!)
|
? Math.floor(range!.start / pointer.chunkSize!)
|
||||||
: 0,
|
: 0,
|
||||||
scan_files_end:
|
scan_files_end: useRanges
|
||||||
useRanges
|
|
||||||
? Math.ceil(range!.end / pointer.chunkSize!) - 1
|
? Math.ceil(range!.end / pointer.chunkSize!) - 1
|
||||||
: -1,
|
: -1,
|
||||||
byteStart: range?.start || 0,
|
byteStart: range?.start || 0,
|
||||||
byteEnd: range?.end || 0
|
byteEnd: range?.end || 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useRanges)
|
if (useRanges)
|
||||||
this.ranges.scan_msg_begin = Math.floor(this.ranges.scan_files_begin / 10),
|
(this.ranges.scan_msg_begin = Math.floor(
|
||||||
this.ranges.scan_msg_end = Math.ceil(this.ranges.scan_files_end / 10),
|
this.ranges.scan_files_begin / 10
|
||||||
this.msgIdx = this.ranges.scan_msg_begin
|
)),
|
||||||
|
(this.ranges.scan_msg_end = Math.ceil(
|
||||||
|
this.ranges.scan_files_end / 10
|
||||||
|
)),
|
||||||
|
(this.msgIdx = this.ranges.scan_msg_begin)
|
||||||
|
|
||||||
console.log(this.ranges)
|
console.log(this.ranges)
|
||||||
}
|
}
|
||||||
|
|
||||||
async _read() {/*
|
async _read() {
|
||||||
|
/*
|
||||||
console.log("Calling for more data")
|
console.log("Calling for more data")
|
||||||
if (this.busy) return
|
if (this.busy) return
|
||||||
this.busy = true
|
this.busy = true
|
||||||
|
@ -160,24 +168,32 @@ export class ReadStream extends Readable {
|
||||||
this.pushData()
|
this.pushData()
|
||||||
}
|
}
|
||||||
|
|
||||||
async _destroy(error: Error | null, callback: (error?: Error | null | undefined) => void): Promise<void> {
|
async _destroy(
|
||||||
if (this.aborter)
|
error: Error | null,
|
||||||
this.aborter.abort()
|
callback: (error?: Error | null | undefined) => void
|
||||||
|
): Promise<void> {
|
||||||
|
if (this.aborter) this.aborter.abort()
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextAttachment() {
|
async getNextAttachment() {
|
||||||
// return first in our attachment buffer
|
// return first in our attachment buffer
|
||||||
let ret = this.attachmentBuffer.splice(0,1)[0]
|
let ret = this.attachmentBuffer.splice(0, 1)[0]
|
||||||
if (ret) return ret
|
if (ret) return ret
|
||||||
|
|
||||||
console.log(this.id, this.msgIdx, this.ranges.scan_msg_end, this.pointer.messageids[this.msgIdx])
|
console.log(
|
||||||
|
this.id,
|
||||||
|
this.msgIdx,
|
||||||
|
this.ranges.scan_msg_end,
|
||||||
|
this.pointer.messageids[this.msgIdx]
|
||||||
|
)
|
||||||
|
|
||||||
// oh, there's none left. let's fetch a new message, then.
|
// oh, there's none left. let's fetch a new message, then.
|
||||||
if (
|
if (
|
||||||
!this.pointer.messageids[this.msgIdx]
|
!this.pointer.messageids[this.msgIdx] ||
|
||||||
|| this.msgIdx > this.ranges.scan_msg_end
|
this.msgIdx > this.ranges.scan_msg_end
|
||||||
) return null
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
let msg = await this.files.api
|
let msg = await this.files.api
|
||||||
.fetchMessage(this.pointer.messageids[this.msgIdx])
|
.fetchMessage(this.pointer.messageids[this.msgIdx])
|
||||||
|
@ -190,95 +206,113 @@ export class ReadStream extends Readable {
|
||||||
let attach = msg.attachments
|
let attach = msg.attachments
|
||||||
console.log(attach)
|
console.log(attach)
|
||||||
|
|
||||||
this.attachmentBuffer = this.ranges.useRanges ? attach.slice(
|
this.attachmentBuffer = this.ranges.useRanges
|
||||||
this.msgIdx == this.ranges.scan_msg_begin
|
? attach.slice(
|
||||||
? this.ranges.scan_files_begin - this.ranges.scan_msg_begin * 10
|
this.msgIdx == this.ranges.scan_msg_begin
|
||||||
: 0,
|
? this.ranges.scan_files_begin -
|
||||||
this.msgIdx == this.ranges.scan_msg_end
|
this.ranges.scan_msg_begin * 10
|
||||||
? this.ranges.scan_files_end - this.ranges.scan_msg_end * 10 + 1
|
: 0,
|
||||||
: attach.length
|
this.msgIdx == this.ranges.scan_msg_end
|
||||||
) : attach
|
? this.ranges.scan_files_end -
|
||||||
|
this.ranges.scan_msg_end * 10 +
|
||||||
|
1
|
||||||
|
: attach.length
|
||||||
|
)
|
||||||
|
: attach
|
||||||
|
|
||||||
console.log(this.attachmentBuffer)
|
console.log(this.attachmentBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.msgIdx++
|
this.msgIdx++
|
||||||
return this.attachmentBuffer.splice(0,1)[0]
|
return this.attachmentBuffer.splice(0, 1)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPusherForWebStream(webStream: ReadableStream) {
|
async getPusherForWebStream(webStream: ReadableStream) {
|
||||||
const reader = await webStream.getReader()
|
const reader = await webStream.getReader()
|
||||||
let pushing = false // acts as a debounce just in case
|
let pushing = false // acts as a debounce just in case
|
||||||
// (words of a girl paranoid from writing readfilestream)
|
// (words of a girl paranoid from writing readfilestream)
|
||||||
|
|
||||||
let pushToStream = this.push.bind(this)
|
let pushToStream = this.push.bind(this)
|
||||||
let stream = this
|
let stream = this
|
||||||
|
|
||||||
return function() {
|
return function () {
|
||||||
if (pushing) return
|
if (pushing) return
|
||||||
pushing = true
|
pushing = true
|
||||||
|
|
||||||
return reader.read().catch(e => {
|
return reader
|
||||||
// Probably means an AbortError; whatever it is we'll need to abort
|
.read()
|
||||||
if (webStream.locked) reader.releaseLock()
|
.catch((e) => {
|
||||||
webStream.cancel().catch(e => undefined)
|
// Probably means an AbortError; whatever it is we'll need to abort
|
||||||
if (!stream.destroyed) stream.destroy()
|
if (webStream.locked) reader.releaseLock()
|
||||||
return e
|
webStream.cancel().catch((e) => undefined)
|
||||||
}).then(result => {
|
if (!stream.destroyed) stream.destroy()
|
||||||
if (result instanceof Error || !result) return result
|
return e
|
||||||
|
})
|
||||||
let pushed
|
.then((result) => {
|
||||||
if (!result.done) {
|
if (result instanceof Error || !result) return result
|
||||||
pushing = false
|
|
||||||
pushed = pushToStream(result.value)
|
let pushed
|
||||||
}
|
if (!result.done) {
|
||||||
return {readyForMore: pushed || false, streamDone: result.done }
|
pushing = false
|
||||||
})
|
pushed = pushToStream(result.value)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
readyForMore: pushed || false,
|
||||||
|
streamDone: result.done,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextChunk() {
|
async getNextChunk() {
|
||||||
let scanning_chunk = await this.getNextAttachment()
|
let scanning_chunk = await this.getNextAttachment()
|
||||||
console.log(this.id, "Next chunk requested; got attachment", scanning_chunk)
|
console.log(
|
||||||
|
this.id,
|
||||||
|
"Next chunk requested; got attachment",
|
||||||
|
scanning_chunk
|
||||||
|
)
|
||||||
if (!scanning_chunk) return null
|
if (!scanning_chunk) return null
|
||||||
|
|
||||||
let {
|
let { byteStart, byteEnd, scan_files_begin, scan_files_end } =
|
||||||
byteStart, byteEnd, scan_files_begin, scan_files_end
|
this.ranges
|
||||||
} = this.ranges
|
|
||||||
|
|
||||||
let headers: HeadersInit =
|
let headers: HeadersInit = this.ranges.useRanges
|
||||||
this.ranges.useRanges
|
? {
|
||||||
? {
|
Range: `bytes=${
|
||||||
Range: `bytes=${
|
this.position == 0
|
||||||
this.position == 0
|
? byteStart -
|
||||||
? byteStart - scan_files_begin * this.pointer.chunkSize!
|
scan_files_begin * this.pointer.chunkSize!
|
||||||
: "0"
|
: "0"
|
||||||
}-${
|
}-${
|
||||||
this.attachmentBuffer.length == 0 && this.msgIdx == scan_files_end
|
this.attachmentBuffer.length == 0 &&
|
||||||
? byteEnd - scan_files_end * this.pointer.chunkSize!
|
this.msgIdx == scan_files_end
|
||||||
: ""
|
? byteEnd - scan_files_end * this.pointer.chunkSize!
|
||||||
}`,
|
: ""
|
||||||
}
|
}`,
|
||||||
: {}
|
}
|
||||||
|
: {}
|
||||||
|
|
||||||
this.aborter = new AbortController()
|
this.aborter = new AbortController()
|
||||||
|
|
||||||
let response = await fetch(scanning_chunk.url, {headers, signal: this.aborter.signal})
|
let response = await fetch(scanning_chunk.url, {
|
||||||
.catch((e: Error) => {
|
headers,
|
||||||
console.error(e)
|
signal: this.aborter.signal,
|
||||||
return {body: e}
|
}).catch((e: Error) => {
|
||||||
})
|
console.error(e)
|
||||||
|
return { body: e }
|
||||||
|
})
|
||||||
|
|
||||||
this.position++
|
this.position++
|
||||||
|
|
||||||
return response.body
|
return response.body
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPusher?: (() => Promise<{readyForMore: boolean, streamDone: boolean } | void> | undefined)
|
currentPusher?: () =>
|
||||||
|
| Promise<{ readyForMore: boolean; streamDone: boolean } | void>
|
||||||
|
| undefined
|
||||||
busy: boolean = false
|
busy: boolean = false
|
||||||
|
|
||||||
async pushData(): Promise<boolean | undefined> {
|
async pushData(): Promise<boolean | undefined> {
|
||||||
|
|
||||||
// uh oh, we don't have a currentPusher
|
// uh oh, we don't have a currentPusher
|
||||||
// let's make one then
|
// let's make one then
|
||||||
if (!this.currentPusher) {
|
if (!this.currentPusher) {
|
||||||
|
@ -292,7 +326,8 @@ export class ReadStream extends Readable {
|
||||||
// or the stream has ended.
|
// or the stream has ended.
|
||||||
// let's destroy the stream
|
// let's destroy the stream
|
||||||
console.log(this.id, "Ending", next)
|
console.log(this.id, "Ending", next)
|
||||||
if (next) this.destroy(next); else this.push(null)
|
if (next) this.destroy(next)
|
||||||
|
else this.push(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -304,12 +339,10 @@ export class ReadStream extends Readable {
|
||||||
this.currentPusher = undefined
|
this.currentPusher = undefined
|
||||||
return this.pushData()
|
return this.pushData()
|
||||||
} else return result?.readyForMore
|
} else return result?.readyForMore
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UploadStream extends Writable {
|
export class UploadStream extends Writable {
|
||||||
|
|
||||||
uploadId?: string
|
uploadId?: string
|
||||||
name?: string
|
name?: string
|
||||||
mime?: string
|
mime?: string
|
||||||
|
@ -331,7 +364,11 @@ export class UploadStream extends Writable {
|
||||||
|
|
||||||
async _write(data: Buffer, encoding: string, callback: () => void) {
|
async _write(data: Buffer, encoding: string, callback: () => void) {
|
||||||
console.log("Write to stream attempted")
|
console.log("Write to stream attempted")
|
||||||
if (this.filled + data.byteLength > (this.files.config.maxDiscordFileSize*this.files.config.maxDiscordFiles))
|
if (
|
||||||
|
this.filled + data.byteLength >
|
||||||
|
this.files.config.maxDiscordFileSize *
|
||||||
|
this.files.config.maxDiscordFiles
|
||||||
|
)
|
||||||
return this.destroy(new WebError(413, "maximum file size exceeded"))
|
return this.destroy(new WebError(413, "maximum file size exceeded"))
|
||||||
|
|
||||||
this.hash.update(data)
|
this.hash.update(data)
|
||||||
|
@ -343,21 +380,32 @@ export class UploadStream extends Writable {
|
||||||
|
|
||||||
while (position < data.byteLength) {
|
while (position < data.byteLength) {
|
||||||
let capture = Math.min(
|
let capture = Math.min(
|
||||||
((this.files.config.maxDiscordFileSize*10) - (this.filled % (this.files.config.maxDiscordFileSize*10))),
|
this.files.config.maxDiscordFileSize * 10 -
|
||||||
data.byteLength-position
|
(this.filled % (this.files.config.maxDiscordFileSize * 10)),
|
||||||
|
data.byteLength - position
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
`Capturing ${capture} bytes for megachunk, ${
|
||||||
|
data.subarray(position, position + capture).byteLength
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
console.log(`Capturing ${capture} bytes for megachunk, ${data.subarray(position, position + capture).byteLength}`)
|
|
||||||
if (!this.current) await this.getNextStream()
|
if (!this.current) await this.getNextStream()
|
||||||
if (!this.current) {
|
if (!this.current) {
|
||||||
this.destroy(new Error("getNextStream called during debounce")); return
|
this.destroy(new Error("getNextStream called during debounce"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
readyForMore = this.current.push( data.subarray(position, position+capture) )
|
readyForMore = this.current.push(
|
||||||
console.log(`pushed ${data.byteLength} byte chunk`);
|
data.subarray(position, position + capture)
|
||||||
position += capture, this.filled += capture
|
)
|
||||||
|
console.log(`pushed ${data.byteLength} byte chunk`)
|
||||||
|
;(position += capture), (this.filled += capture)
|
||||||
|
|
||||||
// message is full, so tell the next run to get a new message
|
// message is full, so tell the next run to get a new message
|
||||||
if (this.filled % (this.files.config.maxDiscordFileSize*10) == 0) {
|
if (
|
||||||
|
this.filled % (this.files.config.maxDiscordFileSize * 10) ==
|
||||||
|
0
|
||||||
|
) {
|
||||||
this.current!.push(null)
|
this.current!.push(null)
|
||||||
this.current = undefined
|
this.current = undefined
|
||||||
}
|
}
|
||||||
|
@ -369,24 +417,27 @@ export class UploadStream extends Writable {
|
||||||
|
|
||||||
async _final(callback: (error?: Error | null | undefined) => void) {
|
async _final(callback: (error?: Error | null | undefined) => void) {
|
||||||
if (this.current) {
|
if (this.current) {
|
||||||
this.current.push(null);
|
this.current.push(null)
|
||||||
// i probably dnt need this but whateverrr :3
|
// i probably dnt need this but whateverrr :3
|
||||||
await new Promise((res,rej) => this.once("debounceReleased", res))
|
await new Promise((res, rej) => this.once("debounceReleased", res))
|
||||||
}
|
}
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
aborted: boolean = false
|
aborted: boolean = false
|
||||||
|
|
||||||
async _destroy(error: Error | null, callback: (err?: Error|null) => void) {
|
async _destroy(
|
||||||
|
error: Error | null,
|
||||||
|
callback: (err?: Error | null) => void
|
||||||
|
) {
|
||||||
this.error = error || undefined
|
this.error = error || undefined
|
||||||
await this.abort()
|
await this.abort()
|
||||||
callback(error)
|
callback(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Cancel & unlock the file. When destroy() is called with a non-WebError, this is automatically called
|
* @description Cancel & unlock the file. When destroy() is called with a non-WebError, this is automatically called
|
||||||
*/
|
*/
|
||||||
async abort() {
|
async abort() {
|
||||||
if (this.aborted) return
|
if (this.aborted) return
|
||||||
this.aborted = true
|
this.aborted = true
|
||||||
|
@ -406,8 +457,13 @@ export class UploadStream extends Writable {
|
||||||
async commit() {
|
async commit() {
|
||||||
if (this.errored) throw this.error
|
if (this.errored) throw this.error
|
||||||
if (!this.writableFinished) {
|
if (!this.writableFinished) {
|
||||||
let err = Error("attempted to commit file when the stream was still unfinished")
|
let err = Error(
|
||||||
if (!this.destroyed) {this.destroy(err)}; throw err
|
"attempted to commit file when the stream was still unfinished"
|
||||||
|
)
|
||||||
|
if (!this.destroyed) {
|
||||||
|
this.destroy(err)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform checks
|
// Perform checks
|
||||||
|
@ -421,7 +477,7 @@ export class UploadStream extends Writable {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.uploadId) this.setUploadId(generateFileId())
|
if (!this.uploadId) this.setUploadId(generateFileId())
|
||||||
|
|
||||||
let ogf = this.files.files[this.uploadId!]
|
let ogf = this.files.files[this.uploadId!]
|
||||||
|
|
||||||
this.files.files[this.uploadId!] = {
|
this.files.files[this.uploadId!] = {
|
||||||
|
@ -430,19 +486,18 @@ export class UploadStream extends Writable {
|
||||||
messageids: this.messages,
|
messageids: this.messages,
|
||||||
owner: this.owner,
|
owner: this.owner,
|
||||||
sizeInBytes: this.filled,
|
sizeInBytes: this.filled,
|
||||||
visibility: ogf ? ogf.visibility
|
visibility: ogf
|
||||||
: (
|
? ogf.visibility
|
||||||
this.owner
|
: this.owner
|
||||||
? Accounts.getFromId(this.owner)?.defaultFileVisibility
|
? Accounts.getFromId(this.owner)?.defaultFileVisibility
|
||||||
: undefined
|
: undefined,
|
||||||
),
|
|
||||||
// so that json.stringify doesnt include tag:undefined
|
// so that json.stringify doesnt include tag:undefined
|
||||||
...((ogf||{}).tag ? {tag:ogf.tag} : {}),
|
...((ogf || {}).tag ? { tag: ogf.tag } : {}),
|
||||||
|
|
||||||
chunkSize: this.files.config.maxDiscordFileSize,
|
chunkSize: this.files.config.maxDiscordFileSize,
|
||||||
|
|
||||||
md5: this.hash.digest("hex"),
|
md5: this.hash.digest("hex"),
|
||||||
lastModified: Date.now()
|
lastModified: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.files.locks[this.uploadId!]
|
delete this.files.locks[this.uploadId!]
|
||||||
|
@ -456,52 +511,72 @@ export class UploadStream extends Writable {
|
||||||
|
|
||||||
setName(name: string) {
|
setName(name: string) {
|
||||||
if (this.name)
|
if (this.name)
|
||||||
return this.destroy( new WebError(400, "duplicate attempt to set filename") )
|
return this.destroy(
|
||||||
|
new WebError(400, "duplicate attempt to set filename")
|
||||||
|
)
|
||||||
if (name.length > 512)
|
if (name.length > 512)
|
||||||
return this.destroy( new WebError(400, "filename can be a maximum of 512 characters") )
|
return this.destroy(
|
||||||
|
new WebError(400, "filename can be a maximum of 512 characters")
|
||||||
this.name = name;
|
)
|
||||||
|
|
||||||
|
this.name = name
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
setType(type: string) {
|
setType(type: string) {
|
||||||
if (this.mime)
|
if (this.mime)
|
||||||
return this.destroy( new WebError(400, "duplicate attempt to set mime type") )
|
return this.destroy(
|
||||||
|
new WebError(400, "duplicate attempt to set mime type")
|
||||||
|
)
|
||||||
if (type.length > 256)
|
if (type.length > 256)
|
||||||
return this.destroy( new WebError(400, "mime type can be a maximum of 256 characters") )
|
return this.destroy(
|
||||||
|
new WebError(
|
||||||
this.mime = type;
|
400,
|
||||||
|
"mime type can be a maximum of 256 characters"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.mime = type
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadId(id: string) {
|
setUploadId(id: string) {
|
||||||
if (this.uploadId)
|
if (this.uploadId)
|
||||||
return this.destroy( new WebError(400, "duplicate attempt to set upload ID") )
|
return this.destroy(
|
||||||
if (!id || id.match(id_check_regex)?.[0] != id
|
new WebError(400, "duplicate attempt to set upload ID")
|
||||||
|| id.length > this.files.config.maxUploadIdLength)
|
)
|
||||||
return this.destroy( new WebError(400, "invalid file ID") )
|
if (
|
||||||
|
!id ||
|
||||||
|
id.match(id_check_regex)?.[0] != id ||
|
||||||
|
id.length > this.files.config.maxUploadIdLength
|
||||||
|
)
|
||||||
|
return this.destroy(new WebError(400, "invalid file ID"))
|
||||||
|
|
||||||
if (this.files.files[id] && this.files.files[id].owner != this.owner)
|
if (this.files.files[id] && this.files.files[id].owner != this.owner)
|
||||||
return this.destroy( new WebError(403, "you don't own this file") )
|
return this.destroy(new WebError(403, "you don't own this file"))
|
||||||
|
|
||||||
if (this.files.locks[id])
|
if (this.files.locks[id])
|
||||||
return this.destroy( new WebError(409, "a file with this ID is already being uploaded") )
|
return this.destroy(
|
||||||
|
new WebError(
|
||||||
|
409,
|
||||||
|
"a file with this ID is already being uploaded"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
this.files.locks[id] = true
|
this.files.locks[id] = true
|
||||||
this.uploadId = id
|
this.uploadId = id
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// merged StreamBuffer helper
|
// merged StreamBuffer helper
|
||||||
|
|
||||||
filled: number = 0
|
filled: number = 0
|
||||||
current?: Readable
|
current?: Readable
|
||||||
messages: string[] = []
|
messages: string[] = []
|
||||||
|
|
||||||
private newmessage_debounce : boolean = true
|
|
||||||
|
|
||||||
private async startMessage(): Promise<Readable | undefined> {
|
|
||||||
|
|
||||||
|
private newmessage_debounce: boolean = true
|
||||||
|
|
||||||
|
private async startMessage(): Promise<Readable | undefined> {
|
||||||
if (!this.newmessage_debounce) return
|
if (!this.newmessage_debounce) return
|
||||||
this.newmessage_debounce = false
|
this.newmessage_debounce = false
|
||||||
|
|
||||||
|
@ -510,24 +585,28 @@ export class UploadStream extends Writable {
|
||||||
let stream = new Readable({
|
let stream = new Readable({
|
||||||
read() {
|
read() {
|
||||||
// this is stupid but it should work
|
// this is stupid but it should work
|
||||||
console.log("Read called; calling on server to execute callback")
|
console.log(
|
||||||
|
"Read called; calling on server to execute callback"
|
||||||
|
)
|
||||||
wrt.emit("exec-callback")
|
wrt.emit("exec-callback")
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
stream.pause()
|
stream.pause()
|
||||||
|
|
||||||
console.log(`Starting a message`)
|
console.log(`Starting a message`)
|
||||||
this.files.api.send(stream).then(message => {
|
this.files.api
|
||||||
this.messages.push(message.id)
|
.send(stream)
|
||||||
console.log(`Sent: ${message.id}`)
|
.then((message) => {
|
||||||
this.newmessage_debounce = true
|
this.messages.push(message.id)
|
||||||
this.emit("debounceReleased")
|
console.log(`Sent: ${message.id}`)
|
||||||
}).catch(e => {
|
this.newmessage_debounce = true
|
||||||
if (!this.errored) this.destroy(e)
|
this.emit("debounceReleased")
|
||||||
})
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (!this.errored) this.destroy(e)
|
||||||
|
})
|
||||||
|
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getNextStream() {
|
private async getNextStream() {
|
||||||
|
@ -536,12 +615,14 @@ export class UploadStream extends Writable {
|
||||||
if (this.current) return this.current
|
if (this.current) return this.current
|
||||||
else if (this.newmessage_debounce) {
|
else if (this.newmessage_debounce) {
|
||||||
// startmessage.... idk
|
// startmessage.... idk
|
||||||
this.current = await this.startMessage();
|
this.current = await this.startMessage()
|
||||||
return this.current
|
return this.current
|
||||||
} else {
|
} else {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log("Waiting for debounce to be released...")
|
console.log("Waiting for debounce to be released...")
|
||||||
this.once("debounceReleased", async () => resolve(await this.getNextStream()))
|
this.once("debounceReleased", async () =>
|
||||||
|
resolve(await this.getNextStream())
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -550,16 +631,16 @@ export class UploadStream extends Writable {
|
||||||
export default class Files {
|
export default class Files {
|
||||||
config: Configuration
|
config: Configuration
|
||||||
api: API
|
api: API
|
||||||
files: { [key: string]: FilePointer } = {}
|
files: Record<string, FilePointer> = Object.create(null) // { [key: string]: FilePointer } = {}
|
||||||
data_directory: string = `${process.cwd()}/.data`
|
data_directory: string = `${process.cwd()}/.data`
|
||||||
|
|
||||||
locks: Record<string, boolean> = {} // I'll, like, do something more proper later
|
locks: Record<string, boolean> = Object.create(null) // I'll, like, do something more proper later
|
||||||
|
|
||||||
constructor(config: Configuration) {
|
constructor(config: Configuration) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.api = new API(process.env.TOKEN!, config)
|
this.api = new API(process.env.TOKEN!, config)
|
||||||
|
|
||||||
readFile(this.data_directory+ "/files.json")
|
readFile(this.data_directory + "/files.json")
|
||||||
.then((buf) => {
|
.then((buf) => {
|
||||||
this.files = JSON.parse(buf.toString() || "{}")
|
this.files = JSON.parse(buf.toString() || "{}")
|
||||||
})
|
})
|
||||||
|
@ -574,7 +655,7 @@ export default class Files {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Saves file database
|
* @description Saves file database
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async write(): Promise<void> {
|
async write(): Promise<void> {
|
||||||
await writeFile(
|
await writeFile(
|
||||||
|
@ -592,7 +673,7 @@ export default class Files {
|
||||||
* @param uploadId Target file's ID
|
* @param uploadId Target file's ID
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async update( uploadId: string ) {
|
async update(uploadId: string) {
|
||||||
let target_file = this.files[uploadId]
|
let target_file = this.files[uploadId]
|
||||||
let attachment_sizes = []
|
let attachment_sizes = []
|
||||||
|
|
||||||
|
@ -604,12 +685,12 @@ export default class Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target_file.sizeInBytes)
|
if (!target_file.sizeInBytes)
|
||||||
target_file.sizeInBytes = attachment_sizes.reduce((a, b) => a + b, 0)
|
target_file.sizeInBytes = attachment_sizes.reduce(
|
||||||
|
(a, b) => a + b,
|
||||||
if (!target_file.chunkSize)
|
0
|
||||||
target_file.chunkSize = attachment_sizes[0]
|
)
|
||||||
|
|
||||||
|
if (!target_file.chunkSize) target_file.chunkSize = attachment_sizes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -624,7 +705,8 @@ export default class Files {
|
||||||
): Promise<ReadStream> {
|
): Promise<ReadStream> {
|
||||||
if (this.files[uploadId]) {
|
if (this.files[uploadId]) {
|
||||||
let file = this.files[uploadId]
|
let file = this.files[uploadId]
|
||||||
if (!file.sizeInBytes || !file.chunkSize) await this.update(uploadId)
|
if (!file.sizeInBytes || !file.chunkSize)
|
||||||
|
await this.update(uploadId)
|
||||||
return new ReadStream(this, file, range)
|
return new ReadStream(this, file, range)
|
||||||
} else {
|
} else {
|
||||||
throw { status: 404, message: "not found" }
|
throw { status: 404, message: "not found" }
|
||||||
|
@ -648,9 +730,15 @@ export default class Files {
|
||||||
}
|
}
|
||||||
delete this.files[uploadId]
|
delete this.files[uploadId]
|
||||||
|
|
||||||
if (!noWrite) this.write().catch((err) => {
|
if (!noWrite)
|
||||||
throw err
|
this.write().catch((err) => {
|
||||||
})
|
throw err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
const config = JSON.parse(
|
||||||
|
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
|
||||||
|
// Also if this were NixOS you can't modify the built folder after build
|
||||||
|
readFileSync(process.cwd() + "/config.json", "utf-8")
|
||||||
|
)
|
||||||
|
export const files_singleton = new Files(config)
|
|
@ -1,7 +1,13 @@
|
||||||
import { createTransport } from "nodemailer"
|
import { createTransport } from "nodemailer"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
import config from "../../../config.json" assert {type:"json"}
|
|
||||||
import { generateFileId } from "./files.js"
|
import { generateFileId } from "./files.js"
|
||||||
|
import { readFile } from "fs/promises"
|
||||||
|
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
|
||||||
|
const config = JSON.parse(
|
||||||
|
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
|
||||||
|
// Also if this were NixOS you can't modify the built folder after build
|
||||||
|
await readFile(process.cwd() + "/config.json", "utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
let mailConfig = config.mail,
|
let mailConfig = config.mail,
|
||||||
transport = createTransport({
|
transport = createTransport({
|
||||||
|
@ -37,25 +43,30 @@ export function sendMail(to: string, subject: string, content: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace CodeMgr {
|
export namespace CodeMgr {
|
||||||
|
export const Intents = ["verifyEmail", "recoverAccount"] as const
|
||||||
|
|
||||||
export const Intents = [
|
export type Intent = (typeof Intents)[number]
|
||||||
"verifyEmail",
|
|
||||||
"recoverAccount"
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type Intent = typeof Intents[number]
|
export function isIntent(intent: string): intent is Intent {
|
||||||
|
return intent in Intents
|
||||||
export function isIntent(intent: string): intent is Intent { return intent in Intents }
|
}
|
||||||
|
|
||||||
export let codes = Object.fromEntries(
|
export let codes = Object.fromEntries(
|
||||||
Intents.map(e => [
|
Intents.map((e) => [
|
||||||
e,
|
e,
|
||||||
{byId: new Map<string, Code>(), byUser: new Map<string, Code[]>()}
|
{
|
||||||
])) as Record<Intent, { byId: Map<string, Code>, byUser: Map<string, Code[]> }>
|
byId: new Map<string, Code>(),
|
||||||
|
byUser: new Map<string, Code[]>(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
) as Record<
|
||||||
|
Intent,
|
||||||
|
{ byId: Map<string, Code>; byUser: Map<string, Code[]> }
|
||||||
|
>
|
||||||
|
|
||||||
// this is stupid whyd i write this
|
// this is stupid whyd i write this
|
||||||
|
|
||||||
export class Code {
|
export class Code {
|
||||||
readonly id: string = generateFileId(12)
|
readonly id: string = generateFileId(12)
|
||||||
readonly for: string
|
readonly for: string
|
||||||
|
|
||||||
|
@ -65,25 +76,30 @@ export namespace CodeMgr {
|
||||||
|
|
||||||
readonly data: any
|
readonly data: any
|
||||||
|
|
||||||
constructor(intent: Intent, forUser: string, data?: any, time: number = 15*60*1000) {
|
constructor(
|
||||||
this.for = forUser;
|
intent: Intent,
|
||||||
|
forUser: string,
|
||||||
|
data?: any,
|
||||||
|
time: number = 15 * 60 * 1000
|
||||||
|
) {
|
||||||
|
this.for = forUser
|
||||||
this.intent = intent
|
this.intent = intent
|
||||||
this.expiryClear = setTimeout(this.terminate.bind(this), time)
|
this.expiryClear = setTimeout(this.terminate.bind(this), time)
|
||||||
this.data = data
|
this.data = data
|
||||||
|
|
||||||
codes[intent].byId.set(this.id, this);
|
codes[intent].byId.set(this.id, this)
|
||||||
|
|
||||||
let byUser = codes[intent].byUser.get(this.for)
|
let byUser = codes[intent].byUser.get(this.for)
|
||||||
if (!byUser) {
|
if (!byUser) {
|
||||||
byUser = []
|
byUser = []
|
||||||
codes[intent].byUser.set(this.for, byUser);
|
codes[intent].byUser.set(this.for, byUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
byUser.push(this)
|
byUser.push(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
terminate() {
|
terminate() {
|
||||||
codes[this.intent].byId.delete(this.id);
|
codes[this.intent].byId.delete(this.id)
|
||||||
let bu = codes[this.intent].byUser.get(this.id)!
|
let bu = codes[this.intent].byUser.get(this.id)!
|
||||||
bu.splice(bu.indexOf(this), 1)
|
bu.splice(bu.indexOf(this), 1)
|
||||||
clearTimeout(this.expiryClear)
|
clearTimeout(this.expiryClear)
|
||||||
|
@ -93,5 +109,4 @@ export namespace CodeMgr {
|
||||||
return forUser === this.for
|
return forUser === this.for
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
|
@ -95,12 +95,14 @@ export const assertAPI = function (
|
||||||
|
|
||||||
// Not really middleware but a utility
|
// Not really middleware but a utility
|
||||||
|
|
||||||
export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
|
export const login = (ctx: Context, account: string) =>
|
||||||
path: "/",
|
setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
|
||||||
sameSite: "Strict",
|
path: "/",
|
||||||
secure: true,
|
sameSite: "Strict",
|
||||||
httpOnly: true
|
secure: true,
|
||||||
})
|
httpOnly: true,
|
||||||
|
maxAge: 34560
|
||||||
|
})
|
||||||
|
|
||||||
type SchemeType = "array" | "object" | "string" | "number" | "boolean"
|
type SchemeType = "array" | "object" | "string" | "number" | "boolean"
|
||||||
|
|
107
src/monofile.tsx
Normal file
107
src/monofile.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import fs from "fs"
|
||||||
|
import { readFile } from "fs/promises"
|
||||||
|
import { files_singleton } from "./lib/files.js"
|
||||||
|
import { fileURLToPath } from "url"
|
||||||
|
import { dirname } from "path"
|
||||||
|
import v0 from "./api/v0/index.js"
|
||||||
|
import v1 from "./api/v1/index.js"
|
||||||
|
import preview_and_verify from "./api/web/index.js"
|
||||||
|
|
||||||
|
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
|
||||||
|
const config = JSON.parse(
|
||||||
|
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
|
||||||
|
// Also if this were NixOS you can't modify the built folder after build
|
||||||
|
fs.readFileSync(process.cwd() + "/config.json", "utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
declare const MONOFILE_VERSION: string // see vite.config.ts
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
// app.get(
|
||||||
|
// "/assets/*",
|
||||||
|
// serveStatic({
|
||||||
|
// rewriteRequestPath: (path) => {
|
||||||
|
// return path.replace("/assets", "/assets")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
// app.get(
|
||||||
|
// "/vite/*",
|
||||||
|
// serveStatic({
|
||||||
|
// rewriteRequestPath: (path) => {
|
||||||
|
// return path.replace("/vite", "/dist/vite")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
// respond to the MOLLER method
|
||||||
|
// get it?
|
||||||
|
// haha...
|
||||||
|
|
||||||
|
app.on(["MOLLER"], "*", async (ctx) => {
|
||||||
|
ctx.header("Content-Type", "image/webp")
|
||||||
|
return ctx.body(await readFile("./assets/moller.png"))
|
||||||
|
})
|
||||||
|
|
||||||
|
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
|
||||||
|
|
||||||
|
// check for ssl, if not redirect
|
||||||
|
if (config.trustProxy) {
|
||||||
|
// app.enable("trust proxy")
|
||||||
|
}
|
||||||
|
if (config.forceSSL) {
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
if (new URL(ctx.req.url).protocol == "http") {
|
||||||
|
return ctx.redirect(
|
||||||
|
`https://${ctx.req.header("host")}${
|
||||||
|
new URL(ctx.req.url).pathname
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/server", (ctx) =>
|
||||||
|
ctx.json({
|
||||||
|
...config,
|
||||||
|
version: MONOFILE_VERSION,
|
||||||
|
files: Object.keys(files.files).length,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// funcs
|
||||||
|
|
||||||
|
// init data
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
if (!fs.existsSync(__dirname + "/../.data/"))
|
||||||
|
fs.mkdirSync(__dirname + "/../.data/")
|
||||||
|
|
||||||
|
// discord
|
||||||
|
let files = files_singleton
|
||||||
|
globalThis.__files = files // Kill me
|
||||||
|
v0(app, files)
|
||||||
|
v1(app, files)
|
||||||
|
preview_and_verify(app, files)
|
||||||
|
|
||||||
|
// moved here to ensure it's matched last
|
||||||
|
app.get("/:fileId", async (ctx) =>
|
||||||
|
app.fetch(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`/api/v1/file/${ctx.req.param("fileId")}`,
|
||||||
|
ctx.req.raw.url
|
||||||
|
).href,
|
||||||
|
ctx.req.raw
|
||||||
|
),
|
||||||
|
ctx.env
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log("This is monofile.")
|
||||||
|
|
||||||
|
export default app
|
19
src/routes/+error.svelte
Normal file
19
src/routes/+error.svelte
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<script>
|
||||||
|
import "../style/error.scss"
|
||||||
|
import { page } from "$app/stores"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="icon" type="image/svg" href="/assets/icons/error.svg" />
|
||||||
|
<link rel="stylesheet" href="/api/v1/account/me/css" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, user-scalable=0"
|
||||||
|
/>
|
||||||
|
<title>{$page.status}</title>
|
||||||
|
<meta name="theme-color" content="rgb(30, 33, 36)" />
|
||||||
|
</svelte:head>
|
||||||
|
<p class="error">
|
||||||
|
<span class="code">{$page.status}</span>
|
||||||
|
{$page.error.message}
|
||||||
|
</p>
|
33
src/routes/+page.svelte
Normal file
33
src/routes/+page.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import Topbar from "./components/Topbar.svelte"
|
||||||
|
import PulldownManager from "./components/PulldownManager.svelte"
|
||||||
|
import UploadWindow from "./components/UploadWindow.svelte"
|
||||||
|
import { pulldownManager } from "./components/stores.js"
|
||||||
|
import "../style/app.scss"
|
||||||
|
let topbar: Topbar
|
||||||
|
|
||||||
|
let pulldown: PulldownManager
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
pulldownManager.set(pulldown)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>monofile</title>
|
||||||
|
|
||||||
|
<meta name="title" content="monofile" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="The open-source Discord-based file sharing service"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="rgb(30, 33, 36)" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Topbar bind:this={topbar} {pulldown} />
|
||||||
|
<div id="appContent">
|
||||||
|
<PulldownManager bind:this={pulldown} />
|
||||||
|
|
||||||
|
<UploadWindow />
|
||||||
|
</div>
|
1
src/routes/+page.ts
Normal file
1
src/routes/+page.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const ssr = false
|
16
src/routes/[...monofile]/+server.ts
Normal file
16
src/routes/[...monofile]/+server.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// This is monofile.
|
||||||
|
import monofile from "../../monofile"
|
||||||
|
|
||||||
|
const hook: import("./$types").RequestHandler = (req) => {
|
||||||
|
console.log("[MONOFILE]", req.request.method, req.url.pathname)
|
||||||
|
return monofile.fetch(req.request) ?? console.log("[MONOFILE]", "Returned undefined!")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = hook
|
||||||
|
export const HEAD = hook
|
||||||
|
export const POST = hook
|
||||||
|
export const PUT = hook
|
||||||
|
export const PATCH = hook
|
||||||
|
export const DELETE = hook
|
||||||
|
export const OPTIONS = hook
|
2
src/routes/api/+layout.server.ts
Normal file
2
src/routes/api/+layout.server.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// TODO do auth here instead of in each route
|
||||||
|
// TODO make sure I don't suffer from https://github.com/sveltejs/kit/issues/6315 - await parent() ?
|
114
src/routes/api/v1/file/[fileId]/+server.ts
Normal file
114
src/routes/api/v1/file/[fileId]/+server.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import * as Accounts from "$lib/accounts"
|
||||||
|
import * as auth from "$lib/auth.js"
|
||||||
|
import { error } from "@sveltejs/kit"
|
||||||
|
import RangeParser from "range-parser"
|
||||||
|
import { files_singleton as files } from "$lib/files"
|
||||||
|
|
||||||
|
const get: import("./$types").RequestHandler = async ({
|
||||||
|
params,
|
||||||
|
cookies,
|
||||||
|
request,
|
||||||
|
url,
|
||||||
|
}) => {
|
||||||
|
const { fileId } = params
|
||||||
|
const token =
|
||||||
|
cookies.get("auth") ??
|
||||||
|
(request.headers.get("authorization")?.startsWith("Bearer ")
|
||||||
|
? request.headers.get("authorization")?.split(" ")[1]
|
||||||
|
: undefined)!
|
||||||
|
const account = Accounts.getFromToken(token)
|
||||||
|
|
||||||
|
const file = files.files[fileId]
|
||||||
|
if (!file) throw error(404, "file not found")
|
||||||
|
const lastModified = new Date(file.lastModified)
|
||||||
|
if (file.visibility == "private") {
|
||||||
|
if (account?.id != file.owner) {
|
||||||
|
throw error(403, "you do not own this file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
auth.getType(token) == "App" &&
|
||||||
|
auth.getPermissions(token)?.includes("private")
|
||||||
|
) {
|
||||||
|
throw error(403, "insufficient permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let range: RangeParser.Range | undefined
|
||||||
|
if (file.chunkSize && request.headers.get("range")) {
|
||||||
|
let ranges = RangeParser(
|
||||||
|
file.sizeInBytes,
|
||||||
|
request.headers.get("range") || ""
|
||||||
|
)
|
||||||
|
if (ranges) {
|
||||||
|
if (typeof ranges == "number")
|
||||||
|
throw error(416, "unsatisfiable ranges")
|
||||||
|
if (ranges.length > 1)
|
||||||
|
throw error(400, "multiple ranges not supported")
|
||||||
|
range = ranges[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream: ReadableStream | undefined
|
||||||
|
if (request.method == "HEAD") {
|
||||||
|
stream = undefined
|
||||||
|
} else {
|
||||||
|
stream = files.readFileStream(fileId, range)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
status: range ? 206 : 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Length": file.sizeInBytes,
|
||||||
|
...(range && {
|
||||||
|
"Content-Length": (range.end - range.start + 1).toString(),
|
||||||
|
"Content-Range": `bytes ${range.start}-${range.end}/${file.sizeInBytes}`,
|
||||||
|
}),
|
||||||
|
"Content-Type": file.mime,
|
||||||
|
ETag: file.md5,
|
||||||
|
"Content-Disposition": `${
|
||||||
|
url.searchParams.get("attachment") == "1"
|
||||||
|
? "attachment"
|
||||||
|
: "inline"
|
||||||
|
}; filename="${encodeURI(file.filename.replaceAll("\n", "\\n"))}"`,
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Content-Security-Policy": "sandbox allow-scripts",
|
||||||
|
// TERRIFYING
|
||||||
|
"Last-Modified":
|
||||||
|
`${
|
||||||
|
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][
|
||||||
|
lastModified.getUTCDay()
|
||||||
|
]
|
||||||
|
}, ${lastModified.getUTCDate()} ` +
|
||||||
|
`${
|
||||||
|
[
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
][lastModified.getUTCMonth()]
|
||||||
|
}` +
|
||||||
|
` ${lastModified.getUTCFullYear()} ${lastModified
|
||||||
|
.getUTCHours()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}` +
|
||||||
|
`:${lastModified
|
||||||
|
.getUTCMinutes()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}:${lastModified
|
||||||
|
.getUTCSeconds()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")} GMT`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = get
|
||||||
|
export const HEAD = get
|
|
@ -137,7 +137,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="uploadWindow">
|
<main>
|
||||||
<h1>
|
<h1>
|
||||||
monofile
|
monofile
|
||||||
{#if notificationPermission === "default"}
|
{#if notificationPermission === "default"}
|
||||||
|
@ -375,4 +375,4 @@
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<div style:height="10px" />
|
<div style:height="10px" />
|
||||||
</div>
|
</main>
|
108
src/routes/components/prompts/OptionPicker.svelte
Normal file
108
src/routes/components/prompts/OptionPicker.svelte
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { fade, slide } from "svelte/transition"
|
||||||
|
|
||||||
|
interface BaseModalOption {
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
id: string | number | symbol | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModalOption =
|
||||||
|
| (BaseModalOption & { inputSettings: { password?: boolean }; id: any })
|
||||||
|
| (BaseModalOption & { description: string })
|
||||||
|
|
||||||
|
type ModalOptions = ModalOption[]
|
||||||
|
type OptionPickerReturns = ({ selected: any } & Record<any, any>) | null
|
||||||
|
let activeModal:
|
||||||
|
| {
|
||||||
|
resolve: (val: OptionPickerReturns) => void
|
||||||
|
title: string
|
||||||
|
modal: ModalOptions
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
let modalResults: Record<string | number | symbol, string> = {}
|
||||||
|
|
||||||
|
export function picker(
|
||||||
|
title: string,
|
||||||
|
mdl: ModalOptions
|
||||||
|
): Promise<OptionPickerReturns> {
|
||||||
|
if (activeModal) forceCancel()
|
||||||
|
|
||||||
|
return new Promise<OptionPickerReturns>((resolve, reject) => {
|
||||||
|
activeModal = {
|
||||||
|
resolve,
|
||||||
|
title,
|
||||||
|
modal: mdl,
|
||||||
|
}
|
||||||
|
|
||||||
|
modalResults = {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forceCancel() {
|
||||||
|
if (activeModal && activeModal.resolve) {
|
||||||
|
activeModal.resolve(null)
|
||||||
|
}
|
||||||
|
activeModal = undefined
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if activeModal}
|
||||||
|
<div class="modalContainer" transition:fade={{ duration: 200 }}>
|
||||||
|
<button class="mdHitbox" on:click|self={forceCancel}></button>
|
||||||
|
<div class="modal" transition:slide={{ duration: 200 }}>
|
||||||
|
<div class="optPicker">
|
||||||
|
<div class="category">
|
||||||
|
<p style:margin-bottom="10px">{activeModal.title}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each activeModal.modal as option (option.id)}
|
||||||
|
{#if "inputSettings" in option}
|
||||||
|
<div class="inp">
|
||||||
|
<img src={option.icon} alt={option.id.toString()} />
|
||||||
|
|
||||||
|
<!-- i have to do this stupidness because of svelte but -->
|
||||||
|
<!-- its reason for blocking this is pretty good sooooo -->
|
||||||
|
|
||||||
|
{#if option.inputSettings.password}
|
||||||
|
<input
|
||||||
|
placeholder={option.name}
|
||||||
|
type="password"
|
||||||
|
bind:value={modalResults[option.id]}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
placeholder={option.name}
|
||||||
|
bind:value={modalResults[option.id]}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
activeModal?.resolve({
|
||||||
|
...modalResults,
|
||||||
|
selected: option.id,
|
||||||
|
})
|
||||||
|
activeModal = undefined
|
||||||
|
modalResults = {}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={option.icon} alt={option.id.toString()} />
|
||||||
|
<p>
|
||||||
|
{option.name}<span
|
||||||
|
><br />{option.description}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<button on:click={forceCancel}>
|
||||||
|
<img src="/assets/icons/delete.svg" alt="cancel" />
|
||||||
|
<p>Cancel</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
435
src/routes/components/prompts/account.ts
Normal file
435
src/routes/components/prompts/account.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
import { fetchAccountData, account, refreshNeeded } from "../stores"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import type OptionPicker from "./OptionPicker.svelte"
|
||||||
|
|
||||||
|
export function deleteAccount(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("What should we do with your files?", [
|
||||||
|
{
|
||||||
|
name: "Delete my files",
|
||||||
|
icon: "/assets/icons/admin/delete_file.svg",
|
||||||
|
description: "Your files will be permanently deleted",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Do nothing",
|
||||||
|
icon: "/assets/icons/file.svg",
|
||||||
|
description: "Your files will not be affected",
|
||||||
|
id: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp) {
|
||||||
|
let deleteFiles = exp.selected
|
||||||
|
|
||||||
|
optPicker
|
||||||
|
.picker(`Enter your username to continue.`, [
|
||||||
|
{
|
||||||
|
name: "Enter your username",
|
||||||
|
icon: "/assets/icons/person.svg",
|
||||||
|
inputSettings: {},
|
||||||
|
id: "username",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Delete account ${
|
||||||
|
deleteFiles ? "& files" : ""
|
||||||
|
}`,
|
||||||
|
icon: "/assets/icons/delete_account.svg",
|
||||||
|
description: `This cannot be undone.`,
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((fin) => {
|
||||||
|
if (fin && fin.selected) {
|
||||||
|
if (fin.username != (get(account) || {}).username) {
|
||||||
|
optPicker.picker(
|
||||||
|
"Incorrect username. Please try again.",
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/auth/delete_account`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
deleteFiles,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userChange(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Change username", [
|
||||||
|
{
|
||||||
|
name: "New username",
|
||||||
|
icon: "/assets/icons/person.svg",
|
||||||
|
id: "username",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update username",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/auth/change_username`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: exp.username,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forgotPassword(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Forgot your password?", [
|
||||||
|
{
|
||||||
|
name: "Username",
|
||||||
|
icon: "/assets/icons/person.svg",
|
||||||
|
id: "user",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/auth/request_emergency_login`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
account: exp.user,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
optPicker.picker(
|
||||||
|
`Please follow the instructions sent to your inbox.`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emailPotentialRemove(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("What would you like to do?", [
|
||||||
|
{
|
||||||
|
name: "Set a new email",
|
||||||
|
icon: "/assets/icons/change_email.svg",
|
||||||
|
description: "",
|
||||||
|
id: "set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Disconnect email",
|
||||||
|
icon: "/assets/icons/disconnect_email.svg",
|
||||||
|
description: "",
|
||||||
|
id: "disconnect",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
switch (exp.selected) {
|
||||||
|
case "set":
|
||||||
|
emailChange(optPicker)
|
||||||
|
break
|
||||||
|
case "disconnect":
|
||||||
|
fetch("/auth/remove_email", { method: "POST" }).then(
|
||||||
|
(response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emailChange(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Change email", [
|
||||||
|
{
|
||||||
|
name: "New email",
|
||||||
|
icon: "/assets/icons/mail.svg",
|
||||||
|
id: "email",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Request email change",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/auth/request_email_change`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: exp.email,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
optPicker.picker(
|
||||||
|
`Please continue to your inbox at ${
|
||||||
|
exp.email.split("@")[1]
|
||||||
|
} and click on the attached link.`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pwdChng(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Change password", [
|
||||||
|
{
|
||||||
|
name: "New password",
|
||||||
|
icon: "/assets/icons/change_password.svg",
|
||||||
|
id: "password",
|
||||||
|
inputSettings: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update password",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "This will log you out of all sessions",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/auth/change_password`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
password: exp.password,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function customcss(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Set custom CSS", [
|
||||||
|
{
|
||||||
|
name: "Enter a file ID",
|
||||||
|
icon: "/assets/icons/file.svg",
|
||||||
|
id: "fileid",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "Refresh to apply changes",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/api/v1/account/customization/css`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
fileId: exp.fileid,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
refreshNeeded.set(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function embedColor(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Set embed color", [
|
||||||
|
{
|
||||||
|
name: "FFFFFF",
|
||||||
|
icon: "/assets/icons/pound.svg",
|
||||||
|
id: "color",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/api/v1/account/customization/embed/color`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
color: exp.color,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function embedSize(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Set embed image size", [
|
||||||
|
{
|
||||||
|
name: "Large",
|
||||||
|
icon: "/assets/icons/image.svg",
|
||||||
|
description: "",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Small",
|
||||||
|
icon: "/assets/icons/small_image.svg",
|
||||||
|
description: "",
|
||||||
|
id: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected !== null) {
|
||||||
|
fetch(`/api/v1/account/customization/embed/size`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
largeImage: exp.selected,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
311
src/routes/components/prompts/admin.ts
Normal file
311
src/routes/components/prompts/admin.ts
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
import { fetchAccountData, fetchFilePointers, account } from "../stores"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import type OptionPicker from "./OptionPicker.svelte"
|
||||||
|
|
||||||
|
export function pwdReset(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Reset password", [
|
||||||
|
{
|
||||||
|
name: "Target user",
|
||||||
|
icon: "/assets/icons/person.svg",
|
||||||
|
id: "target",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New password",
|
||||||
|
icon: "/assets/icons/change_password.svg",
|
||||||
|
id: "password",
|
||||||
|
inputSettings: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update password",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description:
|
||||||
|
"This will log the target user out of all sessions",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/admin/reset`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: exp.target,
|
||||||
|
password: exp.password,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chgOwner(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Transfer file ownership", [
|
||||||
|
{
|
||||||
|
name: "File ID",
|
||||||
|
icon: "/assets/icons/file.svg",
|
||||||
|
id: "file",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New owner",
|
||||||
|
icon: "/assets/icons/person.svg",
|
||||||
|
id: "owner",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Transfer file ownership",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "This will transfer the file to this user",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/admin/transfer`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
owner: exp.owner,
|
||||||
|
target: exp.file,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chgId(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Change file ID", [
|
||||||
|
{
|
||||||
|
name: "Target file",
|
||||||
|
icon: "/assets/icons/file.svg",
|
||||||
|
id: "file",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "New ID",
|
||||||
|
icon: "/assets/icons/admin/change_file_id.svg",
|
||||||
|
id: "new",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "File will not be available at its old ID",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/admin/idchange`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: exp.file,
|
||||||
|
new: exp.new,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delFile(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Delete file", [
|
||||||
|
{
|
||||||
|
name: "File ID",
|
||||||
|
icon: "/assets/icons/file.svg",
|
||||||
|
id: "file",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delete",
|
||||||
|
icon: "/assets/icons/admin/delete_file.svg",
|
||||||
|
description: "This can't be undone",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/admin/delete`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: exp.file,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function elevateUser(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("Elevate user", [
|
||||||
|
{
|
||||||
|
name: "Username",
|
||||||
|
icon: "/assets/icons/person.svg",
|
||||||
|
id: "user",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Elevate to admin",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/admin/elevate`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: exp.user,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// im really lazy so i just stole this from account.js
|
||||||
|
|
||||||
|
export function deleteAccount(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("What should we do with the target account's files?", [
|
||||||
|
{
|
||||||
|
name: "Delete files",
|
||||||
|
icon: "/assets/icons/admin/delete_file.svg",
|
||||||
|
description: "Files will be permanently deleted",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Do nothing",
|
||||||
|
icon: "/assets/icons/file.svg",
|
||||||
|
description: "Files will not be affected",
|
||||||
|
id: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp) {
|
||||||
|
let deleteFiles = exp.selected
|
||||||
|
|
||||||
|
optPicker
|
||||||
|
.picker(
|
||||||
|
`Enter the target account's username to continue.`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "Enter account username",
|
||||||
|
icon: "/assets/icons/person.svg",
|
||||||
|
inputSettings: {},
|
||||||
|
id: "username",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Optional reason",
|
||||||
|
icon: "/assets/icons/more.svg",
|
||||||
|
inputSettings: {},
|
||||||
|
id: "reason",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `Delete account ${
|
||||||
|
deleteFiles ? "& its files" : ""
|
||||||
|
}`,
|
||||||
|
icon: "/assets/icons/delete_account.svg",
|
||||||
|
description: `This cannot be undone.`,
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.then((fin) => {
|
||||||
|
if (fin && fin.selected) {
|
||||||
|
fetch(`/admin/delete_account`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: fin.username,
|
||||||
|
reason: fin.reason,
|
||||||
|
deleteFiles,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
272
src/routes/components/prompts/uploads.ts
Normal file
272
src/routes/components/prompts/uploads.ts
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
import { fetchAccountData, fetchFilePointers, account } from "../stores"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import type OptionPicker from "./OptionPicker.svelte"
|
||||||
|
import type { FilePointer } from "../../../lib/files"
|
||||||
|
|
||||||
|
export let options = {
|
||||||
|
FV: [
|
||||||
|
{
|
||||||
|
name: "Public",
|
||||||
|
icon: "/assets/icons/public.svg",
|
||||||
|
description: "Everyone can view your uploads",
|
||||||
|
id: "public",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Anonymous",
|
||||||
|
icon: "/assets/icons/anonymous.svg",
|
||||||
|
description: "Your username will be hidden",
|
||||||
|
id: "anonymous",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Private",
|
||||||
|
icon: "/assets/icons/private.svg",
|
||||||
|
description: "Nobody but you can view your uploads",
|
||||||
|
id: "private",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
FV2: [
|
||||||
|
{
|
||||||
|
name: "Public",
|
||||||
|
icon: "/assets/icons/public.svg",
|
||||||
|
description: "Everyone can view this file",
|
||||||
|
id: "public",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Anonymous",
|
||||||
|
icon: "/assets/icons/anonymous.svg",
|
||||||
|
description: "Your username will be hidden",
|
||||||
|
id: "anonymous",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Private",
|
||||||
|
icon: "/assets/icons/private.svg",
|
||||||
|
description: "Nobody but you can view this file",
|
||||||
|
id: "private",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
AYS: [
|
||||||
|
{
|
||||||
|
name: "Yes",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dfv(optPicker: OptionPicker) {
|
||||||
|
optPicker.picker("Default file visibility", options.FV).then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/auth/dfv`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
defaultFileVisibility: exp.selected,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get("x-backup-status-message") ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function update_all_files(optPicker: OptionPicker) {
|
||||||
|
optPicker
|
||||||
|
.picker("You sure?", [
|
||||||
|
{
|
||||||
|
name: "Yeah",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: `This will make all of your files ${
|
||||||
|
get(account)?.defaultFileVisibility || "public"
|
||||||
|
}`,
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/files/manage`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: get(account)?.files,
|
||||||
|
action: "changeFileVisibility",
|
||||||
|
|
||||||
|
value: get(account)?.defaultFileVisibility,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileOptions(
|
||||||
|
optPicker: OptionPicker,
|
||||||
|
file: FilePointer & { id: string }
|
||||||
|
) {
|
||||||
|
optPicker
|
||||||
|
.picker(file.filename, [
|
||||||
|
{
|
||||||
|
name: file.tag ? "Remove tag" : "Tag file",
|
||||||
|
icon: `/assets/icons/${file.tag ? "tag_remove" : "tag"}.svg`,
|
||||||
|
description: file.tag || `File has no tag`,
|
||||||
|
id: "tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Change file visibility",
|
||||||
|
icon: `/assets/icons/${file.visibility || "public"}.svg`,
|
||||||
|
description: `File is currently ${file.visibility || "public"}`,
|
||||||
|
id: "changeFileVisibility",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delete file",
|
||||||
|
icon: `/assets/icons/admin/delete_file.svg`,
|
||||||
|
description: ``,
|
||||||
|
id: "delete",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
switch (exp.selected) {
|
||||||
|
case "delete":
|
||||||
|
fetch(`/files/manage`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: [file.id],
|
||||||
|
action: "delete",
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchFilePointers()
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case "changeFileVisibility":
|
||||||
|
optPicker
|
||||||
|
.picker("Set file visibility", options.FV2)
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/files/manage`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: [file.id],
|
||||||
|
action: "changeFileVisibility",
|
||||||
|
|
||||||
|
value: exp.selected,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchFilePointers()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case "tag":
|
||||||
|
if (file.tag) {
|
||||||
|
fetch(`/files/manage`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: [file.id],
|
||||||
|
action: "setTag",
|
||||||
|
}),
|
||||||
|
}).then(fetchFilePointers)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
optPicker
|
||||||
|
.picker("Enter a tag (max 30char)", [
|
||||||
|
{
|
||||||
|
name: "Tag name",
|
||||||
|
icon: "/assets/icons/tag.svg",
|
||||||
|
id: "tag",
|
||||||
|
inputSettings: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
icon: "/assets/icons/update.svg",
|
||||||
|
description: "",
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then((exp) => {
|
||||||
|
if (exp && exp.selected) {
|
||||||
|
fetch(`/files/manage`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
target: [file.id],
|
||||||
|
action: "setTag",
|
||||||
|
|
||||||
|
value: exp.tag || null,
|
||||||
|
}),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status != 200) {
|
||||||
|
optPicker.picker(
|
||||||
|
`${response.status} ${
|
||||||
|
response.headers.get(
|
||||||
|
"x-backup-status-message"
|
||||||
|
) ||
|
||||||
|
response.statusText ||
|
||||||
|
""
|
||||||
|
}`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchFilePointers()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
401
src/routes/components/pulldowns/Accounts.svelte
Normal file
401
src/routes/components/pulldowns/Accounts.svelte
Normal file
|
@ -0,0 +1,401 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Pulldown from "./Pulldown.svelte"
|
||||||
|
import { padding_scaleY } from "../transition/padding_scaleY"
|
||||||
|
import { circIn, circOut } from "svelte/easing"
|
||||||
|
import {
|
||||||
|
account,
|
||||||
|
fetchAccountData,
|
||||||
|
serverStats,
|
||||||
|
refreshNeeded,
|
||||||
|
} from "../stores"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
import OptionPicker from "../prompts/OptionPicker.svelte"
|
||||||
|
import * as accOpts from "../prompts/account"
|
||||||
|
import * as uplOpts from "../prompts/uploads"
|
||||||
|
import * as admOpts from "../prompts/admin"
|
||||||
|
|
||||||
|
let targetAction: "login" | "create"
|
||||||
|
let inProgress: boolean
|
||||||
|
let authError: { status: number; message: string } | undefined
|
||||||
|
|
||||||
|
let pwErr: HTMLDivElement
|
||||||
|
|
||||||
|
let optPicker: OptionPicker
|
||||||
|
|
||||||
|
// lazy
|
||||||
|
|
||||||
|
let username: string
|
||||||
|
let password: string
|
||||||
|
|
||||||
|
let execute = () => {
|
||||||
|
if (inProgress) return
|
||||||
|
|
||||||
|
inProgress = true
|
||||||
|
|
||||||
|
fetch(`/api/v1/session`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
inProgress = false
|
||||||
|
|
||||||
|
if (res.status != 200) {
|
||||||
|
authError = await res.json().catch(() => {
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
message:
|
||||||
|
res.headers.get("x-backup-status-message") ||
|
||||||
|
res.statusText ||
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
;(authError = undefined), (username = ""), (password = "")
|
||||||
|
fetchAccountData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (pwErr && authError) {
|
||||||
|
pwErr.animate(
|
||||||
|
{
|
||||||
|
backgroundColor: ["#885555", "#663333"],
|
||||||
|
easing: "ease-out",
|
||||||
|
},
|
||||||
|
650
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// actual account menu
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Pulldown name="accounts">
|
||||||
|
<OptionPicker bind:this={optPicker} />
|
||||||
|
{#if $account}
|
||||||
|
<div class="loggedIn" transition:fade={{ duration: 200 }}>
|
||||||
|
<h1>
|
||||||
|
Hey there, <span class="monospace">@{$account.username}</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="optPicker">
|
||||||
|
<div class="category">
|
||||||
|
<p>Account</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button on:click={() => accOpts.userChange(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/change_username.svg"
|
||||||
|
alt="change username"
|
||||||
|
/>
|
||||||
|
<p>Change username</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() =>
|
||||||
|
($account?.email
|
||||||
|
? accOpts.emailPotentialRemove
|
||||||
|
: accOpts.emailChange)(optPicker)}
|
||||||
|
>
|
||||||
|
<img src="/assets/icons/mail.svg" alt="change email" />
|
||||||
|
<p>
|
||||||
|
Change email{#if $account.email}<span
|
||||||
|
class="monospaceText"
|
||||||
|
><br />{$account.email}</span
|
||||||
|
>{/if}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => accOpts.pwdChng(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/change_password.svg"
|
||||||
|
alt="change password"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Change password<span
|
||||||
|
><br />You will be logged out of all sessions</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if !$account.admin}
|
||||||
|
<button on:click={() => accOpts.deleteAccount(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/delete_account.svg"
|
||||||
|
alt="delete account"
|
||||||
|
/>
|
||||||
|
<p>Delete account</p>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="category">
|
||||||
|
<p>Uploads</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button on:click={() => uplOpts.dfv(optPicker)}>
|
||||||
|
<img
|
||||||
|
src={`/assets/icons/${$account.defaultFileVisibility || "public"}.svg`}
|
||||||
|
alt={$account.defaultFileVisibility || "public"}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Default file visibility<span
|
||||||
|
><br />Uploads will be
|
||||||
|
<strong
|
||||||
|
>{$account.defaultFileVisibility ||
|
||||||
|
"public"}</strong
|
||||||
|
> by default</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => uplOpts.update_all_files(optPicker)}>
|
||||||
|
<img src="/assets/icons/update.svg" alt="update" />
|
||||||
|
<p>
|
||||||
|
Make all of my files {$account.defaultFileVisibility ||
|
||||||
|
"public"}<span
|
||||||
|
><br />Matches your default file visibility</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="category">
|
||||||
|
<p>Customization</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button on:click={() => accOpts.customcss(optPicker)}>
|
||||||
|
<img src="/assets/icons/paint.svg" alt="customcss" />
|
||||||
|
<p>
|
||||||
|
Set custom CSS<span
|
||||||
|
><br />{@html $account.customCSS
|
||||||
|
? `Using file ID <span class="number">${$account.customCSS}</span>`
|
||||||
|
: "No custom CSS set"}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => accOpts.embedColor(optPicker)}>
|
||||||
|
<img src="/assets/icons/pound.svg" alt="embedColor" />
|
||||||
|
<p>
|
||||||
|
Set custom embed color<span
|
||||||
|
><br />{@html $account?.embed?.color
|
||||||
|
? `Using custom color <span class="number">${$account?.embed?.color}</span>`
|
||||||
|
: ""}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => accOpts.embedSize(optPicker)}>
|
||||||
|
<img src="/assets/icons/image.svg" alt="embedSize" />
|
||||||
|
<p>
|
||||||
|
Set embed image size <span
|
||||||
|
><br />Images currently appear {$account?.embed
|
||||||
|
?.largeImage
|
||||||
|
? `large`
|
||||||
|
: "small"} in embeds</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if $refreshNeeded}
|
||||||
|
<button
|
||||||
|
on:click={() => window.location.reload()}
|
||||||
|
transition:fade={{ duration: 200 }}
|
||||||
|
>
|
||||||
|
<img src="/assets/icons/refresh.svg" alt="refresh" />
|
||||||
|
<p>
|
||||||
|
Refresh<span
|
||||||
|
><br />Changes were made which require a refresh</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="category">
|
||||||
|
<p>Sessions</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() =>
|
||||||
|
fetch(`/api/v1/auth/logout_sessions`, {
|
||||||
|
method: "POST",
|
||||||
|
}).then(() => fetchAccountData())}
|
||||||
|
>
|
||||||
|
<img src="/assets/icons/logout_all.svg" alt="logout_all" />
|
||||||
|
<p>
|
||||||
|
Log out all sessions<span
|
||||||
|
><br />{$account?.sessionCount} session(s) active</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() =>
|
||||||
|
fetch(`/api/v1/session`, { method: "DELETE" }).then(
|
||||||
|
() => fetchAccountData()
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img src="/assets/icons/logout.svg" alt="logout" />
|
||||||
|
<p>
|
||||||
|
Log out<span
|
||||||
|
><br />Session expires {new Date(
|
||||||
|
$account?.sessionExpires
|
||||||
|
).toLocaleDateString()}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if $account.admin}
|
||||||
|
<div class="category">
|
||||||
|
<p>Admin</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button on:click={() => admOpts.deleteAccount(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/delete_account.svg"
|
||||||
|
alt="delete account"
|
||||||
|
/>
|
||||||
|
<p>Delete user account</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => admOpts.pwdReset(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/change_password.svg"
|
||||||
|
alt="change password"
|
||||||
|
/>
|
||||||
|
<p>Change user password</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => admOpts.elevateUser(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/admin/elevate_user.svg"
|
||||||
|
alt="elevate account"
|
||||||
|
/>
|
||||||
|
<p>Elevate account to admin</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => admOpts.chgOwner(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/link.svg"
|
||||||
|
alt="change file owner"
|
||||||
|
/>
|
||||||
|
<p>Change file owner</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => admOpts.chgId(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/admin/change_file_id.svg"
|
||||||
|
alt="change file id"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Change file ID<span
|
||||||
|
><br />Potentially buggy, usage not recommended</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button on:click={() => admOpts.delFile(optPicker)}>
|
||||||
|
<img
|
||||||
|
src="/assets/icons/admin/delete_file.svg"
|
||||||
|
alt="delete file"
|
||||||
|
/>
|
||||||
|
<p>Delete file</p>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<p
|
||||||
|
style="font-size:12px;color:#AAAAAA;text-align:center;"
|
||||||
|
class="monospace"
|
||||||
|
>
|
||||||
|
<br />{$account.id}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="notLoggedIn" transition:fade={{ duration: 200 }}>
|
||||||
|
<div class="container_div">
|
||||||
|
<h1>monofile <span style:color="#999999">accounts</span></h1>
|
||||||
|
<p class="flavor">Gain control of your uploads.</p>
|
||||||
|
|
||||||
|
{#if targetAction}
|
||||||
|
<div
|
||||||
|
class="fields"
|
||||||
|
out:padding_scaleY|local={{ easingFunc: circIn }}
|
||||||
|
in:padding_scaleY|local
|
||||||
|
>
|
||||||
|
{#if !$serverStats?.accounts.registrationEnabled && targetAction == "create"}
|
||||||
|
<div class="pwError">
|
||||||
|
<div style:background-color="#554C33">
|
||||||
|
<p>
|
||||||
|
Account registration has been disabled
|
||||||
|
by this instance's owner
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if authError}
|
||||||
|
<div
|
||||||
|
class="pwError"
|
||||||
|
out:padding_scaleY|local={{
|
||||||
|
easingFunc: circIn,
|
||||||
|
}}
|
||||||
|
in:padding_scaleY|local
|
||||||
|
>
|
||||||
|
<div bind:this={pwErr}>
|
||||||
|
<p>
|
||||||
|
<strong>{authError.status}</strong>
|
||||||
|
{authError.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<input
|
||||||
|
placeholder="username"
|
||||||
|
type="text"
|
||||||
|
bind:value={username}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="password"
|
||||||
|
type="password"
|
||||||
|
bind:value={password}
|
||||||
|
/>
|
||||||
|
<button on:click={execute}
|
||||||
|
>{@html inProgress
|
||||||
|
? "<span class=loader><i>•</i> <i>•</i> <i>•</i></span>"
|
||||||
|
: targetAction == "login"
|
||||||
|
? "Log in"
|
||||||
|
: "Create account"}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
{#if targetAction == "login"}
|
||||||
|
<button
|
||||||
|
class="flavor"
|
||||||
|
on:click={() =>
|
||||||
|
accOpts.forgotPassword(optPicker)}
|
||||||
|
>I forgot my password</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="lgBtnContainer"
|
||||||
|
out:padding_scaleY|local={{ easingFunc: circIn }}
|
||||||
|
in:padding_scaleY|local
|
||||||
|
>
|
||||||
|
<button on:click={() => (targetAction = "login")}
|
||||||
|
>Log in</button
|
||||||
|
>
|
||||||
|
<button on:click={() => (targetAction = "create")}
|
||||||
|
>Sign up</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Pulldown>
|
100
src/routes/components/pulldowns/Files.svelte
Normal file
100
src/routes/components/pulldowns/Files.svelte
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Pulldown from "./Pulldown.svelte"
|
||||||
|
import {
|
||||||
|
account,
|
||||||
|
fetchFilePointers,
|
||||||
|
files,
|
||||||
|
pulldownManager,
|
||||||
|
} from "../stores.js"
|
||||||
|
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
import { flip } from "svelte/animate"
|
||||||
|
import { fileOptions } from "../prompts/uploads"
|
||||||
|
import OptionPicker from "../prompts/OptionPicker.svelte"
|
||||||
|
|
||||||
|
let picker: OptionPicker
|
||||||
|
let query = ""
|
||||||
|
|
||||||
|
fetchFilePointers()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Pulldown name="files">
|
||||||
|
<OptionPicker bind:this={picker} />
|
||||||
|
|
||||||
|
{#if $account?.username}<div class="loggedIn">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={`Search ${$files.length} file(s)`}
|
||||||
|
class="searchBar"
|
||||||
|
bind:value={query}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="fileList">
|
||||||
|
<!-- Probably wildly inefficient but who cares, I just wanna get this over with -->
|
||||||
|
{#each $files.filter((f) => f && (f.filename
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query.toLowerCase()) || f.id
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query.toLowerCase()) || f.tag?.includes(query.toLowerCase()))) as file (file.id)}
|
||||||
|
<div
|
||||||
|
class="flFile"
|
||||||
|
transition:fade={{ duration: 200 }}
|
||||||
|
animate:flip={{ duration: 200 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="hitbox"
|
||||||
|
on:click={() => window.open(`/download/${file.id}`)}
|
||||||
|
></button>
|
||||||
|
<!-- this is bad, but I'm lazy -->
|
||||||
|
<div class="flexCont">
|
||||||
|
<div class="fileInfo">
|
||||||
|
<h2>{file.filename}</h2>
|
||||||
|
<p class="detail">
|
||||||
|
<img
|
||||||
|
src="/assets/icons/{file.visibility ||
|
||||||
|
'public'}.svg"
|
||||||
|
alt={file.visibility || "public"}
|
||||||
|
/>
|
||||||
|
<span class="number">{file.id}</span
|
||||||
|
> — <span class="cd"
|
||||||
|
>{file.mime.split(";")[0]}</span
|
||||||
|
>
|
||||||
|
{#if file.reserved}
|
||||||
|
<br />
|
||||||
|
<img
|
||||||
|
src="/assets/icons/update.svg"
|
||||||
|
alt="uploading"
|
||||||
|
/> Uploading...
|
||||||
|
{/if}
|
||||||
|
{#if file.tag}
|
||||||
|
<br />
|
||||||
|
<img
|
||||||
|
src="/assets/icons/tag.svg"
|
||||||
|
alt="tag"
|
||||||
|
/>
|
||||||
|
<span class="cd">{file.tag}</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="more"
|
||||||
|
on:click={() => fileOptions(picker, file)}
|
||||||
|
>
|
||||||
|
<img src="/assets/icons/more.svg" alt="more" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="notLoggedIn">
|
||||||
|
<div style:height="10px" />
|
||||||
|
<p class="flavor">Log in to view uploads</p>
|
||||||
|
<button on:click={$pulldownManager.openPulldown("account")}
|
||||||
|
>OK</button
|
||||||
|
>
|
||||||
|
<div style:height="14px" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Pulldown>
|
79
src/routes/components/stores.ts
Normal file
79
src/routes/components/stores.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
//import type Pulldown from "./pulldowns/Pulldown.svelte"
|
||||||
|
import type { SvelteComponent } from "svelte"
|
||||||
|
import type { Account } from "../../lib/accounts"
|
||||||
|
import type { FilePointer } from "../../lib/files"
|
||||||
|
|
||||||
|
type cfg = {
|
||||||
|
maxDiscordFiles: number
|
||||||
|
maxDiscordFileSize: number
|
||||||
|
targetChannel: string
|
||||||
|
requestTimeout: number
|
||||||
|
maxUploadIdLength: number
|
||||||
|
accounts: {
|
||||||
|
registrationEnabled: boolean
|
||||||
|
requiredForUpload: boolean
|
||||||
|
}
|
||||||
|
mail: {
|
||||||
|
transport: {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
secure: boolean
|
||||||
|
}
|
||||||
|
send: {
|
||||||
|
from: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trustProxy: boolean
|
||||||
|
forceSSL: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export let refreshNeeded = writable(false)
|
||||||
|
export let pulldownManager = writable<SvelteComponent>()
|
||||||
|
export let account = writable<
|
||||||
|
(Account & { sessionCount: number; sessionExpires: number }) | undefined
|
||||||
|
>()
|
||||||
|
export let serverStats = writable<
|
||||||
|
(cfg & { version: string; files: number }) | undefined
|
||||||
|
>()
|
||||||
|
export let files = writable<(FilePointer & { id: string })[]>([])
|
||||||
|
|
||||||
|
export let fetchAccountData = function () {
|
||||||
|
fetch("/auth/me")
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.status == 200) {
|
||||||
|
account.set(await response.json())
|
||||||
|
} else {
|
||||||
|
account.set(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export let fetchFilePointers = function () {
|
||||||
|
fetch("/files/list", { cache: "no-cache" })
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.status == 200) {
|
||||||
|
files.set(await response.json())
|
||||||
|
} else {
|
||||||
|
files.set([])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export let refresh_stats = () => {
|
||||||
|
fetch("/server")
|
||||||
|
.then(async (data) => {
|
||||||
|
serverStats.set(await data.json())
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAccountData()
|
46
src/routes/download/[fileId]/+page.server.ts
Normal file
46
src/routes/download/[fileId]/+page.server.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import * as Accounts from "$lib/accounts"
|
||||||
|
import { files_singleton as files } from "$lib/files"
|
||||||
|
import { error } from "@sveltejs/kit"
|
||||||
|
|
||||||
|
export const load: import("./$types").PageServerLoad = async ({
|
||||||
|
params,
|
||||||
|
url,
|
||||||
|
cookies,
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const acc = Accounts.getFromToken(cookies.get("auth")!)
|
||||||
|
const { fileId } = params
|
||||||
|
const host = url.host
|
||||||
|
|
||||||
|
const file = files[fileId]
|
||||||
|
if (file) {
|
||||||
|
if (file.visibility == "private" && acc?.id != file.owner) {
|
||||||
|
throw error(403, "you do not own this file")
|
||||||
|
}
|
||||||
|
let fileOwner = file.owner ? Accounts.getFromId(file.owner) : undefined
|
||||||
|
return {
|
||||||
|
embedColor:
|
||||||
|
fileOwner?.embed?.color &&
|
||||||
|
file.visibility != "anonymous" &&
|
||||||
|
request.headers.get("user-agent")?.includes("Discordbot")
|
||||||
|
? `#${fileOwner.embed.color}`
|
||||||
|
: "rgb(30, 33, 36)",
|
||||||
|
mime: file.mime,
|
||||||
|
size: file.sizeInBytes,
|
||||||
|
filename: file.filename,
|
||||||
|
url: `https://${host}/file/${fileId}`,
|
||||||
|
owner:
|
||||||
|
!file.owner || file.visibility == "anonymous"
|
||||||
|
? "Anonymous"
|
||||||
|
: `@${fileOwner?.username || "Deleted User"}`,
|
||||||
|
visibility: file.visibility,
|
||||||
|
largeImage:
|
||||||
|
fileOwner?.embed?.largeImage &&
|
||||||
|
file.visibility != "anonymous" &&
|
||||||
|
file.mime.startsWith("image/"),
|
||||||
|
id: fileId,
|
||||||
|
}
|
||||||
|
} else throw error(404, "file not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
export const csr = false
|
75
src/routes/download/[fileId]/+page.svelte
Normal file
75
src/routes/download/[fileId]/+page.svelte
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<script>
|
||||||
|
import "../../../style/downloads.scss"
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import bytes from "bytes"
|
||||||
|
export let data
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{data.id}</title>
|
||||||
|
|
||||||
|
<meta name="og:site_name" content={data.owner}>
|
||||||
|
<meta name="title" content={data.filename}>
|
||||||
|
<meta name="description" content="{bytes(data.size)} file on monofile {MONOFILE_VERSION}, the Discord-based file sharing service">
|
||||||
|
<meta name="theme-color" content={data.embedColor}>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="./style/downloads.scss"
|
||||||
|
>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="/api/v1/account/me/css"
|
||||||
|
>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/svg"
|
||||||
|
href="/assets/icons/file_icon.svg"
|
||||||
|
>
|
||||||
|
<link rel="canonical" href="{$page.url.origin}/download/{data.id}" />
|
||||||
|
{#if data.mime.startsWith("image/")}
|
||||||
|
{#if data.largeImage}
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
{/if}
|
||||||
|
<meta name="og:image" content="{$page.url.origin}/file/{data.id}">
|
||||||
|
{/if}
|
||||||
|
{#if data.mime.startsWith("video/")}
|
||||||
|
<meta property="og:video:url" content="{$page.url.origin}/cpt/{data.id}/video.{data.mime.split("/")[1] == "quicktime" ? "mov" : data.mime.split("/")[1]}" />
|
||||||
|
<meta property="og:video:secure_url" content="{$page.url.origin}/cpt/{data.id}/video.{data.mime.split("/")[1] == "quicktime" ? "mov" : data.mime.split("/")[1]}" />
|
||||||
|
<meta property="og:type" content="video.other">
|
||||||
|
<!-- honestly probably good enough for now -->
|
||||||
|
<meta property="twitter:image" content="0">
|
||||||
|
<!-- quick lazy fix as a fallback -->
|
||||||
|
<!-- maybe i'll improve this later, but probably not. -->
|
||||||
|
{#if data.size >= 26214400}
|
||||||
|
<meta property="og:video:width" content="1280">
|
||||||
|
<meta property="og:video:height" content="720">
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div id="appContent">
|
||||||
|
<main>
|
||||||
|
<h1>{data.filename}</h1>
|
||||||
|
<p style="color:#999999">
|
||||||
|
<span class="number">{bytes(data.size)}</span> — uploaded by <span class="number">{data.owner}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if data.mime.startsWith("image/")}
|
||||||
|
<div style="min-height:10px"></div><img src="/file/{data.id}" />
|
||||||
|
{:else if data.mime.startsWith("video/")}
|
||||||
|
<div style="min-height:10px"></div><video src="/file/{data.id}" controls></video>
|
||||||
|
{:else if data.mime.startsWith("audio/")}
|
||||||
|
<div style="min-height:10px"></div><audio src="/file/{data.id}" controls></audio>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button style="position:relative;width:100%;top:10px;">
|
||||||
|
<a id="dlbtn" href="/api/v1/file/{data.id}" download={data.filename} style="position:absolute;left:0px;top:0px;height:100%;width:100%;"></a>
|
||||||
|
download
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="min-height:15px" />
|
||||||
|
</main>
|
||||||
|
</div>
|
|
@ -1,136 +0,0 @@
|
||||||
import { serve } from "@hono/node-server"
|
|
||||||
import { serveStatic } from "@hono/node-server/serve-static"
|
|
||||||
import { Hono } from "hono"
|
|
||||||
import fs from "fs"
|
|
||||||
import { readFile } from "fs/promises"
|
|
||||||
import Files from "./lib/files.js"
|
|
||||||
import { getAccount } from "./lib/middleware.js"
|
|
||||||
import APIRouter from "./routes/api.js"
|
|
||||||
import preview from "./routes/api/web/preview.js"
|
|
||||||
import {fileURLToPath} from "url"
|
|
||||||
import {dirname} from "path"
|
|
||||||
import pkg from "../../package.json" assert {type:"json"}
|
|
||||||
import config from "../../config.json" assert {type:"json"}
|
|
||||||
|
|
||||||
const app = new Hono()
|
|
||||||
|
|
||||||
app.get(
|
|
||||||
"/static/assets/*",
|
|
||||||
serveStatic({
|
|
||||||
rewriteRequestPath: (path) => {
|
|
||||||
return path.replace("/static/assets", "/assets")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
app.get(
|
|
||||||
"/static/vite/*",
|
|
||||||
serveStatic({
|
|
||||||
rewriteRequestPath: (path) => {
|
|
||||||
return path.replace("/static/vite", "/dist/static/vite")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// respond to the MOLLER method
|
|
||||||
// get it?
|
|
||||||
// haha...
|
|
||||||
|
|
||||||
app.on(["MOLLER"], "*", async (ctx) => {
|
|
||||||
|
|
||||||
ctx.header("Content-Type", "image/webp")
|
|
||||||
return ctx.body( await readFile("./assets/moller.png") )
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
|
|
||||||
|
|
||||||
// check for ssl, if not redirect
|
|
||||||
if (config.trustProxy) {
|
|
||||||
// app.enable("trust proxy")
|
|
||||||
}
|
|
||||||
if (config.forceSSL) {
|
|
||||||
app.use(async (ctx, next) => {
|
|
||||||
if (new URL(ctx.req.url).protocol == "http") {
|
|
||||||
return ctx.redirect(
|
|
||||||
`https://${ctx.req.header("host")}${
|
|
||||||
new URL(ctx.req.url).pathname
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get("/server", (ctx) =>
|
|
||||||
ctx.json({
|
|
||||||
...config,
|
|
||||||
version: pkg.version,
|
|
||||||
files: Object.keys(files.files).length,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// funcs
|
|
||||||
|
|
||||||
// init data
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
||||||
if (!fs.existsSync(__dirname + "/../.data/"))
|
|
||||||
fs.mkdirSync(__dirname + "/../.data/")
|
|
||||||
|
|
||||||
// discord
|
|
||||||
let files = new Files(config)
|
|
||||||
|
|
||||||
const apiRouter = new APIRouter(files)
|
|
||||||
apiRouter.loadAPIMethods().then(() => {
|
|
||||||
app.route("/", apiRouter.root)
|
|
||||||
console.log("API OK!")
|
|
||||||
|
|
||||||
// moved here to ensure it's matched last
|
|
||||||
app.get("/:fileId", async (ctx) =>
|
|
||||||
app.fetch(
|
|
||||||
new Request(
|
|
||||||
(new URL(
|
|
||||||
`/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href,
|
|
||||||
ctx.req.raw
|
|
||||||
),
|
|
||||||
ctx.env
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// listen on 3000 or MONOFILE_PORT
|
|
||||||
// moved here to prevent a crash if someone manages to access monofile before api routes are mounted
|
|
||||||
|
|
||||||
serve(
|
|
||||||
{
|
|
||||||
fetch: app.fetch,
|
|
||||||
port: Number(process.env.MONOFILE_PORT || 3000),
|
|
||||||
serverOptions: {
|
|
||||||
//@ts-ignore
|
|
||||||
requestTimeout: config.requestTimeout
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(info) => {
|
|
||||||
console.log("Web OK!", info.port, info.address)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// index, clone
|
|
||||||
|
|
||||||
app.get("/", async (ctx) =>
|
|
||||||
ctx.html(
|
|
||||||
await fs.promises.readFile(process.cwd() + "/dist/index.html", "utf-8")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
routes should be in this order:
|
|
||||||
|
|
||||||
index
|
|
||||||
api
|
|
||||||
dl pages
|
|
||||||
file serving
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default app
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { readFile } from "fs/promises"
|
|
||||||
import type { Context } from "hono"
|
|
||||||
import type { StatusCode } from "hono/utils/http-status"
|
|
||||||
|
|
||||||
let errorPage: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Serves an error as a response to a request with an error page attached
|
|
||||||
* @param ctx Express response object
|
|
||||||
* @param code Error code
|
|
||||||
* @param reason Error reason
|
|
||||||
*/
|
|
||||||
export default async function ServeError(
|
|
||||||
ctx: Context,
|
|
||||||
code: number,
|
|
||||||
reason: string
|
|
||||||
) {
|
|
||||||
// fetch error page if not cached
|
|
||||||
errorPage ??= (
|
|
||||||
(await readFile(`${process.cwd()}/dist/error.html`).catch((err) =>
|
|
||||||
console.error(err)
|
|
||||||
)) ?? "<pre>$code $text</pre>"
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
|
|
||||||
// serve error
|
|
||||||
return ctx.req.header("accept")?.includes("text/html") ? ctx.html(
|
|
||||||
errorPage
|
|
||||||
.replaceAll("$code", code.toString())
|
|
||||||
.replaceAll("$text", reason),
|
|
||||||
code as StatusCode/*,
|
|
||||||
{
|
|
||||||
"x-backup-status-message": reason, // glitch default nginx configuration
|
|
||||||
}*/
|
|
||||||
) : ctx.text(reason, code as StatusCode)
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import { Hono } from "hono"
|
|
||||||
import { readFile, readdir } from "fs/promises"
|
|
||||||
import Files from "../lib/files.js"
|
|
||||||
import {fileURLToPath} from "url"
|
|
||||||
import {dirname} from "path"
|
|
||||||
|
|
||||||
const APIDirectory = dirname(fileURLToPath(import.meta.url)) + "/api"
|
|
||||||
|
|
||||||
interface APIMount {
|
|
||||||
file: string
|
|
||||||
to: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIMountResolvable = string | APIMount
|
|
||||||
|
|
||||||
interface APIDefinition {
|
|
||||||
name: string
|
|
||||||
baseURL: string
|
|
||||||
mount: APIMountResolvable[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMount(mount: APIMountResolvable): APIMount {
|
|
||||||
return typeof mount == "string" ? { file: mount, to: "/" + mount } : mount
|
|
||||||
}
|
|
||||||
|
|
||||||
class APIVersion {
|
|
||||||
readonly definition: APIDefinition
|
|
||||||
readonly apiPath: string
|
|
||||||
readonly apiRoot: Hono
|
|
||||||
readonly root: Hono = new Hono()
|
|
||||||
readonly files: Files
|
|
||||||
|
|
||||||
constructor(definition: APIDefinition, files: Files, apiRoot: Hono) {
|
|
||||||
this.definition = definition
|
|
||||||
this.apiPath = APIDirectory + "/" + definition.name
|
|
||||||
this.files = files
|
|
||||||
this.apiRoot = apiRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
for (let _mount of this.definition.mount) {
|
|
||||||
let mount = resolveMount(_mount);
|
|
||||||
// no idea if there's a better way to do this but this is all i can think of
|
|
||||||
let { default: route } = await import(`${this.apiPath}/${mount.file}.js`) as { default: (files: Files, apiRoot: Hono) => Hono }
|
|
||||||
|
|
||||||
this.root.route(mount.to, route(this.files, this.apiRoot))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class APIRouter {
|
|
||||||
readonly files: Files
|
|
||||||
readonly root: Hono = new Hono()
|
|
||||||
|
|
||||||
constructor(files: Files) {
|
|
||||||
this.files = files
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Mounts an APIDefinition to the APIRouter.
|
|
||||||
* @param definition Definition to mount.
|
|
||||||
*/
|
|
||||||
|
|
||||||
private async mount(definition: APIDefinition) {
|
|
||||||
console.log(`mounting APIDefinition ${definition.name}`)
|
|
||||||
|
|
||||||
let def = new APIVersion(definition, this.files, this.root)
|
|
||||||
await def.load()
|
|
||||||
|
|
||||||
this.root.route(
|
|
||||||
definition.baseURL,
|
|
||||||
def.root
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadAPIMethods() {
|
|
||||||
let files = await readdir(APIDirectory)
|
|
||||||
for (let version of files) {
|
|
||||||
let def = JSON.parse(
|
|
||||||
(
|
|
||||||
await readFile(
|
|
||||||
`${process.cwd()}/src/server/routes/api/${version}/api.json`
|
|
||||||
)
|
|
||||||
).toString()
|
|
||||||
) as APIDefinition
|
|
||||||
await this.mount(def)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"name": "v0",
|
|
||||||
"baseURL": "/",
|
|
||||||
"mount": [
|
|
||||||
{ "file": "primaryApi", "to": "/" },
|
|
||||||
{ "file": "adminRoutes", "to": "/admin" },
|
|
||||||
{ "file": "authRoutes", "to": "/auth" },
|
|
||||||
{ "file": "fileApiRoutes", "to": "/files" }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { Hono } from "hono"
|
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
|
||||||
import * as auth from "../../../lib/auth.js"
|
|
||||||
import RangeParser, { type Range } from "range-parser"
|
|
||||||
import ServeError from "../../../lib/errors.js"
|
|
||||||
import Files, { WebError } from "../../../lib/files.js"
|
|
||||||
import { getAccount, requiresPermissions } from "../../../lib/middleware.js"
|
|
||||||
import {Readable} from "node:stream"
|
|
||||||
import type {ReadableStream as StreamWebReadable} from "node:stream/web"
|
|
||||||
import formidable from "formidable"
|
|
||||||
import { HttpBindings } from "@hono/node-server"
|
|
||||||
import pkg from "../../../../../package.json" assert {type: "json"}
|
|
||||||
import { type StatusCode } from "hono/utils/http-status"
|
|
||||||
export let primaryApi = new Hono<{
|
|
||||||
Variables: {
|
|
||||||
account: Accounts.Account
|
|
||||||
},
|
|
||||||
Bindings: HttpBindings
|
|
||||||
}>()
|
|
||||||
|
|
||||||
primaryApi.all("*", getAccount)
|
|
||||||
|
|
||||||
export default function (files: Files, apiRoot: Hono) {
|
|
||||||
primaryApi.get("/file/:fileId", async (ctx) =>
|
|
||||||
apiRoot.fetch(
|
|
||||||
new Request(
|
|
||||||
(new URL(
|
|
||||||
`/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href,
|
|
||||||
ctx.req.raw
|
|
||||||
),
|
|
||||||
ctx.env
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
primaryApi.post("/upload", async (ctx) =>
|
|
||||||
apiRoot.fetch(
|
|
||||||
new Request(
|
|
||||||
(new URL(
|
|
||||||
`/api/v1/file`, ctx.req.raw.url)).href,
|
|
||||||
{
|
|
||||||
...ctx.req.raw,
|
|
||||||
method: "PUT"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
ctx.env
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return primaryApi
|
|
||||||
}
|
|
|
@ -1,384 +0,0 @@
|
||||||
// Modules
|
|
||||||
|
|
||||||
|
|
||||||
import { type Context, Hono } from "hono"
|
|
||||||
import { getCookie, setCookie } from "hono/cookie"
|
|
||||||
|
|
||||||
// Libs
|
|
||||||
|
|
||||||
import Files, { id_check_regex } from "../../../lib/files.js"
|
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
|
||||||
import * as auth from "../../../lib/auth.js"
|
|
||||||
import {
|
|
||||||
assertAPI,
|
|
||||||
getAccount,
|
|
||||||
login,
|
|
||||||
noAPIAccess,
|
|
||||||
requiresAccount,
|
|
||||||
requiresPermissions,
|
|
||||||
} from "../../../lib/middleware.js"
|
|
||||||
import ServeError from "../../../lib/errors.js"
|
|
||||||
import { CodeMgr, sendMail } from "../../../lib/mail.js"
|
|
||||||
|
|
||||||
import Configuration from "../../../../../config.json" assert {type:"json"}
|
|
||||||
|
|
||||||
const router = new Hono<{
|
|
||||||
Variables: {
|
|
||||||
account: Accounts.Account,
|
|
||||||
target: Accounts.Account
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
type UserUpdateParameters = Partial<Omit<Accounts.Account, "password"> & { password: string, currentPassword?: string }>
|
|
||||||
type Message = [200 | 400 | 401 | 403 | 429 | 501, string]
|
|
||||||
|
|
||||||
// there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
|
|
||||||
// @Jack5079 make typings better if possible
|
|
||||||
|
|
||||||
type Validator<T extends keyof Partial<Accounts.Account>, ValueNotNull extends boolean> =
|
|
||||||
/**
|
|
||||||
* @param actor The account performing this action
|
|
||||||
* @param target The target account for this action
|
|
||||||
* @param params Changes being patched in by the user
|
|
||||||
*/
|
|
||||||
(actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & (ValueNotNull extends true ? {
|
|
||||||
[K in keyof Pick<UserUpdateParameters, T>]-? : UserUpdateParameters[K]
|
|
||||||
} : {}), ctx: Context) => Accounts.Account[T] | Message
|
|
||||||
|
|
||||||
// this type is so stupid stg
|
|
||||||
type ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> = {
|
|
||||||
acceptsNull: true,
|
|
||||||
validator: Validator<T, false>
|
|
||||||
} | {
|
|
||||||
acceptsNull?: false,
|
|
||||||
validator: Validator<T, true>
|
|
||||||
}
|
|
||||||
|
|
||||||
const validators: {
|
|
||||||
[T in keyof Partial<Accounts.Account>]:
|
|
||||||
Validator<T, true> | ValidatorWithSettings<T>
|
|
||||||
} = {
|
|
||||||
defaultFileVisibility(actor, target, params) {
|
|
||||||
if (["public", "private", "anonymous"].includes(params.defaultFileVisibility))
|
|
||||||
return params.defaultFileVisibility
|
|
||||||
else return [400, "invalid file visibility"]
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
acceptsNull: true,
|
|
||||||
validator: (actor, target, params, ctx) => {
|
|
||||||
if (!params.currentPassword // actor on purpose here to allow admins
|
|
||||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
|
|
||||||
return [401, "current password incorrect"]
|
|
||||||
|
|
||||||
if (!params.email) {
|
|
||||||
if (target.email) {
|
|
||||||
sendMail(
|
|
||||||
target.email,
|
|
||||||
`Email disconnected`,
|
|
||||||
`<b>Hello there!</b> Your email address (<span code>${target.email}</span>) has been disconnected from the monofile account <span username>${target.username}</span>. Thank you for using monofile.`
|
|
||||||
).catch()
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof params.email !== "string") return [400, "email must be string"]
|
|
||||||
if (actor.admin)
|
|
||||||
return params.email
|
|
||||||
|
|
||||||
// send verification email
|
|
||||||
|
|
||||||
if ((CodeMgr.codes.verifyEmail.byUser.get(target.id)?.length || 0) >= 2) return [429, "you have too many active codes"]
|
|
||||||
|
|
||||||
let code = new CodeMgr.Code("verifyEmail", target.id, params.email)
|
|
||||||
|
|
||||||
sendMail(
|
|
||||||
params.email,
|
|
||||||
`Hey there, ${target.username} - let's connect your email`,
|
|
||||||
`<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${
|
|
||||||
params.email.split("@")[0]
|
|
||||||
}<span style="opacity:0.5">@${
|
|
||||||
params.email.split("@")[1]
|
|
||||||
}</span></span>, to your account, <span username>${
|
|
||||||
target.username
|
|
||||||
}</span>. If you would like to continue, please <a href="https://${ctx.req.header(
|
|
||||||
"Host"
|
|
||||||
)}/go/verify/${code.id}"><span code>click here</span></a>, or go to https://${ctx.req.header(
|
|
||||||
"Host"
|
|
||||||
)}/go/verify/${code.id}.`
|
|
||||||
)
|
|
||||||
|
|
||||||
return [200, "please check your inbox"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
password(actor, target, params) {
|
|
||||||
if (
|
|
||||||
!params.currentPassword // actor on purpose here to allow admins
|
|
||||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))
|
|
||||||
) return [401, "current password incorrect"]
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof params.password != "string"
|
|
||||||
|| params.password.length < 8
|
|
||||||
) return [400, "password must be 8 characters or longer"]
|
|
||||||
|
|
||||||
if (target.email) {
|
|
||||||
sendMail(
|
|
||||||
target.email,
|
|
||||||
`Your login details have been updated`,
|
|
||||||
`<b>Hello there!</b> Your password on your account, <span username>${target.username}</span>, has been updated`
|
|
||||||
+ `${actor != target ? ` by <span username>${actor.username}</span>` : ""}. `
|
|
||||||
+ `Please update your saved login details accordingly.`
|
|
||||||
).catch()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Accounts.password.hash(params.password)
|
|
||||||
|
|
||||||
},
|
|
||||||
username(actor, target, params) {
|
|
||||||
if (!params.currentPassword // actor on purpose here to allow admins
|
|
||||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
|
|
||||||
return [401, "current password incorrect"]
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof params.username != "string"
|
|
||||||
|| params.username.length < 3
|
|
||||||
|| params.username.length > 20
|
|
||||||
) return [400, "username must be between 3 and 20 characters in length"]
|
|
||||||
|
|
||||||
if (Accounts.getFromUsername(params.username))
|
|
||||||
return [400, "account with this username already exists"]
|
|
||||||
|
|
||||||
if ((params.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != params.username)
|
|
||||||
return [400, "username has invalid characters"]
|
|
||||||
|
|
||||||
if (target.email) {
|
|
||||||
sendMail(
|
|
||||||
target.email,
|
|
||||||
`Your login details have been updated`,
|
|
||||||
`<b>Hello there!</b> Your username on your account, <span username>${target.username}</span>, has been updated`
|
|
||||||
+ `${actor != target ? ` by <span username>${actor.username}</span>` : ""} to <span username>${params.username}</span>. `
|
|
||||||
+ `Please update your saved login details accordingly.`
|
|
||||||
).catch()
|
|
||||||
}
|
|
||||||
|
|
||||||
return params.username
|
|
||||||
|
|
||||||
},
|
|
||||||
customCSS: {
|
|
||||||
acceptsNull: true,
|
|
||||||
validator: (actor, target, params) => {
|
|
||||||
if (
|
|
||||||
!params.customCSS ||
|
|
||||||
(params.customCSS.match(id_check_regex)?.[0] == params.customCSS &&
|
|
||||||
params.customCSS.length <= Configuration.maxUploadIdLength)
|
|
||||||
) return params.customCSS
|
|
||||||
else return [400, "bad file id"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
embed(actor, target, params) {
|
|
||||||
if (typeof params.embed !== "object") return [400, "must use an object for embed"]
|
|
||||||
if (params.embed.color === undefined) {
|
|
||||||
params.embed.color = target.embed?.color
|
|
||||||
} else if (!((params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
|
|
||||||
params.embed.color.toLowerCase() &&
|
|
||||||
params.embed.color.length == 6) || params.embed.color == null)) return [400, "bad embed color"]
|
|
||||||
|
|
||||||
|
|
||||||
if (params.embed.largeImage === undefined) {
|
|
||||||
params.embed.largeImage = target.embed?.largeImage
|
|
||||||
} else params.embed.largeImage = Boolean(params.embed.largeImage)
|
|
||||||
|
|
||||||
return params.embed
|
|
||||||
},
|
|
||||||
admin(actor, target, params) {
|
|
||||||
if (actor.admin && !target.admin) return params.admin
|
|
||||||
else if (!actor.admin) return [400, "cannot promote yourself"]
|
|
||||||
else return [400, "cannot demote an admin"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
router.use(getAccount)
|
|
||||||
router.all("/:user", async (ctx, next) => {
|
|
||||||
let acc =
|
|
||||||
ctx.req.param("user") == "me"
|
|
||||||
? ctx.get("account")
|
|
||||||
: (
|
|
||||||
ctx.req.param("user").startsWith("@")
|
|
||||||
? Accounts.getFromUsername(ctx.req.param("user").slice(1))
|
|
||||||
: Accounts.getFromId(ctx.req.param("user"))
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
acc != ctx.get("account")
|
|
||||||
&& !ctx.get("account")?.admin
|
|
||||||
) return ServeError(ctx, 403, "you cannot manage this user")
|
|
||||||
if (!acc) return ServeError(ctx, 404, "account does not exist")
|
|
||||||
|
|
||||||
ctx.set("target", acc)
|
|
||||||
|
|
||||||
return next()
|
|
||||||
})
|
|
||||||
|
|
||||||
function isMessage(object: any): object is Message {
|
|
||||||
return Array.isArray(object)
|
|
||||||
&& object.length == 2
|
|
||||||
&& typeof object[0] == "number"
|
|
||||||
&& typeof object[1] == "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function (files: Files) {
|
|
||||||
|
|
||||||
router.post("/", async (ctx) => {
|
|
||||||
const body = await ctx.req.json()
|
|
||||||
if (!Configuration.accounts.registrationEnabled) {
|
|
||||||
return ServeError(ctx, 403, "account registration disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.validate(getCookie(ctx, "auth")!)) {
|
|
||||||
return ServeError(ctx, 400, "you are already logged in")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Accounts.getFromUsername(body.username)) {
|
|
||||||
return ServeError(
|
|
||||||
ctx,
|
|
||||||
400,
|
|
||||||
"account with this username already exists"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body.username.length < 3 || body.username.length > 20) {
|
|
||||||
return ServeError(
|
|
||||||
ctx,
|
|
||||||
400,
|
|
||||||
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
|
||||||
) {
|
|
||||||
return ServeError(ctx, 400, "username contains invalid characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body.password.length < 8) {
|
|
||||||
return ServeError(
|
|
||||||
ctx,
|
|
||||||
400,
|
|
||||||
"password must be 8 characters or longer"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Accounts.create(body.username, body.password)
|
|
||||||
.then((account) => {
|
|
||||||
login(ctx, account)
|
|
||||||
return ctx.text("logged in")
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return ServeError(ctx, 500, "internal server error")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
router.patch(
|
|
||||||
"/:user",
|
|
||||||
requiresAccount,
|
|
||||||
requiresPermissions("manage"),
|
|
||||||
async (ctx) => {
|
|
||||||
const body = await ctx.req.json() as UserUpdateParameters
|
|
||||||
const actor = ctx.get("account")!
|
|
||||||
const target = ctx.get("target")!
|
|
||||||
if (Array.isArray(body))
|
|
||||||
return ServeError(ctx, 400, "invalid body")
|
|
||||||
|
|
||||||
let results: ([keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]|Message)[] =
|
|
||||||
(Object.entries(body)
|
|
||||||
.filter(e => e[0] !== "currentPassword") as [keyof Accounts.Account, UserUpdateParameters[keyof Accounts.Account]][])
|
|
||||||
.map(([x, v]) => {
|
|
||||||
if (!validators[x])
|
|
||||||
return [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message
|
|
||||||
|
|
||||||
let validator =
|
|
||||||
(typeof validators[x] == "object"
|
|
||||||
? validators[x]
|
|
||||||
: {
|
|
||||||
validator: validators[x] as Validator<typeof x, false>,
|
|
||||||
acceptsNull: false
|
|
||||||
}) as ValidatorWithSettings<typeof x>
|
|
||||||
|
|
||||||
if (!validator.acceptsNull && !v)
|
|
||||||
return [400, `the ${x} validator does not accept null values`] as Message
|
|
||||||
|
|
||||||
return [
|
|
||||||
x,
|
|
||||||
validator.validator(actor, target, body as any, ctx)
|
|
||||||
] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]
|
|
||||||
})
|
|
||||||
|
|
||||||
let allMsgs = results.map((v) => {
|
|
||||||
if (isMessage(v))
|
|
||||||
return v
|
|
||||||
target[v[0]] = v[1] as never // lol
|
|
||||||
return [200, "OK"] as Message
|
|
||||||
})
|
|
||||||
|
|
||||||
await Accounts.save()
|
|
||||||
|
|
||||||
if (allMsgs.length == 1)
|
|
||||||
return ctx.text(...allMsgs[0]!.reverse() as [Message[1], Message[0]]) // im sorry
|
|
||||||
else return ctx.json(allMsgs)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
router.delete("/:user", requiresAccount, noAPIAccess, async (ctx) => {
|
|
||||||
let acc = ctx.get("target")
|
|
||||||
|
|
||||||
auth.AuthTokens.filter((e) => e.account == acc?.id).forEach(
|
|
||||||
(token) => {
|
|
||||||
auth.invalidate(token.token)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await Accounts.deleteAccount(acc.id)
|
|
||||||
|
|
||||||
if (acc.email) {
|
|
||||||
await sendMail(
|
|
||||||
acc.email,
|
|
||||||
"Notice of account deletion",
|
|
||||||
`Your account, <span username>${
|
|
||||||
acc.username
|
|
||||||
}</span>, has been removed. Thank you for using monofile.`
|
|
||||||
).catch()
|
|
||||||
return ctx.text("OK")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.text("account deleted")
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get("/:user", requiresAccount, async (ctx) => {
|
|
||||||
let acc = ctx.get("target")
|
|
||||||
let sessionToken = auth.tokenFor(ctx)!
|
|
||||||
|
|
||||||
return ctx.json({
|
|
||||||
...acc,
|
|
||||||
password: undefined,
|
|
||||||
email:
|
|
||||||
auth.getType(sessionToken) == "User" ||
|
|
||||||
auth.getPermissions(sessionToken)?.includes("email")
|
|
||||||
? acc.email
|
|
||||||
: undefined,
|
|
||||||
activeSessions: auth.AuthTokens.filter(
|
|
||||||
(e) =>
|
|
||||||
e.type != "App" &&
|
|
||||||
e.account == acc.id &&
|
|
||||||
(e.expire > Date.now() || !e.expire)
|
|
||||||
).length,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get("/css", async (ctx) => {
|
|
||||||
let acc = ctx.get('account')
|
|
||||||
if (acc?.customCSS)
|
|
||||||
return ctx.redirect(`/file/${acc.customCSS}`)
|
|
||||||
else return ctx.text("")
|
|
||||||
})
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "v1",
|
|
||||||
"baseURL": "/api/v1",
|
|
||||||
"mount": [
|
|
||||||
"account",
|
|
||||||
"session",
|
|
||||||
{
|
|
||||||
"file": "file/index",
|
|
||||||
"to": "/file"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "file/individual",
|
|
||||||
"to": "/file"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
import { Hono } from "hono"
|
|
||||||
import * as Accounts from "../../../../lib/accounts.js"
|
|
||||||
import * as auth from "../../../../lib/auth.js"
|
|
||||||
import RangeParser, { type Range } from "range-parser"
|
|
||||||
import ServeError from "../../../../lib/errors.js"
|
|
||||||
import Files, { WebError } from "../../../../lib/files.js"
|
|
||||||
import { getAccount, requiresPermissions } from "../../../../lib/middleware.js"
|
|
||||||
import {Readable} from "node:stream"
|
|
||||||
import type {ReadableStream as StreamWebReadable} from "node:stream/web"
|
|
||||||
import formidable from "formidable"
|
|
||||||
import { HttpBindings } from "@hono/node-server"
|
|
||||||
import pkg from "../../../../../../package.json" assert {type: "json"}
|
|
||||||
import { type StatusCode } from "hono/utils/http-status"
|
|
||||||
|
|
||||||
const router = new Hono<{
|
|
||||||
Variables: {
|
|
||||||
account: Accounts.Account
|
|
||||||
},
|
|
||||||
Bindings: HttpBindings
|
|
||||||
}>()
|
|
||||||
router.all("*", getAccount)
|
|
||||||
|
|
||||||
export default function(files: Files) {
|
|
||||||
|
|
||||||
router.on(
|
|
||||||
["PUT", "POST"],
|
|
||||||
"/",
|
|
||||||
requiresPermissions("upload"),
|
|
||||||
(ctx) => { return new Promise((resolve,reject) => {
|
|
||||||
ctx.env.incoming.removeAllListeners("data") // remove hono's buffering
|
|
||||||
|
|
||||||
let errEscalated = false
|
|
||||||
function escalate(err:Error) {
|
|
||||||
if (errEscalated) return
|
|
||||||
errEscalated = true
|
|
||||||
console.error(err)
|
|
||||||
|
|
||||||
if ("httpCode" in err)
|
|
||||||
ctx.status(err.httpCode as StatusCode)
|
|
||||||
else if (err instanceof WebError)
|
|
||||||
ctx.status(err.statusCode as StatusCode)
|
|
||||||
else ctx.status(400)
|
|
||||||
resolve(ctx.body(err.message))
|
|
||||||
}
|
|
||||||
|
|
||||||
let acc = ctx.get("account") as Accounts.Account | undefined
|
|
||||||
|
|
||||||
if (!ctx.req.header("Content-Type")?.startsWith("multipart/form-data"))
|
|
||||||
return resolve(ctx.body("must be multipart/form-data", 400))
|
|
||||||
|
|
||||||
if (!ctx.req.raw.body)
|
|
||||||
return resolve(ctx.body("body must be supplied", 400))
|
|
||||||
|
|
||||||
let file = files.createWriteStream(acc?.id)
|
|
||||||
|
|
||||||
file
|
|
||||||
.on("error", escalate)
|
|
||||||
.on("finish", async () => {
|
|
||||||
if (!ctx.env.incoming.readableEnded) await new Promise(res => ctx.env.incoming.once("end", res))
|
|
||||||
file.commit()
|
|
||||||
.then(id => resolve(ctx.body(id!)))
|
|
||||||
.catch(escalate)
|
|
||||||
})
|
|
||||||
|
|
||||||
let parser = formidable({
|
|
||||||
maxFieldsSize: 65536,
|
|
||||||
maxFileSize: files.config.maxDiscordFileSize*files.config.maxDiscordFiles,
|
|
||||||
maxFiles: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
let acceptNewData = true
|
|
||||||
|
|
||||||
parser.onPart = function(part) {
|
|
||||||
if (!part.originalFilename || !part.mimetype) {
|
|
||||||
parser._handlePart(part)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// lol
|
|
||||||
if (part.name == "file") {
|
|
||||||
if (!acceptNewData || file.writableEnded)
|
|
||||||
return part.emit("error", new WebError(400, "cannot set file after previously setting up another upload"))
|
|
||||||
acceptNewData = false
|
|
||||||
file.setName(part.originalFilename || "")
|
|
||||||
file.setType(part.mimetype || "")
|
|
||||||
|
|
||||||
file.on("drain", () => ctx.env.incoming.resume())
|
|
||||||
file.on("error", (err) => part.emit("error", err))
|
|
||||||
|
|
||||||
part.on("data", (data: Buffer) => {
|
|
||||||
if (!file.write(data))
|
|
||||||
ctx.env.incoming.pause()
|
|
||||||
})
|
|
||||||
part.on("end", () => file.end())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.on("field", async (k,v) => {
|
|
||||||
if (k == "uploadId") {
|
|
||||||
if (files.files[v] && ctx.req.method == "POST")
|
|
||||||
return file.destroy(new WebError(409, "file already exists"))
|
|
||||||
file.setUploadId(v)
|
|
||||||
// I'M GONNA KILL MYSELF!!!!
|
|
||||||
} else if (k == "file") {
|
|
||||||
if (!acceptNewData || file.writableEnded)
|
|
||||||
return file.destroy(new WebError(400, "cannot set file after previously setting up another upload"))
|
|
||||||
acceptNewData = false
|
|
||||||
|
|
||||||
let res = await fetch(v, {
|
|
||||||
headers: {
|
|
||||||
"user-agent": `monofile ${pkg.version} (+https://${ctx.req.header("Host")})`
|
|
||||||
}
|
|
||||||
}).catch(escalate)
|
|
||||||
|
|
||||||
if (!res) return
|
|
||||||
|
|
||||||
if (!file
|
|
||||||
.setName(
|
|
||||||
res.headers.get("Content-Disposition")
|
|
||||||
?.match(/filename="(.*)"/)?.[1]
|
|
||||||
|| v.split("/")[
|
|
||||||
v.split("/").length - 1
|
|
||||||
] || "generic"
|
|
||||||
)) return
|
|
||||||
|
|
||||||
if (res.headers.has("Content-Type"))
|
|
||||||
if (!file.setType(res.headers.get("Content-Type")!))
|
|
||||||
return
|
|
||||||
|
|
||||||
if (!res.ok) return file.destroy(new WebError(500, `got ${res.status} ${res.statusText}`))
|
|
||||||
if (!res.body) return file.destroy(new WebError(500, `Internal Server Error`))
|
|
||||||
if (
|
|
||||||
res.headers.has("Content-Length")
|
|
||||||
&& !Number.isNaN(parseInt(res.headers.get("Content-Length")!,10))
|
|
||||||
&& parseInt(res.headers.get("Content-Length")!,10) > files.config.maxDiscordFileSize*files.config.maxDiscordFiles
|
|
||||||
)
|
|
||||||
return file.destroy(new WebError(413, `file reports to be too large`))
|
|
||||||
|
|
||||||
Readable.fromWeb(res.body as StreamWebReadable)
|
|
||||||
.pipe(file)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
parser.parse(ctx.env.incoming)
|
|
||||||
.catch(e => console.error(e))
|
|
||||||
|
|
||||||
parser.on('error', (err) => {
|
|
||||||
escalate(err)
|
|
||||||
if (!file.destroyed) file.destroy(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
})}
|
|
||||||
)
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
import { Hono } from "hono"
|
|
||||||
import * as Accounts from "../../../../lib/accounts.js"
|
|
||||||
import * as auth from "../../../../lib/auth.js"
|
|
||||||
import RangeParser, { type Range } from "range-parser"
|
|
||||||
import ServeError from "../../../../lib/errors.js"
|
|
||||||
import Files, { WebError } from "../../../../lib/files.js"
|
|
||||||
import { getAccount, requiresPermissions } from "../../../../lib/middleware.js"
|
|
||||||
import {Readable} from "node:stream"
|
|
||||||
import type {ReadableStream as StreamWebReadable} from "node:stream/web"
|
|
||||||
import formidable from "formidable"
|
|
||||||
import { HttpBindings } from "@hono/node-server"
|
|
||||||
import pkg from "../../../../../../package.json" assert {type: "json"}
|
|
||||||
import { type StatusCode } from "hono/utils/http-status"
|
|
||||||
|
|
||||||
const router = new Hono<{
|
|
||||||
Variables: {
|
|
||||||
account: Accounts.Account
|
|
||||||
},
|
|
||||||
Bindings: HttpBindings
|
|
||||||
}>()
|
|
||||||
router.all("*", getAccount)
|
|
||||||
|
|
||||||
export default function(files: Files, apiRoot: Hono) {
|
|
||||||
|
|
||||||
router.get("/:id", async (ctx) => {
|
|
||||||
const fileId = ctx.req.param("id")
|
|
||||||
|
|
||||||
let acc = ctx.get("account") as Accounts.Account
|
|
||||||
|
|
||||||
let file = files.files[fileId]
|
|
||||||
ctx.header("Accept-Ranges", "bytes")
|
|
||||||
ctx.header("Access-Control-Allow-Origin", "*")
|
|
||||||
ctx.header("Content-Security-Policy", "sandbox allow-scripts")
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
ctx.header("Content-Disposition", `${ctx.req.query("attachment") == "1" ? "attachment" : "inline"}; filename="${encodeURI(file.filename.replaceAll("\n","\\n"))}"`)
|
|
||||||
ctx.header("ETag", file.md5)
|
|
||||||
if (file.lastModified) {
|
|
||||||
let lm = new Date(file.lastModified)
|
|
||||||
// TERRIFYING
|
|
||||||
ctx.header("Last-Modified",
|
|
||||||
`${['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][lm.getUTCDay()]}, ${lm.getUTCDate()} `
|
|
||||||
+ `${['Jan','Feb','Mar','Apr','May','Jun',"Jul",'Aug','Sep','Oct','Nov','Dec'][lm.getUTCMonth()]}`
|
|
||||||
+ ` ${lm.getUTCFullYear()} ${lm.getUTCHours().toString().padStart(2,"0")}`
|
|
||||||
+ `:${lm.getUTCMinutes().toString().padStart(2,"0")}:${lm.getUTCSeconds().toString().padStart(2,"0")} GMT`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.visibility == "private") {
|
|
||||||
if (acc?.id != file.owner) {
|
|
||||||
return ServeError(ctx, 403, "you do not own this file")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
auth.getType(auth.tokenFor(ctx)!) == "App" &&
|
|
||||||
auth
|
|
||||||
.getPermissions(auth.tokenFor(ctx)!)
|
|
||||||
?.includes("private")
|
|
||||||
) {
|
|
||||||
return ServeError(ctx, 403, "insufficient permissions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let range: Range | undefined
|
|
||||||
|
|
||||||
ctx.header("Content-Type", file.mime)
|
|
||||||
if (file.sizeInBytes) {
|
|
||||||
ctx.header("Content-Length", file.sizeInBytes.toString())
|
|
||||||
|
|
||||||
if (file.chunkSize && ctx.req.header("Range")) {
|
|
||||||
let ranges = RangeParser(file.sizeInBytes, ctx.req.header("Range") || "")
|
|
||||||
|
|
||||||
if (ranges) {
|
|
||||||
if (typeof ranges == "number")
|
|
||||||
return ServeError(ctx, ranges == -1 ? 416 : 400, ranges == -1 ? "unsatisfiable ranges" : "invalid ranges")
|
|
||||||
if (ranges.length > 1) return ServeError(ctx, 400, "multiple ranges not supported")
|
|
||||||
range = ranges[0]
|
|
||||||
|
|
||||||
ctx.status(206)
|
|
||||||
ctx.header(
|
|
||||||
"Content-Length",
|
|
||||||
(range.end - range.start + 1).toString()
|
|
||||||
)
|
|
||||||
ctx.header(
|
|
||||||
"Content-Range",
|
|
||||||
`bytes ${range.start}-${range.end}/${file.sizeInBytes}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.req.method == "HEAD")
|
|
||||||
return ctx.body(null)
|
|
||||||
|
|
||||||
return files
|
|
||||||
.readFileStream(fileId, range)
|
|
||||||
.then(async (stream) => {
|
|
||||||
let rs = new ReadableStream({
|
|
||||||
start(controller) {
|
|
||||||
stream.once("end", () => controller.close())
|
|
||||||
stream.once("error", (err) => controller.error(err))
|
|
||||||
},
|
|
||||||
cancel(reason) {
|
|
||||||
stream.destroy(reason instanceof Error ? reason : new Error(reason))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
stream.pipe(ctx.env.outgoing)
|
|
||||||
return new Response(rs, ctx.body(null))
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
return ServeError(ctx, err.status, err.message)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return ServeError(ctx, 404, "file not found")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
router.on(["PUT", "POST"], "/:id", async (ctx) => {
|
|
||||||
ctx.env.incoming.push(
|
|
||||||
`--${ctx.req.header("content-type")?.match(/boundary=(\S+)/)?.[1]}\r\n`
|
|
||||||
+ `Content-Disposition: form-data; name="uploadId"\r\n\r\n`
|
|
||||||
+ ctx.req.param("id")
|
|
||||||
+ "\r\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
return apiRoot.fetch(
|
|
||||||
new Request(
|
|
||||||
(new URL(
|
|
||||||
`/api/v1/file`, ctx.req.raw.url)).href,
|
|
||||||
ctx.req.raw
|
|
||||||
),
|
|
||||||
ctx.env
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
import fs from "fs/promises"
|
|
||||||
import bytes from "bytes"
|
|
||||||
import ServeError from "../../../lib/errors.js"
|
|
||||||
import * as Accounts from "../../../lib/accounts.js"
|
|
||||||
import type Files from "../../../lib/files.js"
|
|
||||||
import pkg from "../../../../../package.json" assert {type:"json"}
|
|
||||||
import { Hono } from "hono"
|
|
||||||
import { getAccount } from "../../../lib/middleware.js"
|
|
||||||
export let router = new Hono<{
|
|
||||||
Variables: {
|
|
||||||
account: Accounts.Account
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
export default function (files: Files) {
|
|
||||||
router.get("/:fileId", getAccount, async (ctx) => {
|
|
||||||
let acc = ctx.get("account") as Accounts.Account
|
|
||||||
const fileId = ctx.req.param("fileId")
|
|
||||||
const host = ctx.req.header("Host")
|
|
||||||
const file = files.files[fileId]
|
|
||||||
if (file) {
|
|
||||||
if (file.visibility == "private" && acc?.id != file.owner) {
|
|
||||||
return ServeError(ctx, 403, "you do not own this file")
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = await fs
|
|
||||||
.readFile(process.cwd() + "/dist/download.html", "utf8")
|
|
||||||
.catch(() => {
|
|
||||||
throw ctx.status(500)
|
|
||||||
})
|
|
||||||
let fileOwner = file.owner
|
|
||||||
? Accounts.getFromId(file.owner)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return ctx.html(
|
|
||||||
template
|
|
||||||
.replaceAll("$FileId", fileId)
|
|
||||||
.replaceAll("$Version", pkg.version)
|
|
||||||
.replaceAll(
|
|
||||||
"$FileSize",
|
|
||||||
file.sizeInBytes
|
|
||||||
? bytes(file.sizeInBytes)
|
|
||||||
: "[File size unknown]"
|
|
||||||
)
|
|
||||||
.replaceAll(
|
|
||||||
"$FileName",
|
|
||||||
file.filename
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"<!--metaTags-->",
|
|
||||||
(file.mime.startsWith("image/")
|
|
||||||
? `<meta name="og:image" content="https://${host}/file/${fileId}" />`
|
|
||||||
: file.mime.startsWith("video/")
|
|
||||||
? `<meta property="og:video:url" content="https://${host}/cpt/${fileId}/video.${
|
|
||||||
file.mime.split("/")[1] == "quicktime"
|
|
||||||
? "mov"
|
|
||||||
: file.mime.split("/")[1]
|
|
||||||
}" />
|
|
||||||
<meta property="og:video:secure_url" content="https://${host}/cpt/${fileId}/video.${
|
|
||||||
file.mime.split("/")[1] == "quicktime"
|
|
||||||
? "mov"
|
|
||||||
: file.mime.split("/")[1]
|
|
||||||
}" />
|
|
||||||
<meta property="og:type" content="video.other">
|
|
||||||
<!-- honestly probably good enough for now -->
|
|
||||||
<meta property="twitter:image" content="0">` +
|
|
||||||
// quick lazy fix as a fallback
|
|
||||||
// maybe i'll improve this later, but probably not.
|
|
||||||
((file.sizeInBytes || 0) >= 26214400
|
|
||||||
? `
|
|
||||||
<meta property="og:video:width" content="1280">
|
|
||||||
<meta property="og:video:height" content="720">`
|
|
||||||
: "")
|
|
||||||
: "") +
|
|
||||||
(fileOwner?.embed?.largeImage &&
|
|
||||||
file.visibility != "anonymous" &&
|
|
||||||
file.mime.startsWith("image/")
|
|
||||||
? `<meta name="twitter:card" content="summary_large_image">`
|
|
||||||
: "") +
|
|
||||||
`\n<meta name="theme-color" content="${
|
|
||||||
fileOwner?.embed?.color &&
|
|
||||||
file.visibility != "anonymous" &&
|
|
||||||
(ctx.req.header("user-agent") || "").includes(
|
|
||||||
"Discordbot"
|
|
||||||
)
|
|
||||||
? `#${fileOwner.embed.color}`
|
|
||||||
: "rgb(30, 33, 36)"
|
|
||||||
}">`
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"<!--preview-->",
|
|
||||||
file.mime.startsWith("image/")
|
|
||||||
? `<div style="min-height:10px"></div><img src="/file/${fileId}" />`
|
|
||||||
: file.mime.startsWith("video/")
|
|
||||||
? `<div style="min-height:10px"></div><video src="/file/${fileId}" controls></video>`
|
|
||||||
: file.mime.startsWith("audio/")
|
|
||||||
? `<div style="min-height:10px"></div><audio src="/file/${fileId}" controls></audio>`
|
|
||||||
: ""
|
|
||||||
)
|
|
||||||
.replaceAll(
|
|
||||||
"$Uploader",
|
|
||||||
!file.owner || file.visibility == "anonymous"
|
|
||||||
? "Anonymous"
|
|
||||||
: `@${fileOwner?.username || "Deleted User"}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else return ServeError(ctx, 404, "file not found")
|
|
||||||
})
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
{
|
|
||||||
"include":["**/*"],
|
|
||||||
"compilerOptions": {
|
|
||||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
|
||||||
|
|
||||||
/* Projects */
|
|
||||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
|
||||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
|
||||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
|
||||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
|
||||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
|
||||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
|
||||||
|
|
||||||
/* Language and Environment */
|
|
||||||
"target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
|
||||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
|
||||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
|
||||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
|
||||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
|
||||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
|
||||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
|
||||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
|
||||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
|
||||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
|
||||||
|
|
||||||
/* Modules */
|
|
||||||
"module": "nodenext", /* Specify what module code is generated. */
|
|
||||||
//"rootDir": "./src/", /* Specify the root folder within your source files. */
|
|
||||||
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
|
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
|
||||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
|
||||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
||||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
|
||||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
|
||||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
|
||||||
|
|
||||||
/* JavaScript Support */
|
|
||||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
|
||||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
|
||||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
|
||||||
|
|
||||||
/* Emit */
|
|
||||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
|
||||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
|
||||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
|
||||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
|
||||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
|
||||||
"outDir": "../../out/server", /* Specify an output folder for all emitted files. */
|
|
||||||
// "removeComments": true, /* Disable emitting comments. */
|
|
||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
|
||||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
|
||||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
|
||||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
|
||||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
|
||||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
|
||||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
|
||||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
|
||||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
|
||||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
|
||||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
|
||||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
|
||||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
|
||||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
|
||||||
|
|
||||||
/* Interop Constraints */
|
|
||||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
|
||||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
|
||||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
|
||||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
||||||
|
|
||||||
/* Type Checking */
|
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
|
||||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
|
||||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
|
||||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
|
||||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
|
||||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
|
||||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
|
||||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
|
||||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
|
||||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
|
||||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
|
||||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
|
||||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
|
||||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
|
||||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
|
||||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
|
||||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
|
||||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
|
||||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
|
||||||
|
|
||||||
/* Completeness */
|
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{ "path": "../../tsconfig.json" }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -3,20 +3,15 @@
|
||||||
from the server but it's fine for now
|
from the server but it's fine for now
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url("/static/assets/fonts/inconsolata.css");
|
@import url("/assets/fonts/inconsolata.css");
|
||||||
@import url("/static/assets/fonts/source_sans.css");
|
@import url("/assets/fonts/source_sans.css");
|
||||||
@import url("/static/assets/fonts/fira_code.css");
|
@import url("/assets/fonts/fira_code.css");
|
||||||
|
|
||||||
$FallbackFonts:
|
$FallbackFonts: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
-apple-system,
|
|
||||||
system-ui,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
"Segoe UI",
|
|
||||||
Roboto,
|
|
||||||
sans-serif;
|
sans-serif;
|
||||||
|
|
||||||
%normal {
|
%normal {
|
||||||
font-family: "Source Sans Pro", $FallbackFonts
|
font-family: "Source Sans Pro", $FallbackFonts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -25,14 +20,17 @@ $FallbackFonts:
|
||||||
(it's just in case)
|
(it's just in case)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
*:not(span), .normal { @extend %normal; }
|
*:not(span),
|
||||||
|
.normal {
|
||||||
|
@extend %normal;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
for code blocks / terminal
|
for code blocks / terminal
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.monospace {
|
.monospace {
|
||||||
font-family: "Fira Code", monospace
|
font-family: "Fira Code", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -49,37 +47,33 @@ $darkish: rgb(54, 62, 70);
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: rgb(30, 33, 36); // this is here so that
|
background-color: rgb(30, 33, 36); // this is here so that
|
||||||
// pulling down to refresh
|
// pulling down to refresh
|
||||||
// on mobile looks good
|
// on mobile looks good
|
||||||
}
|
}
|
||||||
|
|
||||||
#appContent {
|
#appContent {
|
||||||
background-color: $Background
|
background-color: $Background;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
scrollbars
|
scrollbars
|
||||||
*/
|
*/
|
||||||
|
|
||||||
* {
|
/* nice scrollbars aren't needed on mobile so */
|
||||||
/* nice scrollbars aren't needed on mobile so */
|
@media screen and (min-width: 500px) {
|
||||||
@media screen and (min-width:500px) {
|
::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width:5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background-color:#222222;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color:#333;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color:#373737;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #333;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #373737;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#uploadWindow {
|
main {
|
||||||
#add_new_files {
|
#add_new_files {
|
||||||
background-color:#191919;
|
background-color:#191919;
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
|
@ -112,4 +112,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// should probably start using mixins for thingss like this
|
// should probably start using mixins for thingss like this
|
||||||
|
|
||||||
#uploadWindow {
|
main {
|
||||||
.file {
|
.file {
|
||||||
background-color:#191919;
|
background-color:#191919;
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use "uploader/add_new_files";
|
@use "uploader/add_new_files";
|
||||||
@use "uploader/file";
|
@use "uploader/file";
|
||||||
|
|
||||||
#uploadWindow {
|
main {
|
||||||
position:absolute;
|
position:absolute;
|
||||||
left:50%;
|
left:50%;
|
||||||
top:50%;
|
top:50%;
|
||||||
|
@ -83,4 +83,4 @@
|
||||||
top:10px;
|
top:10px;
|
||||||
padding:0px;
|
padding:0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadWindow {
|
main {
|
||||||
img, video, audio {
|
img, video, audio {
|
||||||
width:100%;
|
width:100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,168 +1,166 @@
|
||||||
#uploadWindow {
|
main {
|
||||||
color: #FFFFFF
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color:#DDDDDD;
|
background-color: #dddddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
#appContent {
|
#appContent {
|
||||||
background: darkgray;
|
background: darkgray;
|
||||||
@media screen and (max-width:500px) {
|
@media screen and (max-width: 500px) {
|
||||||
background:white;
|
background: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadWindow {
|
main {
|
||||||
background: white;
|
background: white;
|
||||||
|
|
||||||
color:black;
|
color: black;
|
||||||
|
|
||||||
h1, p, a {
|
h1,
|
||||||
|
p,
|
||||||
|
a {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color:rgb(153, 153, 153);
|
color: rgb(153, 153, 153);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-weight:600;
|
font-weight: 600;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
text-align:center;
|
text-align: center;
|
||||||
|
|
||||||
@media screen and (max-width:500px) {
|
@media screen and (max-width: 500px) {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > p:nth-of-type(1) {
|
& > p:nth-of-type(1) {
|
||||||
text-align:center;
|
text-align: center;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight:600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color:black !important;
|
color: black !important;
|
||||||
@media screen and (max-width:500px) {
|
@media screen and (max-width: 500px) {
|
||||||
font-size: 21px;
|
font-size: 21px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
cursor:pointer;
|
cursor: pointer;
|
||||||
color:black;
|
color: black;
|
||||||
border:none;
|
border: none;
|
||||||
outline:none;
|
outline: none;
|
||||||
padding:5px;
|
padding: 5px;
|
||||||
background: #AAAAAA;
|
background: #aaaaaa;
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 500px) {
|
||||||
font-size:16px;
|
font-size: 16px;
|
||||||
padding:10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
outline: 1px solid #333333;
|
outline: 1px solid #333333;
|
||||||
color: black;
|
color: black;
|
||||||
background-color:#AAAAAA;
|
background-color: #aaaaaa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > button:nth-last-of-type(1) {
|
& > button:nth-last-of-type(1) {
|
||||||
background-color:#66AAFF;
|
background-color: #66aaff;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color:#66AAFF;
|
background-color: #66aaff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#add_new_files {
|
#add_new_files {
|
||||||
background-color: #AAAAAA66;
|
background-color: #aaaaaa66;
|
||||||
border:1px solid #AAAAAA;
|
border: 1px solid #aaaaaa;
|
||||||
|
|
||||||
#file_add_btns {
|
#file_add_btns {
|
||||||
button, input[type=text] {
|
button,
|
||||||
transition-duration:0s;
|
input[type="text"] {
|
||||||
|
transition-duration: 0s;
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 500px) {
|
||||||
font-size:16px;
|
font-size: 16px;
|
||||||
padding:10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text] {
|
input[type="text"] {
|
||||||
font-family: "Fira Code", monospace;
|
font-family: "Fira Code", monospace;
|
||||||
background-color:#AAAAAA;
|
background-color: #aaaaaa;
|
||||||
color:black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
cursor:pointer;
|
cursor: pointer;
|
||||||
background-color:#AAAAAA;
|
background-color: #aaaaaa;
|
||||||
color: black;
|
color: black;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
flex-basis: 50%;
|
flex-basis: 50%;
|
||||||
transition-duration:0s;
|
transition-duration: 0s;
|
||||||
background-color:#AAAAAA;
|
background-color: #aaaaaa;
|
||||||
color: black;
|
color: black;
|
||||||
outline: 1px solid #333333;
|
outline: 1px solid #333333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileUpload {
|
.fileUpload {
|
||||||
background-color:#AAAAAA;
|
background-color: #aaaaaa;
|
||||||
transition-duration:250ms;
|
transition-duration: 250ms;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transition-duration:0s;
|
transition-duration: 0s;
|
||||||
background-color:#AAAAAA;
|
background-color: #aaaaaa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file {
|
.file {
|
||||||
background-color: #AAAAAA66;
|
background-color: #aaaaaa66;
|
||||||
border: 1px solid #AAAAAA;
|
border: 1px solid #aaaaaa;
|
||||||
|
|
||||||
input[type=text] {
|
input[type="text"] {
|
||||||
font-family: "Fira Code", monospace;
|
font-family: "Fira Code", monospace;
|
||||||
background-color:#AAAAAA;
|
background-color: #aaaaaa;
|
||||||
color:black;
|
color: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
/* nice scrollbars aren't needed on mobile so */
|
||||||
/* nice scrollbars aren't needed on mobile so */
|
@media screen and (min-width: 500px) {
|
||||||
@media screen and (min-width:500px) {
|
::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
&::-webkit-scrollbar {
|
}
|
||||||
width:5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background-color:#AAAAAA;
|
background-color: #aaaaaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color:#DDDDDD;
|
background-color: #dddddd;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color:#FFFFFF;
|
background-color: #ffffff;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#topbar {
|
#topbar {
|
||||||
background-color: #DDDDDD;
|
background-color: #dddddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
.code {
|
.code {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import Topbar from "./elem/Topbar.svelte";
|
|
||||||
import PulldownManager from "./elem/PulldownManager.svelte";
|
|
||||||
import UploadWindow from "./elem/UploadWindow.svelte";
|
|
||||||
import { pulldownManager } from "./elem/stores.js";
|
|
||||||
|
|
||||||
let topbar: Topbar;
|
|
||||||
|
|
||||||
let pulldown: PulldownManager;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
pulldownManager.set(pulldown)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Topbar bind:this={topbar} pulldown={pulldown} />
|
|
||||||
<div id="appContent">
|
|
||||||
<PulldownManager bind:this={pulldown} />
|
|
||||||
|
|
||||||
<UploadWindow/>
|
|
||||||
</div>
|
|
|
@ -1,80 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { fade, slide } from "svelte/transition";
|
|
||||||
|
|
||||||
interface BaseModalOption {
|
|
||||||
name:string,
|
|
||||||
icon:string,
|
|
||||||
id: string | number | symbol | boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModalOption = BaseModalOption & {inputSettings: {password?: boolean}, id: any} | BaseModalOption & { description: string }
|
|
||||||
|
|
||||||
type ModalOptions = ModalOption[]
|
|
||||||
type OptionPickerReturns = {selected: any} & Record<any,any> | null
|
|
||||||
let activeModal: {resolve: (val: OptionPickerReturns) => void, title: string, modal: ModalOptions } | undefined;
|
|
||||||
let modalResults: Record<string | number | symbol, string> = {};
|
|
||||||
|
|
||||||
export function picker(title: string,mdl: ModalOptions): Promise<OptionPickerReturns> {
|
|
||||||
if (activeModal) forceCancel()
|
|
||||||
|
|
||||||
return new Promise<OptionPickerReturns>((resolve,reject) => {
|
|
||||||
activeModal = {
|
|
||||||
resolve,
|
|
||||||
title,
|
|
||||||
modal:mdl
|
|
||||||
}
|
|
||||||
|
|
||||||
modalResults = {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function forceCancel() {
|
|
||||||
if (activeModal && activeModal.resolve) {
|
|
||||||
activeModal.resolve(null)
|
|
||||||
}
|
|
||||||
activeModal = undefined
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if activeModal}
|
|
||||||
<div class="modalContainer" transition:fade={{duration:200}}>
|
|
||||||
<button class="mdHitbox" on:click|self={forceCancel}></button>
|
|
||||||
<div class="modal" transition:slide={{duration:200}}>
|
|
||||||
|
|
||||||
<div class="optPicker">
|
|
||||||
|
|
||||||
<div class="category">
|
|
||||||
<p style:margin-bottom="10px">{activeModal.title}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#each activeModal.modal as option (option.id)}
|
|
||||||
{#if "inputSettings" in option}
|
|
||||||
<div class="inp">
|
|
||||||
<img src={option.icon} alt={option.id.toString()}>
|
|
||||||
|
|
||||||
<!-- i have to do this stupidness because of svelte but -->
|
|
||||||
<!-- its reason for blocking this is pretty good sooooo -->
|
|
||||||
|
|
||||||
{#if option.inputSettings.password}
|
|
||||||
<input placeholder={option.name} type="password" bind:value={modalResults[option.id]}>
|
|
||||||
{:else}
|
|
||||||
<input placeholder={option.name} bind:value={modalResults[option.id]}>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<button on:click={() => {activeModal?.resolve({...modalResults,selected:option.id});activeModal=undefined;modalResults={};}}>
|
|
||||||
<img src={option.icon} alt={option.id.toString()}>
|
|
||||||
<p>{option.name}<span><br />{option.description}</span></p>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<button on:click={forceCancel}>
|
|
||||||
<img src="/static/assets/icons/delete.svg" alt="cancel">
|
|
||||||
<p>Cancel</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
|
@ -1,331 +0,0 @@
|
||||||
import { fetchAccountData, account, refreshNeeded } from "../stores"
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import type OptionPicker from "./OptionPicker.svelte";
|
|
||||||
|
|
||||||
export function deleteAccount(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("What should we do with your files?",[
|
|
||||||
{
|
|
||||||
name: "Delete my files",
|
|
||||||
icon: "/static/assets/icons/admin/delete_file.svg",
|
|
||||||
description: "Your files will be permanently deleted",
|
|
||||||
id: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Do nothing",
|
|
||||||
icon: "/static/assets/icons/file.svg",
|
|
||||||
description: "Your files will not be affected",
|
|
||||||
id: false
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp) {
|
|
||||||
let deleteFiles = exp.selected
|
|
||||||
|
|
||||||
optPicker.picker(`Enter your username to continue.`,[
|
|
||||||
{
|
|
||||||
name: "Enter your username",
|
|
||||||
icon: "/static/assets/icons/person.svg",
|
|
||||||
inputSettings: {},
|
|
||||||
id:"username"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `Delete account ${deleteFiles ? "& files" : ""}`,
|
|
||||||
icon: "/static/assets/icons/delete_account.svg",
|
|
||||||
description: `This cannot be undone.`,
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((fin) => {
|
|
||||||
if (fin && fin.selected) {
|
|
||||||
if (fin.username != (get(account)||{}).username) {
|
|
||||||
optPicker.picker("Incorrect username. Please try again.",[])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(`/auth/delete_account`,{method:"POST", body:JSON.stringify({
|
|
||||||
deleteFiles
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function userChange(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Change username",[
|
|
||||||
{
|
|
||||||
name: "New username",
|
|
||||||
icon: "/static/assets/icons/person.svg",
|
|
||||||
id: "username",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Update username",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/auth/change_username`,{method:"POST", body:JSON.stringify({
|
|
||||||
username:exp.username
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function forgotPassword(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Forgot your password?",[
|
|
||||||
{
|
|
||||||
name: "Username",
|
|
||||||
icon: "/static/assets/icons/person.svg",
|
|
||||||
id: "user",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "OK",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/auth/request_emergency_login`,{method:"POST", body:JSON.stringify({
|
|
||||||
account:exp.user
|
|
||||||
})}).then((response) => {
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
} else {
|
|
||||||
optPicker.picker(`Please follow the instructions sent to your inbox.`,[])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emailPotentialRemove(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("What would you like to do?",[
|
|
||||||
{
|
|
||||||
name: "Set a new email",
|
|
||||||
icon: "/static/assets/icons/change_email.svg",
|
|
||||||
description: "",
|
|
||||||
id: "set"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Disconnect email",
|
|
||||||
icon: "/static/assets/icons/disconnect_email.svg",
|
|
||||||
description: "",
|
|
||||||
id: "disconnect"
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
switch (exp.selected) {
|
|
||||||
case "set":
|
|
||||||
emailChange(optPicker);
|
|
||||||
break
|
|
||||||
case "disconnect":
|
|
||||||
fetch("/auth/remove_email", {method: "POST"}).then((response) => {
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emailChange(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Change email",[
|
|
||||||
{
|
|
||||||
name: "New email",
|
|
||||||
icon: "/static/assets/icons/mail.svg",
|
|
||||||
id: "email",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Request email change",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/auth/request_email_change`,{method:"POST", body:JSON.stringify({
|
|
||||||
email:exp.email
|
|
||||||
})}).then((response) => {
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
} else {
|
|
||||||
optPicker.picker(`Please continue to your inbox at ${exp.email.split("@")[1]} and click on the attached link.`,[])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pwdChng(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Change password",[
|
|
||||||
{
|
|
||||||
name: "New password",
|
|
||||||
icon: "/static/assets/icons/change_password.svg",
|
|
||||||
id: "password",
|
|
||||||
inputSettings: {
|
|
||||||
password: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Update password",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "This will log you out of all sessions",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/auth/change_password`,{method:"POST", body:JSON.stringify({
|
|
||||||
password:exp.password
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function customcss(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Set custom CSS",[
|
|
||||||
{
|
|
||||||
name: "Enter a file ID",
|
|
||||||
icon: "/static/assets/icons/file.svg",
|
|
||||||
id: "fileid",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "OK",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "Refresh to apply changes",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/api/v1/account/customization/css`, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({
|
|
||||||
fileId: exp.fileid,
|
|
||||||
}),
|
|
||||||
}).then((response) => {
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(
|
|
||||||
`${response.status} ${
|
|
||||||
response.headers.get("x-backup-status-message") ||
|
|
||||||
response.statusText ||
|
|
||||||
""
|
|
||||||
}`,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
refreshNeeded.set(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function embedColor(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Set embed color",[
|
|
||||||
{
|
|
||||||
name: "FFFFFF",
|
|
||||||
icon: "/static/assets/icons/pound.svg",
|
|
||||||
id: "color",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "OK",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/api/v1/account/customization/embed/color`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
color: exp.color,
|
|
||||||
}),
|
|
||||||
}).then((response) => {
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(
|
|
||||||
`${response.status} ${
|
|
||||||
response.headers.get("x-backup-status-message") ||
|
|
||||||
response.statusText ||
|
|
||||||
""
|
|
||||||
}`,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function embedSize(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Set embed image size",[
|
|
||||||
{
|
|
||||||
name: "Large",
|
|
||||||
icon: "/static/assets/icons/image.svg",
|
|
||||||
description: "",
|
|
||||||
id: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Small",
|
|
||||||
icon: "/static/assets/icons/small_image.svg",
|
|
||||||
description: "",
|
|
||||||
id: false
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected !== null) {
|
|
||||||
fetch(`/api/v1/account/customization/embed/size`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
largeImage: exp.selected,
|
|
||||||
}),
|
|
||||||
}).then((response) => {
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(
|
|
||||||
`${response.status} ${
|
|
||||||
response.headers.get("x-backup-status-message") ||
|
|
||||||
response.statusText ||
|
|
||||||
""
|
|
||||||
}`,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,231 +0,0 @@
|
||||||
import { fetchAccountData, fetchFilePointers, account } from "../stores"
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import type OptionPicker from "./OptionPicker.svelte";
|
|
||||||
|
|
||||||
export function pwdReset(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Reset password",[
|
|
||||||
{
|
|
||||||
name: "Target user",
|
|
||||||
icon: "/static/assets/icons/person.svg",
|
|
||||||
id: "target",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "New password",
|
|
||||||
icon: "/static/assets/icons/change_password.svg",
|
|
||||||
id: "password",
|
|
||||||
inputSettings: {
|
|
||||||
password: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Update password",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "This will log the target user out of all sessions",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/admin/reset`,{method:"POST", body:JSON.stringify({
|
|
||||||
target: exp.target,
|
|
||||||
password:exp.password
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function chgOwner(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Transfer file ownership",[
|
|
||||||
{
|
|
||||||
name: "File ID",
|
|
||||||
icon: "/static/assets/icons/file.svg",
|
|
||||||
id: "file",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "New owner",
|
|
||||||
icon: "/static/assets/icons/person.svg",
|
|
||||||
id: "owner",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Transfer file ownership",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "This will transfer the file to this user",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/admin/transfer`,{method:"POST", body:JSON.stringify({
|
|
||||||
owner: exp.owner,
|
|
||||||
target: exp.file
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function chgId(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Change file ID",[
|
|
||||||
{
|
|
||||||
name: "Target file",
|
|
||||||
icon: "/static/assets/icons/file.svg",
|
|
||||||
id: "file",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "New ID",
|
|
||||||
icon: "/static/assets/icons/admin/change_file_id.svg",
|
|
||||||
id: "new",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Update",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "File will not be available at its old ID",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/admin/idchange`,{method:"POST", body:JSON.stringify({
|
|
||||||
target: exp.file,
|
|
||||||
new: exp.new
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function delFile(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Delete file",[
|
|
||||||
{
|
|
||||||
name: "File ID",
|
|
||||||
icon: "/static/assets/icons/file.svg",
|
|
||||||
id: "file",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Delete",
|
|
||||||
icon: "/static/assets/icons/admin/delete_file.svg",
|
|
||||||
description: "This can't be undone",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/admin/delete`,{method:"POST", body:JSON.stringify({
|
|
||||||
target: exp.file
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function elevateUser(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Elevate user",[
|
|
||||||
{
|
|
||||||
name: "Username",
|
|
||||||
icon: "/static/assets/icons/person.svg",
|
|
||||||
id: "user",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Elevate to admin",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/admin/elevate`,{method:"POST", body:JSON.stringify({
|
|
||||||
target: exp.user
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// im really lazy so i just stole this from account.js
|
|
||||||
|
|
||||||
export function deleteAccount(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("What should we do with the target account's files?",[
|
|
||||||
{
|
|
||||||
name: "Delete files",
|
|
||||||
icon: "/static/assets/icons/admin/delete_file.svg",
|
|
||||||
description: "Files will be permanently deleted",
|
|
||||||
id: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Do nothing",
|
|
||||||
icon: "/static/assets/icons/file.svg",
|
|
||||||
description: "Files will not be affected",
|
|
||||||
id: false
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp) {
|
|
||||||
let deleteFiles = exp.selected
|
|
||||||
|
|
||||||
optPicker.picker(`Enter the target account's username to continue.`,[
|
|
||||||
{
|
|
||||||
name: "Enter account username",
|
|
||||||
icon: "/static/assets/icons/person.svg",
|
|
||||||
inputSettings: {},
|
|
||||||
id:"username"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Optional reason",
|
|
||||||
icon: "/static/assets/icons/more.svg",
|
|
||||||
inputSettings: {},
|
|
||||||
id:"reason"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `Delete account ${deleteFiles ? "& its files" : ""}`,
|
|
||||||
icon: "/static/assets/icons/delete_account.svg",
|
|
||||||
description: `This cannot be undone.`,
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((fin) => {
|
|
||||||
if (fin && fin.selected) {
|
|
||||||
fetch(`/admin/delete_account`,{method:"POST", body:JSON.stringify({
|
|
||||||
target: fin.username,
|
|
||||||
reason: fin.reason,
|
|
||||||
deleteFiles
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,220 +0,0 @@
|
||||||
import { fetchAccountData, fetchFilePointers, account } from "../stores"
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import type OptionPicker from "./OptionPicker.svelte"
|
|
||||||
import type { FilePointer } from "../../../server/lib/files";
|
|
||||||
|
|
||||||
export let options = {
|
|
||||||
FV: [
|
|
||||||
{
|
|
||||||
name: "Public",
|
|
||||||
icon: "/static/assets/icons/public.svg",
|
|
||||||
description: "Everyone can view your uploads",
|
|
||||||
id: "public"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Anonymous",
|
|
||||||
icon: "/static/assets/icons/anonymous.svg",
|
|
||||||
description: "Your username will be hidden",
|
|
||||||
id: "anonymous"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Private",
|
|
||||||
icon: "/static/assets/icons/private.svg",
|
|
||||||
description: "Nobody but you can view your uploads",
|
|
||||||
id: "private"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
FV2: [
|
|
||||||
{
|
|
||||||
name: "Public",
|
|
||||||
icon: "/static/assets/icons/public.svg",
|
|
||||||
description: "Everyone can view this file",
|
|
||||||
id: "public"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Anonymous",
|
|
||||||
icon: "/static/assets/icons/anonymous.svg",
|
|
||||||
description: "Your username will be hidden",
|
|
||||||
id: "anonymous"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Private",
|
|
||||||
icon: "/static/assets/icons/private.svg",
|
|
||||||
description: "Nobody but you can view this file",
|
|
||||||
id: "private"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
AYS: [
|
|
||||||
{
|
|
||||||
name: "Yes",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dfv(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("Default file visibility",options.FV).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/auth/dfv`,{method:"POST", body:JSON.stringify({
|
|
||||||
defaultFileVisibility: exp.selected
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function update_all_files(optPicker: OptionPicker) {
|
|
||||||
optPicker.picker("You sure?",[
|
|
||||||
{
|
|
||||||
name: "Yeah",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: `This will make all of your files ${get(account)?.defaultFileVisibility || "public"}`,
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
fetch(`/files/manage`,{method:"POST", body:JSON.stringify({
|
|
||||||
target:get(account)?.files,
|
|
||||||
action: "changeFileVisibility",
|
|
||||||
|
|
||||||
value: get(account)?.defaultFileVisibility
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fileOptions(optPicker: OptionPicker, file: FilePointer & {id:string}) {
|
|
||||||
optPicker.picker(file.filename,[
|
|
||||||
{
|
|
||||||
name: file.tag ? "Remove tag" : "Tag file",
|
|
||||||
icon: `/static/assets/icons/${file.tag ? "tag_remove" : "tag"}.svg`,
|
|
||||||
description: file.tag || `File has no tag`,
|
|
||||||
id: "tag"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Change file visibility",
|
|
||||||
icon: `/static/assets/icons/${file.visibility||"public"}.svg`,
|
|
||||||
description: `File is currently ${file.visibility||"public"}`,
|
|
||||||
id: "changeFileVisibility"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Delete file",
|
|
||||||
icon: `/static/assets/icons/admin/delete_file.svg`,
|
|
||||||
description: ``,
|
|
||||||
id: "delete"
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
|
|
||||||
switch( exp.selected ) {
|
|
||||||
|
|
||||||
case "delete":
|
|
||||||
|
|
||||||
fetch(`/files/manage`,{method:"POST", body:JSON.stringify({
|
|
||||||
target: [ file.id ],
|
|
||||||
action: "delete",
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFilePointers();
|
|
||||||
})
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "changeFileVisibility":
|
|
||||||
|
|
||||||
optPicker.picker("Set file visibility", options.FV2).then((exp) => {
|
|
||||||
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
|
|
||||||
fetch(`/files/manage`, {method: "POST", body: JSON.stringify({
|
|
||||||
target: [ file.id ],
|
|
||||||
action: "changeFileVisibility",
|
|
||||||
|
|
||||||
value: exp.selected
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFilePointers();
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "tag":
|
|
||||||
|
|
||||||
if (file.tag) {
|
|
||||||
fetch(`/files/manage`, {method: "POST", body: JSON.stringify({
|
|
||||||
target: [ file.id ],
|
|
||||||
action: "setTag"
|
|
||||||
})}).then(fetchFilePointers)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
optPicker.picker("Enter a tag (max 30char)",[
|
|
||||||
{
|
|
||||||
name: "Tag name",
|
|
||||||
icon: "/static/assets/icons/tag.svg",
|
|
||||||
id: "tag",
|
|
||||||
inputSettings: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "OK",
|
|
||||||
icon: "/static/assets/icons/update.svg",
|
|
||||||
description: "",
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
]).then((exp) => {
|
|
||||||
|
|
||||||
if (exp && exp.selected) {
|
|
||||||
|
|
||||||
fetch(`/files/manage`, {method: "POST", body: JSON.stringify({
|
|
||||||
target: [ file.id ],
|
|
||||||
action: "setTag",
|
|
||||||
|
|
||||||
value: exp.tag || null
|
|
||||||
})}).then((response) => {
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFilePointers();
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,243 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import Pulldown from "./Pulldown.svelte"
|
|
||||||
import { padding_scaleY } from "../transition/padding_scaleY"
|
|
||||||
import { circIn,circOut } from "svelte/easing"
|
|
||||||
import { account, fetchAccountData, serverStats, refreshNeeded } from "../stores";
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import OptionPicker from "../prompts/OptionPicker.svelte";
|
|
||||||
import * as accOpts from "../prompts/account";
|
|
||||||
import * as uplOpts from "../prompts/uploads";
|
|
||||||
import * as admOpts from "../prompts/admin";
|
|
||||||
|
|
||||||
let targetAction: "login"|"create"
|
|
||||||
let inProgress: boolean
|
|
||||||
let authError:{status:number,message:string}|undefined
|
|
||||||
|
|
||||||
let pwErr: HTMLDivElement
|
|
||||||
|
|
||||||
let optPicker: OptionPicker;
|
|
||||||
|
|
||||||
// lazy
|
|
||||||
|
|
||||||
let username: string
|
|
||||||
let password: string
|
|
||||||
|
|
||||||
let execute = () => {
|
|
||||||
if (inProgress) return
|
|
||||||
|
|
||||||
inProgress = true
|
|
||||||
|
|
||||||
fetch(`/auth/${targetAction}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
username, password
|
|
||||||
})
|
|
||||||
}).then(async (res) => {
|
|
||||||
inProgress = false
|
|
||||||
|
|
||||||
if (res.status != 200) {
|
|
||||||
authError = await res.json().catch(() => {
|
|
||||||
return {
|
|
||||||
status: res.status,
|
|
||||||
message: res.headers.get("x-backup-status-message") || res.statusText || ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
authError = undefined, username = "", password = "";
|
|
||||||
fetchAccountData();
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (pwErr && authError) {
|
|
||||||
pwErr.animate({
|
|
||||||
backgroundColor: ["#885555","#663333"],
|
|
||||||
easing: "ease-out"
|
|
||||||
},650)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// actual account menu
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Pulldown name="accounts">
|
|
||||||
<OptionPicker bind:this={optPicker} />
|
|
||||||
{#if $account}
|
|
||||||
<div class="loggedIn" transition:fade={{duration:200}}>
|
|
||||||
<h1>
|
|
||||||
Hey there, <span class="monospace">@{$account.username}</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="optPicker">
|
|
||||||
|
|
||||||
<div class="category">
|
|
||||||
<p>Account</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button on:click={() => accOpts.userChange(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/change_username.svg" alt="change username">
|
|
||||||
<p>Change username</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => ($account?.email ? accOpts.emailPotentialRemove : accOpts.emailChange)(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/mail.svg" alt="change email">
|
|
||||||
<p>Change email{#if $account.email}<span class="monospaceText"><br />{$account.email}</span>{/if}</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => accOpts.pwdChng(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/change_password.svg" alt="change password">
|
|
||||||
<p>Change password<span><br />You will be logged out of all sessions</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if !$account.admin}
|
|
||||||
<button on:click={() => accOpts.deleteAccount(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/delete_account.svg" alt="delete account">
|
|
||||||
<p>Delete account</p>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="category">
|
|
||||||
<p>Uploads</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button on:click={() => uplOpts.dfv(optPicker)}>
|
|
||||||
<img src={`/static/assets/icons/${$account.defaultFileVisibility || "public"}.svg`} alt={$account.defaultFileVisibility || "public"}>
|
|
||||||
<p>Default file visibility<span><br />Uploads will be <strong>{$account.defaultFileVisibility || "public"}</strong> by default</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => uplOpts.update_all_files(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/update.svg" alt="update">
|
|
||||||
<p>Make all of my files {$account.defaultFileVisibility || "public"}<span><br />Matches your default file visibility</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="category">
|
|
||||||
<p>Customization</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button on:click={() => accOpts.customcss(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/paint.svg" alt="customcss">
|
|
||||||
<p>Set custom CSS<span><br />{@html $account.customCSS ? `Using file ID <span class="number">${$account.customCSS}</span>` : "No custom CSS set"}</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => accOpts.embedColor(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/pound.svg" alt="embedColor">
|
|
||||||
<p>Set custom embed color<span><br />{@html $account?.embed?.color ? `Using custom color <span class="number">${$account?.embed?.color}</span>` : ""}</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => accOpts.embedSize(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/image.svg" alt="embedSize">
|
|
||||||
<p>Set embed image size <span><br />Images currently appear {$account?.embed?.largeImage ? `large` : "small"} in embeds</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if $refreshNeeded}
|
|
||||||
<button on:click={() => window.location.reload()} transition:fade={{duration: 200}}>
|
|
||||||
<img src="/static/assets/icons/refresh.svg" alt="refresh">
|
|
||||||
<p>Refresh<span><br />Changes were made which require a refresh</span></p>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="category">
|
|
||||||
<p>Sessions</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button on:click={() => fetch(`/auth/logout_sessions`,{method:"POST"}).then(() => fetchAccountData())}>
|
|
||||||
<img src="/static/assets/icons/logout_all.svg" alt="logout_all">
|
|
||||||
<p>Log out all sessions<span><br />{$account?.sessionCount} session(s) active</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => fetch(`/auth/logout`,{method:"POST"}).then(() => fetchAccountData())}>
|
|
||||||
<img src="/static/assets/icons/logout.svg" alt="logout">
|
|
||||||
<p>Log out<span><br />Session expires {new Date($account?.sessionExpires).toLocaleDateString()}</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if $account.admin}
|
|
||||||
|
|
||||||
<div class="category">
|
|
||||||
<p>Admin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button on:click={() => admOpts.deleteAccount(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/delete_account.svg" alt="delete account">
|
|
||||||
<p>Delete user account</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => admOpts.pwdReset(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/change_password.svg" alt="change password">
|
|
||||||
<p>Change user password</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => admOpts.elevateUser(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/admin/elevate_user.svg" alt="elevate account">
|
|
||||||
<p>Elevate account to admin</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => admOpts.chgOwner(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/link.svg" alt="change file owner">
|
|
||||||
<p>Change file owner</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => admOpts.chgId(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/admin/change_file_id.svg" alt="change file id">
|
|
||||||
<p>Change file ID<span><br />Potentially buggy, usage not recommended</span></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button on:click={() => admOpts.delFile(optPicker)}>
|
|
||||||
<img src="/static/assets/icons/admin/delete_file.svg" alt="delete file">
|
|
||||||
<p>Delete file</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/if}
|
|
||||||
<p style="font-size:12px;color:#AAAAAA;text-align:center;" class="monospace"><br />{$account.id}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="notLoggedIn" transition:fade={{duration:200}}>
|
|
||||||
<div class="container_div">
|
|
||||||
<h1>monofile <span style:color="#999999">accounts</span></h1>
|
|
||||||
<p class="flavor">Gain control of your uploads.</p>
|
|
||||||
|
|
||||||
{#if targetAction}
|
|
||||||
|
|
||||||
<div class="fields" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
|
|
||||||
{#if !$serverStats?.accounts.registrationEnabled && targetAction == "create"}
|
|
||||||
<div class="pwError">
|
|
||||||
<div style:background-color="#554C33">
|
|
||||||
<p>Account registration has been disabled by this instance's owner</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if authError}
|
|
||||||
<div class="pwError" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
|
|
||||||
<div bind:this={pwErr}>
|
|
||||||
<p><strong>{authError.status}</strong> {authError.message}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<input placeholder="username" type="text" bind:value={username}>
|
|
||||||
<input placeholder="password" type="password" bind:value={password}>
|
|
||||||
<button on:click={execute}>{@html inProgress ? "<span class=loader><i>•</i> <i>•</i> <i>•</i></span>" : (targetAction=="login" ? "Log in" : "Create account") }</button>
|
|
||||||
|
|
||||||
{#if targetAction == "login"}
|
|
||||||
<button class="flavor" on:click={() => accOpts.forgotPassword(optPicker)}>I forgot my password</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{:else}
|
|
||||||
|
|
||||||
<div class="lgBtnContainer" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
|
|
||||||
<button on:click={() => targetAction="login"}>Log in</button>
|
|
||||||
<button on:click={() => targetAction="create"}>Sign up</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Pulldown>
|
|
|
@ -1,63 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import Pulldown from "./Pulldown.svelte";
|
|
||||||
import { account, fetchFilePointers, files, pulldownManager } from "../stores.js";
|
|
||||||
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import { flip } from "svelte/animate";
|
|
||||||
import { fileOptions } from "../prompts/uploads";
|
|
||||||
import OptionPicker from "../prompts/OptionPicker.svelte";
|
|
||||||
|
|
||||||
let picker: OptionPicker;
|
|
||||||
let query = "";
|
|
||||||
|
|
||||||
fetchFilePointers();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Pulldown name="files">
|
|
||||||
|
|
||||||
<OptionPicker bind:this={picker} />
|
|
||||||
|
|
||||||
{#if $account?.username}<div class="loggedIn">
|
|
||||||
<input type="text" placeholder={`Search ${$files.length} file(s)`} class="searchBar" bind:value={query}>
|
|
||||||
|
|
||||||
<div class="fileList">
|
|
||||||
<!-- Probably wildly inefficient but who cares, I just wanna get this over with -->
|
|
||||||
{#each $files.filter(f => f&&(f.filename.toLowerCase().includes(query.toLowerCase()) || f.id.toLowerCase().includes(query.toLowerCase()) || f.tag?.includes(query.toLowerCase()))) as file (file.id)}
|
|
||||||
<div class="flFile" transition:fade={{duration:200}} animate:flip={{duration:200}}>
|
|
||||||
<button class="hitbox" on:click={() => window.open(`/download/${file.id}`)}></button> <!-- this is bad, but I'm lazy -->
|
|
||||||
<div class="flexCont">
|
|
||||||
<div class="fileInfo">
|
|
||||||
<h2>{file.filename}</h2>
|
|
||||||
<p class="detail">
|
|
||||||
<img src="/static/assets/icons/{file.visibility || "public"}.svg" alt={file.visibility||"public"} />
|
|
||||||
<span class="number">{file.id}</span> — <span class="cd">{file.mime.split(";")[0]}</span>
|
|
||||||
{#if file.reserved}
|
|
||||||
<br />
|
|
||||||
<img src="/static/assets/icons/update.svg" alt="uploading"/>
|
|
||||||
Uploading...
|
|
||||||
{/if}
|
|
||||||
{#if file.tag}
|
|
||||||
<br />
|
|
||||||
<img src="/static/assets/icons/tag.svg" alt="tag"/>
|
|
||||||
<span class="cd">{file.tag}</span>
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="more" on:click={() => fileOptions(picker, file)}>
|
|
||||||
<img src="/static/assets/icons/more.svg" alt="more" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="notLoggedIn">
|
|
||||||
<div style:height="10px" />
|
|
||||||
<p class="flavor">Log in to view uploads</p>
|
|
||||||
<button on:click={$pulldownManager.openPulldown("account")}>OK</button>
|
|
||||||
<div style:height="14px" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
</Pulldown>
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
//import type Pulldown from "./pulldowns/Pulldown.svelte"
|
|
||||||
import type { SvelteComponent } from "svelte"
|
|
||||||
import type { Account } from "../../server/lib/accounts"
|
|
||||||
import type cfg from "../../../config.json"
|
|
||||||
import type { FilePointer } from "../../server/lib/files"
|
|
||||||
|
|
||||||
export let refreshNeeded = writable(false)
|
|
||||||
export let pulldownManager = writable<SvelteComponent>()
|
|
||||||
export let account = writable<Account & {sessionCount: number, sessionExpires: number}|undefined>()
|
|
||||||
export let serverStats = writable<typeof cfg & {version: string, files: number} | undefined>()
|
|
||||||
export let files = writable<(FilePointer & {id:string})[]>([])
|
|
||||||
|
|
||||||
export let fetchAccountData = function() {
|
|
||||||
fetch("/auth/me").then(async (response) => {
|
|
||||||
if (response.status == 200) {
|
|
||||||
account.set(await response.json())
|
|
||||||
} else {
|
|
||||||
account.set(undefined)
|
|
||||||
}
|
|
||||||
}).catch((err) => { console.error(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
export let fetchFilePointers = function() {
|
|
||||||
fetch("/files/list", { cache: "no-cache" }).then(async (response) => {
|
|
||||||
if (response.status == 200) {
|
|
||||||
files.set(await response.json())
|
|
||||||
} else {
|
|
||||||
files.set([])
|
|
||||||
}
|
|
||||||
}).catch((err) => { console.error(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
export let refresh_stats = () => {
|
|
||||||
fetch("/server").then(async (data) => {
|
|
||||||
serverStats.set(await data.json())
|
|
||||||
}).catch((err) => { console.error(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountData()
|
|
1
src/svelte/global.d.ts
vendored
1
src/svelte/global.d.ts
vendored
|
@ -1 +0,0 @@
|
||||||
/// <reference types="svelte" />
|
|
|
@ -1,5 +0,0 @@
|
||||||
import App from "./App.svelte"
|
|
||||||
|
|
||||||
new App({
|
|
||||||
target: document.body
|
|
||||||
})
|
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
|
||||||
"include": ["**/*"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"outDir": "../../dist/static/vite",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"moduleResolution": "bundler"
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{ "path": "../../tsconfig.json" }
|
|
||||||
]
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 401 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue