mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 13:36:25 -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
|
||||
out
|
||||
dist
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
.svelte-kit
|
||||
|
|
12
config.json
12
config.json
|
@ -1,15 +1,13 @@
|
|||
{
|
||||
"maxDiscordFiles": 1000,
|
||||
"maxDiscordFiles": 500,
|
||||
"maxDiscordFileSize": 10485760,
|
||||
"targetGuild": "906767804575928390",
|
||||
"targetChannel": "1024080525993971913",
|
||||
"requestTimeout": 3600000,
|
||||
"targetChannel": "1160783463696302182",
|
||||
"requestTimeout": 1800000,
|
||||
"maxUploadIdLength": 30,
|
||||
"accounts": {
|
||||
"registrationEnabled": true,
|
||||
"requiredForUpload": false
|
||||
"requiredForUpload": true
|
||||
},
|
||||
|
||||
"mail": {
|
||||
"transport": {
|
||||
"host": "smtp.fastmail.com",
|
||||
|
@ -22,4 +20,4 @@
|
|||
},
|
||||
"trustProxy": true,
|
||||
"forceSSL": false
|
||||
}
|
||||
}
|
||||
|
|
26
package.json
26
package.json
|
@ -5,12 +5,17 @@
|
|||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node ./out/server/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "vite",
|
||||
"build": "tsc --build src/server && vite build",
|
||||
"preview": "vite preview"
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"lint": "prettier --check .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 version"
|
||||
],
|
||||
"keywords": [],
|
||||
"author": "Etcetera (https://cetera.uk)",
|
||||
"license": "Unlicense",
|
||||
|
@ -31,14 +36,17 @@
|
|||
"dotenv": "^16.0.2",
|
||||
"express": "^4.18.1",
|
||||
"formidable": "^3.5.1",
|
||||
"hono": "^4.0.10",
|
||||
"hono": "4.0.10",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"nodemailer": "^6.9.3",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"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",
|
||||
"@types/bytes": "^3.1.1",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
|
@ -46,9 +54,9 @@
|
|||
"@types/range-parser": "^1.2.6",
|
||||
"discord-api-types": "^0.37.61",
|
||||
"sass": "^1.57.1",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte": "4",
|
||||
"svelte-preprocess": "^5.1.3",
|
||||
"tslib": "^2.6.2",
|
||||
"vite": "^4.5.0"
|
||||
"vite": "5"
|
||||
}
|
||||
}
|
||||
|
|
655
pnpm-lock.yaml
655
pnpm-lock.yaml
|
@ -6,8 +6,8 @@ settings:
|
|||
|
||||
dependencies:
|
||||
'@hono/node-server':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
specifier: ^1.8.2
|
||||
version: 1.8.2
|
||||
'@types/body-parser':
|
||||
specifier: ^1.19.2
|
||||
version: 1.19.3
|
||||
|
@ -29,24 +29,30 @@ dependencies:
|
|||
bytes:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
commander:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
cookie-parser:
|
||||
specifier: ^1.4.6
|
||||
version: 1.4.6
|
||||
discord.js:
|
||||
specifier: ^14.7.1
|
||||
version: 14.13.0
|
||||
dotenv:
|
||||
specifier: ^16.0.2
|
||||
version: 16.3.1
|
||||
express:
|
||||
specifier: ^4.18.1
|
||||
version: 4.18.2
|
||||
formidable:
|
||||
specifier: ^3.5.1
|
||||
version: 3.5.1
|
||||
hono:
|
||||
specifier: ^3.8.3
|
||||
version: 3.8.3
|
||||
specifier: ^4.0.10
|
||||
version: 4.0.10
|
||||
multer:
|
||||
specifier: ^1.4.5-lts.1
|
||||
version: 1.4.5-lts.1
|
||||
node-fetch:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
nodemailer:
|
||||
specifier: ^6.9.3
|
||||
version: 6.9.5
|
||||
|
@ -55,92 +61,99 @@ dependencies:
|
|||
version: 5.2.2
|
||||
|
||||
devDependencies:
|
||||
'@hono/vite-dev-server':
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0(hono@4.0.10)
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6(svelte@3.59.2)(vite@4.5.0)
|
||||
'@tsconfig/svelte':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
'@types/bytes':
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.2
|
||||
'@types/cookie-parser':
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.4
|
||||
'@types/formidable':
|
||||
specifier: ^3.4.5
|
||||
version: 3.4.5
|
||||
'@types/range-parser':
|
||||
specifier: ^1.2.6
|
||||
version: 1.2.6
|
||||
discord-api-types:
|
||||
specifier: ^0.37.61
|
||||
version: 0.37.71
|
||||
sass:
|
||||
specifier: ^1.57.1
|
||||
version: 1.69.0
|
||||
svelte:
|
||||
specifier: ^3.55.1
|
||||
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:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.0(sass@1.69.0)
|
||||
|
||||
packages:
|
||||
|
||||
/@discordjs/builders@1.6.5:
|
||||
resolution: {integrity: sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==}
|
||||
engines: {node: '>=16.11.0'}
|
||||
/@cloudflare/workerd-darwin-64@1.20240320.1:
|
||||
resolution: {integrity: sha512-ioG5k2M17xyiAlK/k3L21NZLMVeSHMjwlmGtZyCyzSLL5/zGINcgZ5yPLV0UuWiysw07/6Jjzm5Sx94hzMVybg==}
|
||||
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:
|
||||
'@discordjs/formatters': 0.3.2
|
||||
'@discordjs/util': 1.0.1
|
||||
'@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
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
dev: true
|
||||
|
||||
/@esbuild/android-arm64@0.18.20:
|
||||
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||
|
@ -340,32 +353,46 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/@hono/node-server@1.2.0:
|
||||
resolution: {integrity: sha512-aHT8lDMLpd7ioXJ1/057+h+oE/k7rCOWmjklYDsE0jE4CoNB9XzG4f8dRHvw4s5HJFocaYDiGgYM/V0kYbQ0ww==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
dev: false
|
||||
/@fastify/busboy@2.1.1:
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
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:
|
||||
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
||||
dev: true
|
||||
|
||||
/@sapphire/async-queue@1.5.0:
|
||||
resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==}
|
||||
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'}
|
||||
/@jridgewell/trace-mapping@0.3.9:
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
lodash: 4.17.21
|
||||
dev: false
|
||||
|
||||
/@sapphire/snowflake@3.5.1:
|
||||
resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==}
|
||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
||||
dev: false
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/@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==}
|
||||
|
@ -403,6 +430,10 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@tsconfig/svelte@4.0.1:
|
||||
resolution: {integrity: sha512-B+XlGpmuAQzJqDoBATNCvEPqQg0HkO7S8pM14QDI5NsmtymzRexQ1N+nX2H6RTtFbuFgaZD4I8AAi8voGg0GLg==}
|
||||
dev: true
|
||||
|
||||
/@types/body-parser@1.19.3:
|
||||
resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==}
|
||||
dependencies:
|
||||
|
@ -440,6 +471,12 @@ packages:
|
|||
'@types/qs': 6.9.8
|
||||
'@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:
|
||||
resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==}
|
||||
|
||||
|
@ -464,6 +501,10 @@ packages:
|
|||
'@types/node': 20.8.3
|
||||
dev: false
|
||||
|
||||
/@types/pug@2.0.10:
|
||||
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
|
||||
dev: true
|
||||
|
||||
/@types/qs@6.9.8:
|
||||
resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==}
|
||||
|
||||
|
@ -483,17 +524,6 @@ packages:
|
|||
'@types/mime': 3.0.2
|
||||
'@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:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -502,6 +532,17 @@ packages:
|
|||
negotiator: 0.6.3
|
||||
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:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -518,6 +559,16 @@ packages:
|
|||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: false
|
||||
|
@ -531,6 +582,10 @@ packages:
|
|||
- debug
|
||||
dev: false
|
||||
|
||||
/balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
dev: true
|
||||
|
||||
/binary-extensions@2.2.0:
|
||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -576,6 +631,19 @@ packages:
|
|||
- supports-color
|
||||
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:
|
||||
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -583,6 +651,10 @@ packages:
|
|||
fill-range: 7.0.1
|
||||
dev: true
|
||||
|
||||
/buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: true
|
||||
|
||||
/buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
dev: false
|
||||
|
@ -606,6 +678,15 @@ packages:
|
|||
get-intrinsic: 1.2.1
|
||||
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:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
@ -628,6 +709,15 @@ packages:
|
|||
delayed-stream: 1.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
||||
engines: {'0': node >= 0.8}
|
||||
|
@ -670,12 +760,20 @@ packages:
|
|||
/cookie@0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
|
@ -719,32 +817,21 @@ packages:
|
|||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dev: false
|
||||
|
||||
/discord-api-types@0.37.50:
|
||||
resolution: {integrity: sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==}
|
||||
/detect-indent@6.1.0:
|
||||
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
|
||||
|
||||
/discord.js@14.13.0:
|
||||
resolution: {integrity: sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==}
|
||||
engines: {node: '>=16.11.0'}
|
||||
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
|
||||
/discord-api-types@0.37.71:
|
||||
resolution: {integrity: sha512-oYDVWoiQdblr9DpwOgpi5d78dVhPcoN9YZCCqYZf2T0v9+iICs7k2bYGumoHuYMtaIitpp5aQNs+2guVkgjbOA==}
|
||||
dev: true
|
||||
|
||||
/dotenv@16.3.1:
|
||||
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
|
||||
|
@ -760,6 +847,10 @@ packages:
|
|||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/es6-promise@3.3.1:
|
||||
resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
|
||||
dev: true
|
||||
|
||||
/esbuild@0.18.20:
|
||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -799,6 +890,11 @@ packages:
|
|||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/exit-hook@2.2.1:
|
||||
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/express@4.18.2:
|
||||
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
@ -838,8 +934,12 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
/fetch-blob@3.2.0:
|
||||
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
|
||||
|
||||
/fill-range@7.0.1:
|
||||
|
@ -883,6 +983,21 @@ packages:
|
|||
mime-types: 2.1.35
|
||||
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:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -893,6 +1008,10 @@ packages:
|
|||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
dev: true
|
||||
|
||||
/fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
|
@ -914,6 +1033,13 @@ packages:
|
|||
has-symbols: 1.0.3
|
||||
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:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
@ -921,6 +1047,25 @@ packages:
|
|||
is-glob: 4.0.3
|
||||
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:
|
||||
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -936,11 +1081,15 @@ packages:
|
|||
engines: {node: '>= 0.4.0'}
|
||||
dev: false
|
||||
|
||||
/hono@3.8.3:
|
||||
resolution: {integrity: sha512-NLJgUCKKMvijBy+V+U1FQTsNwHk2bD1KGlWJA9+qaCNWgx5St9bhfQwxrpcTGvG2Gi2naemTWCzBavDNXOqO6Q==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
/hexoid@1.0.0:
|
||||
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/hono@4.0.10:
|
||||
resolution: {integrity: sha512-sq0RFAC3Ij+bkhZu90EGAQnVI1EhohRsjo9BU+BjXLbC71GSy41JjsFqCeg8MRpO2Gdu0A4MXF5licO89tn/rw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
/http-errors@2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -963,9 +1112,15 @@ packages:
|
|||
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: false
|
||||
|
||||
/ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
|
@ -1005,18 +1160,6 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
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:
|
||||
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -1056,16 +1199,55 @@ packages:
|
|||
hasBin: true
|
||||
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:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
dev: false
|
||||
|
||||
/mkdirp@0.5.6:
|
||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
dev: false
|
||||
|
||||
/ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
@ -1092,6 +1274,11 @@ packages:
|
|||
xtend: 4.0.2
|
||||
dev: false
|
||||
|
||||
/mustache@4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid@3.3.6:
|
||||
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
|
@ -1103,6 +1290,20 @@ packages:
|
|||
engines: {node: '>= 0.6'}
|
||||
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:
|
||||
resolution: {integrity: sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
@ -1129,11 +1330,21 @@ packages:
|
|||
ee-first: 1.1.1
|
||||
dev: false
|
||||
|
||||
/once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
/parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
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:
|
||||
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||
dev: false
|
||||
|
@ -1156,6 +1367,10 @@ packages:
|
|||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/printable-characters@1.0.42:
|
||||
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||
dev: true
|
||||
|
||||
/process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
dev: false
|
||||
|
@ -1219,6 +1434,13 @@ packages:
|
|||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/rimraf@2.7.1:
|
||||
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
dev: true
|
||||
|
||||
/rollup@3.29.4:
|
||||
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
|
@ -1239,6 +1461,15 @@ packages:
|
|||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
@ -1294,16 +1525,43 @@ packages:
|
|||
object-inspect: 1.12.3
|
||||
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:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/stoppable@1.1.0:
|
||||
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
|
||||
engines: {node: '>=4', npm: '>=6'}
|
||||
dev: true
|
||||
|
||||
/streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
@ -1315,6 +1573,13 @@ packages:
|
|||
safe-buffer: 5.1.2
|
||||
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):
|
||||
resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
|
||||
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
||||
|
@ -1324,6 +1589,54 @@ packages:
|
|||
svelte: 3.59.2
|
||||
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:
|
||||
resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -1341,13 +1654,9 @@ packages:
|
|||
engines: {node: '>=0.6'}
|
||||
dev: false
|
||||
|
||||
/ts-mixer@6.0.3:
|
||||
resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==}
|
||||
dev: false
|
||||
|
||||
/tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
dev: false
|
||||
dev: true
|
||||
|
||||
/type-is@1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
|
@ -1365,14 +1674,13 @@ packages:
|
|||
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/undici@5.22.1:
|
||||
resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==}
|
||||
/undici@5.28.3:
|
||||
resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==}
|
||||
engines: {node: '>=14.0'}
|
||||
dependencies:
|
||||
busboy: 1.6.0
|
||||
dev: false
|
||||
'@fastify/busboy': 2.1.1
|
||||
dev: true
|
||||
|
||||
/unpipe@1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
|
@ -1440,8 +1748,29 @@ packages:
|
|||
vite: 4.5.0(sass@1.69.0)
|
||||
dev: true
|
||||
|
||||
/ws@8.14.2:
|
||||
resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==}
|
||||
/web-streams-polyfill@3.3.3:
|
||||
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'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
|
@ -1451,9 +1780,21 @@ packages:
|
|||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
dev: false
|
||||
dev: true
|
||||
|
||||
/xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
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 * as Accounts from "../../../lib/accounts.js"
|
||||
import * as auth from "../../../lib/auth.js"
|
||||
import * as Accounts from "../../lib/accounts.js"
|
||||
import * as auth from "../../lib/auth.js"
|
||||
import { writeFile } from "fs/promises"
|
||||
import { sendMail } from "../../../lib/mail.js"
|
||||
import { sendMail } from "../../lib/mail.js"
|
||||
import {
|
||||
getAccount,
|
||||
requiresAccount,
|
||||
requiresAdmin,
|
||||
requiresPermissions,
|
||||
} from "../../../lib/middleware.js"
|
||||
import Files from "../../../lib/files.js"
|
||||
} from "../../lib/middleware.js"
|
||||
import Files from "../../lib/files.js"
|
||||
|
||||
export let adminRoutes = new Hono<{
|
||||
Variables: {
|
|
@ -1,24 +1,24 @@
|
|||
import { Hono, Handler } from "hono"
|
||||
import { getCookie, setCookie } from "hono/cookie"
|
||||
import * as Accounts from "../../../lib/accounts.js"
|
||||
import * as auth from "../../../lib/auth.js"
|
||||
import { sendMail } from "../../../lib/mail.js"
|
||||
import * as Accounts from "../../lib/accounts.js"
|
||||
import * as auth from "../../lib/auth.js"
|
||||
import { sendMail } from "../../lib/mail.js"
|
||||
import {
|
||||
getAccount,
|
||||
noAPIAccess,
|
||||
requiresAccount,
|
||||
requiresPermissions,
|
||||
} from "../../../lib/middleware.js"
|
||||
import { accountRatelimit } from "../../../lib/ratelimit.js"
|
||||
} from "../../lib/middleware.js"
|
||||
import { accountRatelimit } from "../../lib/ratelimit.js"
|
||||
|
||||
import ServeError from "../../../lib/errors.js"
|
||||
import ServeError from "../../lib/errors.js"
|
||||
import Files, {
|
||||
FileVisibility,
|
||||
generateFileId,
|
||||
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<{
|
||||
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)
|
||||
|
||||
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) => {
|
||||
if (!config.accounts.registrationEnabled) {
|
||||
return ServeError(ctx, 403, "account registration disabled")
|
|
@ -1,12 +1,12 @@
|
|||
import { Hono } from "hono"
|
||||
import * as Accounts from "../../../lib/accounts.js"
|
||||
import * as Accounts from "../../lib/accounts.js"
|
||||
import { writeFile } from "fs/promises"
|
||||
import Files from "../../../lib/files.js"
|
||||
import Files from "../../lib/files.js"
|
||||
import {
|
||||
getAccount,
|
||||
requiresAccount,
|
||||
requiresPermissions,
|
||||
} from "../../../lib/middleware.js"
|
||||
} from "../../lib/middleware.js"
|
||||
|
||||
export let fileApiRoutes = new Hono<{
|
||||
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
|
||||
|
||||
|
||||
import { 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 {
|
||||
getAccount,
|
||||
login,
|
||||
requiresAccount
|
||||
} from "../../../lib/middleware.js"
|
||||
import ServeError from "../../../lib/errors.js"
|
||||
import Files, { id_check_regex } from "../../lib/files.js"
|
||||
import * as Accounts from "../../lib/accounts.js"
|
||||
import * as auth from "../../lib/auth.js"
|
||||
import { getAccount, login, requiresAccount } from "../../lib/middleware.js"
|
||||
import ServeError from "../../lib/errors.js"
|
||||
|
||||
const router = new Hono<{
|
||||
Variables: {
|
||||
|
@ -51,12 +46,11 @@ export default function (files: Files) {
|
|||
return ctx.text("logged in")
|
||||
})
|
||||
|
||||
router.get("/", requiresAccount, ctx => {
|
||||
router.get("/", requiresAccount, (ctx) => {
|
||||
let sessionToken = auth.tokenFor(ctx)
|
||||
return ctx.json({
|
||||
expiry: auth.AuthTokens.find(
|
||||
(e) => e.token == sessionToken
|
||||
)?.expire,
|
||||
expiry: auth.AuthTokens.find((e) => e.token == sessionToken)
|
||||
?.expire,
|
||||
})
|
||||
})
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
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 { CodeMgr } from "../../../lib/mail.js"
|
||||
import ServeError from "../../lib/errors.js"
|
||||
import * as Accounts from "../../lib/accounts.js"
|
||||
import type Files from "../../lib/files.js"
|
||||
import { CodeMgr } from "../../lib/mail.js"
|
||||
import { Hono } from "hono"
|
||||
import { getAccount, login } from "../../../lib/middleware.js"
|
||||
import { getAccount, login } from "../../lib/middleware.js"
|
||||
export let router = new Hono<{
|
||||
Variables: {
|
||||
account: Accounts.Account
|
||||
|
@ -20,7 +19,11 @@ export default function (files: Files) {
|
|||
|
||||
if (code) {
|
||||
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) {
|
||||
|
@ -32,10 +35,10 @@ export default function (files: Files) {
|
|||
|
||||
currentAccount.email = code.data
|
||||
await Accounts.save()
|
||||
|
||||
return ctx.redirect('/')
|
||||
|
||||
return ctx.redirect("/")
|
||||
} else return ServeError(ctx, 404, "code not found")
|
||||
})
|
||||
|
||||
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 { basename } from "path"
|
||||
import { Writable } from "node:stream"
|
||||
import pkg from "../../package.json" assert { type: "json" }
|
||||
import config from "../../config.json" assert { type: "json" }
|
||||
import pkg from "../package.json" assert { type: "json" }
|
||||
import { fileURLToPath } from "url"
|
||||
import { dirname } from "path"
|
||||
|
||||
|
@ -15,6 +14,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|||
if (!fs.existsSync(__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
|
||||
let files = new Files(config)
|
||||
|
||||
|
@ -23,65 +29,65 @@ program
|
|||
.description("Quickly run monofile to execute a query or so")
|
||||
.version(pkg.version)
|
||||
|
||||
program.command("list")
|
||||
program
|
||||
.command("list")
|
||||
.alias("ls")
|
||||
.description("List files in the database")
|
||||
.action(() => {
|
||||
Object.keys(files.files).forEach(e => console.log(e))
|
||||
Object.keys(files.files).forEach((e) => console.log(e))
|
||||
})
|
||||
|
||||
|
||||
program.command("download")
|
||||
program
|
||||
.command("download")
|
||||
.alias("dl")
|
||||
.description("Download a file from the database")
|
||||
.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) => {
|
||||
|
||||
await (new Promise<void>(resolve => setTimeout(() => resolve(), 1000)))
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000))
|
||||
|
||||
let fp = files.files[id]
|
||||
|
||||
if (!fp)
|
||||
throw `file ${id} not found`
|
||||
|
||||
let out = options.output as string || `./`
|
||||
if (!fp) throw `file ${id} not found`
|
||||
|
||||
let out = (options.output as string) || `./`
|
||||
|
||||
if (fs.existsSync(out) && (await stat(out)).isDirectory())
|
||||
out = `${out.replace(/\/+$/, "")}/${fp.filename}`
|
||||
|
||||
let filestream = await files.readFileStream(id)
|
||||
|
||||
let prog=0
|
||||
filestream.on("data", dt => {
|
||||
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)`)
|
||||
let prog = 0
|
||||
filestream.on("data", (dt) => {
|
||||
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)`
|
||||
)
|
||||
})
|
||||
|
||||
filestream.pipe(
|
||||
fs.createWriteStream(out)
|
||||
)
|
||||
filestream.pipe(fs.createWriteStream(out))
|
||||
})
|
||||
|
||||
|
||||
program.command("upload")
|
||||
program
|
||||
.command("upload")
|
||||
.alias("up")
|
||||
.description("Upload a file to the instance")
|
||||
.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) => {
|
||||
|
||||
await (new Promise<void>(resolve => setTimeout(() => resolve(), 1000)))
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000))
|
||||
|
||||
if (!(fs.existsSync(file) && (await stat(file)).isFile()))
|
||||
throw `${file} is not a file`
|
||||
|
||||
|
||||
let writable = files.createWriteStream()
|
||||
|
||||
writable
|
||||
.setName(basename(file))
|
||||
?.setType("application/octet-stream")
|
||||
|
||||
writable.setName(basename(file))?.setType("application/octet-stream")
|
||||
|
||||
if (options.id) writable.setUploadId(options.id)
|
||||
|
||||
if (!(writable instanceof Writable))
|
||||
|
@ -90,7 +96,7 @@ program.command("upload")
|
|||
console.log(`started: ${file}`)
|
||||
|
||||
writable.on("drain", () => {
|
||||
console.log("Drained");
|
||||
console.log("Drained")
|
||||
})
|
||||
|
||||
writable.on("finish", async () => {
|
||||
|
@ -108,11 +114,8 @@ program.command("upload")
|
|||
|
||||
writable.on("close", () => {
|
||||
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>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="./style/error.scss"
|
||||
>
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg"
|
||||
href="/static/assets/icons/error.svg"
|
||||
>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/api/v1/account/me/css"
|
||||
>
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
<link rel="stylesheet" href="./style/error.scss" />
|
||||
<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>$code</title>
|
||||
|
||||
<meta name="theme-color" content="rgb(30, 33, 36)">
|
||||
|
||||
/>
|
||||
<title>%sveltekit.status%</title>
|
||||
<meta name="theme-color" content="rgb(30, 33, 36)" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p class="error">
|
||||
<span class="code">$code</span>
|
||||
$text
|
||||
<span class="code">%sveltekit.status%</span>
|
||||
%sveltekit.error.message%
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</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 { files } from "./accounts.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 { readFileSync } from "node:fs"
|
||||
|
||||
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
|
||||
* @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()) {
|
||||
if (cond) return err
|
||||
}
|
||||
|
@ -80,18 +83,15 @@ export interface StatusCodeError {
|
|||
}
|
||||
|
||||
export class WebError extends Error {
|
||||
|
||||
readonly statusCode: number = 500
|
||||
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.statusCode = status
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ReadStream extends Readable {
|
||||
|
||||
files: Files
|
||||
pointer: FilePointer
|
||||
|
||||
|
@ -100,52 +100,60 @@ export class ReadStream extends Readable {
|
|||
position: number = 0
|
||||
|
||||
ranges: {
|
||||
useRanges: boolean,
|
||||
byteStart: number,
|
||||
useRanges: boolean
|
||||
byteStart: number
|
||||
byteEnd: number
|
||||
scan_msg_begin: number,
|
||||
scan_msg_end: number,
|
||||
scan_files_begin: number,
|
||||
scan_msg_begin: number
|
||||
scan_msg_end: number
|
||||
scan_files_begin: number
|
||||
scan_files_end: number
|
||||
}
|
||||
|
||||
id: number = Math.random()
|
||||
aborter?: AbortController
|
||||
|
||||
constructor(files: Files, pointer: FilePointer, range?: {start: number, end: number}) {
|
||||
constructor(
|
||||
files: Files,
|
||||
pointer: FilePointer,
|
||||
range?: { start: number; end: number }
|
||||
) {
|
||||
super()
|
||||
console.log(this.id, range)
|
||||
this.files = files
|
||||
this.pointer = pointer
|
||||
|
||||
let useRanges =
|
||||
Boolean(range && pointer.chunkSize && pointer.sizeInBytes)
|
||||
|
||||
let useRanges = Boolean(
|
||||
range && pointer.chunkSize && pointer.sizeInBytes
|
||||
)
|
||||
|
||||
this.ranges = {
|
||||
useRanges,
|
||||
scan_msg_begin: 0,
|
||||
scan_msg_end: pointer.messageids.length - 1,
|
||||
scan_files_begin:
|
||||
useRanges
|
||||
scan_files_begin: useRanges
|
||||
? Math.floor(range!.start / pointer.chunkSize!)
|
||||
: 0,
|
||||
scan_files_end:
|
||||
useRanges
|
||||
scan_files_end: useRanges
|
||||
? Math.ceil(range!.end / pointer.chunkSize!) - 1
|
||||
: -1,
|
||||
byteStart: range?.start || 0,
|
||||
byteEnd: range?.end || 0
|
||||
byteEnd: range?.end || 0,
|
||||
}
|
||||
|
||||
if (useRanges)
|
||||
this.ranges.scan_msg_begin = Math.floor(this.ranges.scan_files_begin / 10),
|
||||
this.ranges.scan_msg_end = Math.ceil(this.ranges.scan_files_end / 10),
|
||||
this.msgIdx = this.ranges.scan_msg_begin
|
||||
|
||||
(this.ranges.scan_msg_begin = Math.floor(
|
||||
this.ranges.scan_files_begin / 10
|
||||
)),
|
||||
(this.ranges.scan_msg_end = Math.ceil(
|
||||
this.ranges.scan_files_end / 10
|
||||
)),
|
||||
(this.msgIdx = this.ranges.scan_msg_begin)
|
||||
|
||||
console.log(this.ranges)
|
||||
}
|
||||
|
||||
async _read() {/*
|
||||
async _read() {
|
||||
/*
|
||||
console.log("Calling for more data")
|
||||
if (this.busy) return
|
||||
this.busy = true
|
||||
|
@ -160,24 +168,32 @@ export class ReadStream extends Readable {
|
|||
this.pushData()
|
||||
}
|
||||
|
||||
async _destroy(error: Error | null, callback: (error?: Error | null | undefined) => void): Promise<void> {
|
||||
if (this.aborter)
|
||||
this.aborter.abort()
|
||||
async _destroy(
|
||||
error: Error | null,
|
||||
callback: (error?: Error | null | undefined) => void
|
||||
): Promise<void> {
|
||||
if (this.aborter) this.aborter.abort()
|
||||
callback()
|
||||
}
|
||||
|
||||
async getNextAttachment() {
|
||||
// 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
|
||||
|
||||
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.
|
||||
if (
|
||||
!this.pointer.messageids[this.msgIdx]
|
||||
|| this.msgIdx > this.ranges.scan_msg_end
|
||||
) return null
|
||||
!this.pointer.messageids[this.msgIdx] ||
|
||||
this.msgIdx > this.ranges.scan_msg_end
|
||||
)
|
||||
return null
|
||||
|
||||
let msg = await this.files.api
|
||||
.fetchMessage(this.pointer.messageids[this.msgIdx])
|
||||
|
@ -190,95 +206,113 @@ export class ReadStream extends Readable {
|
|||
let attach = msg.attachments
|
||||
console.log(attach)
|
||||
|
||||
this.attachmentBuffer = this.ranges.useRanges ? attach.slice(
|
||||
this.msgIdx == this.ranges.scan_msg_begin
|
||||
? this.ranges.scan_files_begin - this.ranges.scan_msg_begin * 10
|
||||
: 0,
|
||||
this.msgIdx == this.ranges.scan_msg_end
|
||||
? this.ranges.scan_files_end - this.ranges.scan_msg_end * 10 + 1
|
||||
: attach.length
|
||||
) : attach
|
||||
this.attachmentBuffer = this.ranges.useRanges
|
||||
? attach.slice(
|
||||
this.msgIdx == this.ranges.scan_msg_begin
|
||||
? this.ranges.scan_files_begin -
|
||||
this.ranges.scan_msg_begin * 10
|
||||
: 0,
|
||||
this.msgIdx == this.ranges.scan_msg_end
|
||||
? this.ranges.scan_files_end -
|
||||
this.ranges.scan_msg_end * 10 +
|
||||
1
|
||||
: attach.length
|
||||
)
|
||||
: attach
|
||||
|
||||
console.log(this.attachmentBuffer)
|
||||
}
|
||||
|
||||
this.msgIdx++
|
||||
return this.attachmentBuffer.splice(0,1)[0]
|
||||
return this.attachmentBuffer.splice(0, 1)[0]
|
||||
}
|
||||
|
||||
async getPusherForWebStream(webStream: ReadableStream) {
|
||||
const reader = await webStream.getReader()
|
||||
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 stream = this
|
||||
|
||||
return function() {
|
||||
|
||||
return function () {
|
||||
if (pushing) return
|
||||
pushing = true
|
||||
|
||||
return reader.read().catch(e => {
|
||||
// Probably means an AbortError; whatever it is we'll need to abort
|
||||
if (webStream.locked) reader.releaseLock()
|
||||
webStream.cancel().catch(e => undefined)
|
||||
if (!stream.destroyed) stream.destroy()
|
||||
return e
|
||||
}).then(result => {
|
||||
if (result instanceof Error || !result) return result
|
||||
|
||||
let pushed
|
||||
if (!result.done) {
|
||||
pushing = false
|
||||
pushed = pushToStream(result.value)
|
||||
}
|
||||
return {readyForMore: pushed || false, streamDone: result.done }
|
||||
})
|
||||
return reader
|
||||
.read()
|
||||
.catch((e) => {
|
||||
// Probably means an AbortError; whatever it is we'll need to abort
|
||||
if (webStream.locked) reader.releaseLock()
|
||||
webStream.cancel().catch((e) => undefined)
|
||||
if (!stream.destroyed) stream.destroy()
|
||||
return e
|
||||
})
|
||||
.then((result) => {
|
||||
if (result instanceof Error || !result) return result
|
||||
|
||||
let pushed
|
||||
if (!result.done) {
|
||||
pushing = false
|
||||
pushed = pushToStream(result.value)
|
||||
}
|
||||
return {
|
||||
readyForMore: pushed || false,
|
||||
streamDone: result.done,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async getNextChunk() {
|
||||
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
|
||||
|
||||
let {
|
||||
byteStart, byteEnd, scan_files_begin, scan_files_end
|
||||
} = this.ranges
|
||||
let { byteStart, byteEnd, scan_files_begin, scan_files_end } =
|
||||
this.ranges
|
||||
|
||||
let headers: HeadersInit =
|
||||
this.ranges.useRanges
|
||||
? {
|
||||
Range: `bytes=${
|
||||
this.position == 0
|
||||
? byteStart - scan_files_begin * this.pointer.chunkSize!
|
||||
: "0"
|
||||
}-${
|
||||
this.attachmentBuffer.length == 0 && this.msgIdx == scan_files_end
|
||||
? byteEnd - scan_files_end * this.pointer.chunkSize!
|
||||
: ""
|
||||
}`,
|
||||
}
|
||||
: {}
|
||||
let headers: HeadersInit = this.ranges.useRanges
|
||||
? {
|
||||
Range: `bytes=${
|
||||
this.position == 0
|
||||
? byteStart -
|
||||
scan_files_begin * this.pointer.chunkSize!
|
||||
: "0"
|
||||
}-${
|
||||
this.attachmentBuffer.length == 0 &&
|
||||
this.msgIdx == scan_files_end
|
||||
? byteEnd - scan_files_end * this.pointer.chunkSize!
|
||||
: ""
|
||||
}`,
|
||||
}
|
||||
: {}
|
||||
|
||||
this.aborter = new AbortController()
|
||||
|
||||
let response = await fetch(scanning_chunk.url, {headers, signal: this.aborter.signal})
|
||||
.catch((e: Error) => {
|
||||
console.error(e)
|
||||
return {body: e}
|
||||
})
|
||||
let response = await fetch(scanning_chunk.url, {
|
||||
headers,
|
||||
signal: this.aborter.signal,
|
||||
}).catch((e: Error) => {
|
||||
console.error(e)
|
||||
return { body: e }
|
||||
})
|
||||
|
||||
this.position++
|
||||
|
||||
|
||||
return response.body
|
||||
}
|
||||
|
||||
currentPusher?: (() => Promise<{readyForMore: boolean, streamDone: boolean } | void> | undefined)
|
||||
currentPusher?: () =>
|
||||
| Promise<{ readyForMore: boolean; streamDone: boolean } | void>
|
||||
| undefined
|
||||
busy: boolean = false
|
||||
|
||||
async pushData(): Promise<boolean | undefined> {
|
||||
|
||||
// uh oh, we don't have a currentPusher
|
||||
// let's make one then
|
||||
if (!this.currentPusher) {
|
||||
|
@ -292,7 +326,8 @@ export class ReadStream extends Readable {
|
|||
// or the stream has ended.
|
||||
// let's destroy the stream
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -304,12 +339,10 @@ export class ReadStream extends Readable {
|
|||
this.currentPusher = undefined
|
||||
return this.pushData()
|
||||
} else return result?.readyForMore
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class UploadStream extends Writable {
|
||||
|
||||
uploadId?: string
|
||||
name?: string
|
||||
mime?: string
|
||||
|
@ -331,7 +364,11 @@ export class UploadStream extends Writable {
|
|||
|
||||
async _write(data: Buffer, encoding: string, callback: () => void) {
|
||||
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"))
|
||||
|
||||
this.hash.update(data)
|
||||
|
@ -343,21 +380,32 @@ export class UploadStream extends Writable {
|
|||
|
||||
while (position < data.byteLength) {
|
||||
let capture = Math.min(
|
||||
((this.files.config.maxDiscordFileSize*10) - (this.filled % (this.files.config.maxDiscordFileSize*10))),
|
||||
data.byteLength-position
|
||||
this.files.config.maxDiscordFileSize * 10 -
|
||||
(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) {
|
||||
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) )
|
||||
console.log(`pushed ${data.byteLength} byte chunk`);
|
||||
position += capture, this.filled += capture
|
||||
readyForMore = this.current.push(
|
||||
data.subarray(position, position + 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
|
||||
if (this.filled % (this.files.config.maxDiscordFileSize*10) == 0) {
|
||||
if (
|
||||
this.filled % (this.files.config.maxDiscordFileSize * 10) ==
|
||||
0
|
||||
) {
|
||||
this.current!.push(null)
|
||||
this.current = undefined
|
||||
}
|
||||
|
@ -369,24 +417,27 @@ export class UploadStream extends Writable {
|
|||
|
||||
async _final(callback: (error?: Error | null | undefined) => void) {
|
||||
if (this.current) {
|
||||
this.current.push(null);
|
||||
this.current.push(null)
|
||||
// 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()
|
||||
}
|
||||
|
||||
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
|
||||
await this.abort()
|
||||
callback(error)
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @description Cancel & unlock the file. When destroy() is called with a non-WebError, this is automatically called
|
||||
*/
|
||||
*/
|
||||
async abort() {
|
||||
if (this.aborted) return
|
||||
this.aborted = true
|
||||
|
@ -406,8 +457,13 @@ export class UploadStream extends Writable {
|
|||
async commit() {
|
||||
if (this.errored) throw this.error
|
||||
if (!this.writableFinished) {
|
||||
let err = Error("attempted to commit file when the stream was still unfinished")
|
||||
if (!this.destroyed) {this.destroy(err)}; throw err
|
||||
let err = Error(
|
||||
"attempted to commit file when the stream was still unfinished"
|
||||
)
|
||||
if (!this.destroyed) {
|
||||
this.destroy(err)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
// Perform checks
|
||||
|
@ -421,7 +477,7 @@ export class UploadStream extends Writable {
|
|||
}
|
||||
|
||||
if (!this.uploadId) this.setUploadId(generateFileId())
|
||||
|
||||
|
||||
let ogf = this.files.files[this.uploadId!]
|
||||
|
||||
this.files.files[this.uploadId!] = {
|
||||
|
@ -430,19 +486,18 @@ export class UploadStream extends Writable {
|
|||
messageids: this.messages,
|
||||
owner: this.owner,
|
||||
sizeInBytes: this.filled,
|
||||
visibility: ogf ? ogf.visibility
|
||||
: (
|
||||
this.owner
|
||||
? Accounts.getFromId(this.owner)?.defaultFileVisibility
|
||||
: undefined
|
||||
),
|
||||
visibility: ogf
|
||||
? ogf.visibility
|
||||
: this.owner
|
||||
? Accounts.getFromId(this.owner)?.defaultFileVisibility
|
||||
: undefined,
|
||||
// so that json.stringify doesnt include tag:undefined
|
||||
...((ogf||{}).tag ? {tag:ogf.tag} : {}),
|
||||
...((ogf || {}).tag ? { tag: ogf.tag } : {}),
|
||||
|
||||
chunkSize: this.files.config.maxDiscordFileSize,
|
||||
|
||||
md5: this.hash.digest("hex"),
|
||||
lastModified: Date.now()
|
||||
lastModified: Date.now(),
|
||||
}
|
||||
|
||||
delete this.files.locks[this.uploadId!]
|
||||
|
@ -456,52 +511,72 @@ export class UploadStream extends Writable {
|
|||
|
||||
setName(name: string) {
|
||||
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)
|
||||
return this.destroy( new WebError(400, "filename can be a maximum of 512 characters") )
|
||||
|
||||
this.name = name;
|
||||
return this.destroy(
|
||||
new WebError(400, "filename can be a maximum of 512 characters")
|
||||
)
|
||||
|
||||
this.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
setType(type: string) {
|
||||
setType(type: string) {
|
||||
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)
|
||||
return this.destroy( new WebError(400, "mime type can be a maximum of 256 characters") )
|
||||
|
||||
this.mime = type;
|
||||
return this.destroy(
|
||||
new WebError(
|
||||
400,
|
||||
"mime type can be a maximum of 256 characters"
|
||||
)
|
||||
)
|
||||
|
||||
this.mime = type
|
||||
return this
|
||||
}
|
||||
|
||||
setUploadId(id: string) {
|
||||
if (this.uploadId)
|
||||
return this.destroy( new WebError(400, "duplicate attempt to set upload 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") )
|
||||
return this.destroy(
|
||||
new WebError(400, "duplicate attempt to set upload 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)
|
||||
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])
|
||||
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.uploadId = id
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// merged StreamBuffer helper
|
||||
|
||||
|
||||
filled: number = 0
|
||||
current?: Readable
|
||||
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
|
||||
this.newmessage_debounce = false
|
||||
|
||||
|
@ -510,24 +585,28 @@ export class UploadStream extends Writable {
|
|||
let stream = new Readable({
|
||||
read() {
|
||||
// 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")
|
||||
}
|
||||
},
|
||||
})
|
||||
stream.pause()
|
||||
|
||||
|
||||
console.log(`Starting a message`)
|
||||
this.files.api.send(stream).then(message => {
|
||||
this.messages.push(message.id)
|
||||
console.log(`Sent: ${message.id}`)
|
||||
this.newmessage_debounce = true
|
||||
this.emit("debounceReleased")
|
||||
}).catch(e => {
|
||||
if (!this.errored) this.destroy(e)
|
||||
})
|
||||
this.files.api
|
||||
.send(stream)
|
||||
.then((message) => {
|
||||
this.messages.push(message.id)
|
||||
console.log(`Sent: ${message.id}`)
|
||||
this.newmessage_debounce = true
|
||||
this.emit("debounceReleased")
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!this.errored) this.destroy(e)
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
}
|
||||
|
||||
private async getNextStream() {
|
||||
|
@ -536,12 +615,14 @@ export class UploadStream extends Writable {
|
|||
if (this.current) return this.current
|
||||
else if (this.newmessage_debounce) {
|
||||
// startmessage.... idk
|
||||
this.current = await this.startMessage();
|
||||
this.current = await this.startMessage()
|
||||
return this.current
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
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 {
|
||||
config: Configuration
|
||||
api: API
|
||||
files: { [key: string]: FilePointer } = {}
|
||||
files: Record<string, FilePointer> = Object.create(null) // { [key: string]: FilePointer } = {}
|
||||
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) {
|
||||
this.config = config
|
||||
this.api = new API(process.env.TOKEN!, config)
|
||||
|
||||
readFile(this.data_directory+ "/files.json")
|
||||
readFile(this.data_directory + "/files.json")
|
||||
.then((buf) => {
|
||||
this.files = JSON.parse(buf.toString() || "{}")
|
||||
})
|
||||
|
@ -574,7 +655,7 @@ export default class Files {
|
|||
|
||||
/**
|
||||
* @description Saves file database
|
||||
*
|
||||
*
|
||||
*/
|
||||
async write(): Promise<void> {
|
||||
await writeFile(
|
||||
|
@ -592,7 +673,7 @@ export default class Files {
|
|||
* @param uploadId Target file's ID
|
||||
*/
|
||||
|
||||
async update( uploadId: string ) {
|
||||
async update(uploadId: string) {
|
||||
let target_file = this.files[uploadId]
|
||||
let attachment_sizes = []
|
||||
|
||||
|
@ -604,12 +685,12 @@ export default class Files {
|
|||
}
|
||||
|
||||
if (!target_file.sizeInBytes)
|
||||
target_file.sizeInBytes = attachment_sizes.reduce((a, b) => a + b, 0)
|
||||
|
||||
if (!target_file.chunkSize)
|
||||
target_file.chunkSize = attachment_sizes[0]
|
||||
target_file.sizeInBytes = attachment_sizes.reduce(
|
||||
(a, b) => a + b,
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
if (!target_file.chunkSize) target_file.chunkSize = attachment_sizes[0]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -624,7 +705,8 @@ export default class Files {
|
|||
): Promise<ReadStream> {
|
||||
if (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)
|
||||
} else {
|
||||
throw { status: 404, message: "not found" }
|
||||
|
@ -648,9 +730,15 @@ export default class Files {
|
|||
}
|
||||
delete this.files[uploadId]
|
||||
|
||||
if (!noWrite) this.write().catch((err) => {
|
||||
throw err
|
||||
})
|
||||
if (!noWrite)
|
||||
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 "dotenv/config"
|
||||
import config from "../../../config.json" assert {type:"json"}
|
||||
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,
|
||||
transport = createTransport({
|
||||
|
@ -37,25 +43,30 @@ export function sendMail(to: string, subject: string, content: string) {
|
|||
}
|
||||
|
||||
export namespace CodeMgr {
|
||||
export const Intents = ["verifyEmail", "recoverAccount"] as const
|
||||
|
||||
export const Intents = [
|
||||
"verifyEmail",
|
||||
"recoverAccount"
|
||||
] as const
|
||||
export type Intent = (typeof Intents)[number]
|
||||
|
||||
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(
|
||||
Intents.map(e => [
|
||||
e,
|
||||
{byId: new Map<string, Code>(), byUser: new Map<string, Code[]>()}
|
||||
])) as Record<Intent, { byId: Map<string, Code>, byUser: Map<string, Code[]> }>
|
||||
Intents.map((e) => [
|
||||
e,
|
||||
{
|
||||
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
|
||||
|
||||
export class Code {
|
||||
export class Code {
|
||||
readonly id: string = generateFileId(12)
|
||||
readonly for: string
|
||||
|
||||
|
@ -65,25 +76,30 @@ export namespace CodeMgr {
|
|||
|
||||
readonly data: any
|
||||
|
||||
constructor(intent: Intent, forUser: string, data?: any, time: number = 15*60*1000) {
|
||||
this.for = forUser;
|
||||
constructor(
|
||||
intent: Intent,
|
||||
forUser: string,
|
||||
data?: any,
|
||||
time: number = 15 * 60 * 1000
|
||||
) {
|
||||
this.for = forUser
|
||||
this.intent = intent
|
||||
this.expiryClear = setTimeout(this.terminate.bind(this), time)
|
||||
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)
|
||||
if (!byUser) {
|
||||
byUser = []
|
||||
codes[intent].byUser.set(this.for, byUser);
|
||||
codes[intent].byUser.set(this.for, byUser)
|
||||
}
|
||||
|
||||
byUser.push(this)
|
||||
}
|
||||
|
||||
terminate() {
|
||||
codes[this.intent].byId.delete(this.id);
|
||||
codes[this.intent].byId.delete(this.id)
|
||||
let bu = codes[this.intent].byUser.get(this.id)!
|
||||
bu.splice(bu.indexOf(this), 1)
|
||||
clearTimeout(this.expiryClear)
|
||||
|
@ -93,5 +109,4 @@ export namespace CodeMgr {
|
|||
return forUser === this.for
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -95,12 +95,14 @@ export const assertAPI = function (
|
|||
|
||||
// Not really middleware but a utility
|
||||
|
||||
export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
|
||||
path: "/",
|
||||
sameSite: "Strict",
|
||||
secure: true,
|
||||
httpOnly: true
|
||||
})
|
||||
export const login = (ctx: Context, account: string) =>
|
||||
setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
|
||||
path: "/",
|
||||
sameSite: "Strict",
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
maxAge: 34560
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<div id="uploadWindow">
|
||||
<main>
|
||||
<h1>
|
||||
monofile
|
||||
{#if notificationPermission === "default"}
|
||||
|
@ -375,4 +375,4 @@
|
|||
>
|
||||
</p>
|
||||
<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
|
||||
*/
|
||||
|
||||
@import url("/static/assets/fonts/inconsolata.css");
|
||||
@import url("/static/assets/fonts/source_sans.css");
|
||||
@import url("/static/assets/fonts/fira_code.css");
|
||||
@import url("/assets/fonts/inconsolata.css");
|
||||
@import url("/assets/fonts/source_sans.css");
|
||||
@import url("/assets/fonts/fira_code.css");
|
||||
|
||||
$FallbackFonts:
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
$FallbackFonts: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
|
||||
%normal {
|
||||
font-family: "Source Sans Pro", $FallbackFonts
|
||||
font-family: "Source Sans Pro", $FallbackFonts;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -25,14 +20,17 @@ $FallbackFonts:
|
|||
(it's just in case)
|
||||
*/
|
||||
|
||||
*:not(span), .normal { @extend %normal; }
|
||||
*:not(span),
|
||||
.normal {
|
||||
@extend %normal;
|
||||
}
|
||||
|
||||
/*
|
||||
for code blocks / terminal
|
||||
*/
|
||||
|
||||
.monospace {
|
||||
font-family: "Fira Code", monospace
|
||||
font-family: "Fira Code", monospace;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -49,37 +47,33 @@ $darkish: rgb(54, 62, 70);
|
|||
|
||||
body {
|
||||
background-color: rgb(30, 33, 36); // this is here so that
|
||||
// pulling down to refresh
|
||||
// on mobile looks good
|
||||
// pulling down to refresh
|
||||
// on mobile looks good
|
||||
}
|
||||
|
||||
#appContent {
|
||||
background-color: $Background
|
||||
background-color: $Background;
|
||||
}
|
||||
|
||||
/*
|
||||
scrollbars
|
||||
*/
|
||||
|
||||
* {
|
||||
/* nice scrollbars aren't needed on mobile so */
|
||||
@media screen and (min-width:500px) {
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width:5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color:#222222;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color:#333;
|
||||
|
||||
&:hover {
|
||||
background-color:#373737;
|
||||
}
|
||||
}
|
||||
|
||||
/* nice scrollbars aren't needed on mobile so */
|
||||
@media screen and (min-width: 500px) {
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
::-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 {
|
||||
background-color:#191919;
|
||||
border: 1px solid gray;
|
||||
|
@ -112,4 +112,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// should probably start using mixins for thingss like this
|
||||
|
||||
#uploadWindow {
|
||||
main {
|
||||
.file {
|
||||
background-color:#191919;
|
||||
border: 1px solid gray;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@use "uploader/add_new_files";
|
||||
@use "uploader/file";
|
||||
|
||||
#uploadWindow {
|
||||
main {
|
||||
position:absolute;
|
||||
left:50%;
|
||||
top:50%;
|
||||
|
@ -83,4 +83,4 @@
|
|||
top:10px;
|
||||
padding:0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
#uploadWindow {
|
||||
main {
|
||||
img, video, audio {
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,168 +1,166 @@
|
|||
#uploadWindow {
|
||||
color: #FFFFFF
|
||||
main {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color:#DDDDDD;
|
||||
background-color: #dddddd;
|
||||
}
|
||||
|
||||
#appContent {
|
||||
background: darkgray;
|
||||
@media screen and (max-width:500px) {
|
||||
background:white;
|
||||
@media screen and (max-width: 500px) {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
#uploadWindow {
|
||||
main {
|
||||
background: white;
|
||||
|
||||
color:black;
|
||||
color: black;
|
||||
|
||||
h1, p, a {
|
||||
h1,
|
||||
p,
|
||||
a {
|
||||
margin: 0px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color:rgb(153, 153, 153);
|
||||
color: rgb(153, 153, 153);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight:600;
|
||||
font-weight: 600;
|
||||
font-size: 25px;
|
||||
text-align:center;
|
||||
text-align: center;
|
||||
|
||||
@media screen and (max-width:500px) {
|
||||
@media screen and (max-width: 500px) {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
& > p:nth-of-type(1) {
|
||||
text-align:center;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-weight:600;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color:black !important;
|
||||
@media screen and (max-width:500px) {
|
||||
color: black !important;
|
||||
@media screen and (max-width: 500px) {
|
||||
font-size: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor:pointer;
|
||||
color:black;
|
||||
border:none;
|
||||
outline:none;
|
||||
padding:5px;
|
||||
background: #AAAAAA;
|
||||
cursor: pointer;
|
||||
color: black;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 5px;
|
||||
background: #aaaaaa;
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
font-size:16px;
|
||||
padding:10px;
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
outline: 1px solid #333333;
|
||||
color: black;
|
||||
background-color:#AAAAAA;
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
}
|
||||
|
||||
& > button:nth-last-of-type(1) {
|
||||
background-color:#66AAFF;
|
||||
background-color: #66aaff;
|
||||
&:hover {
|
||||
background-color:#66AAFF;
|
||||
background-color: #66aaff;
|
||||
}
|
||||
}
|
||||
|
||||
#add_new_files {
|
||||
background-color: #AAAAAA66;
|
||||
border:1px solid #AAAAAA;
|
||||
background-color: #aaaaaa66;
|
||||
border: 1px solid #aaaaaa;
|
||||
|
||||
#file_add_btns {
|
||||
button, input[type=text] {
|
||||
transition-duration:0s;
|
||||
|
||||
button,
|
||||
input[type="text"] {
|
||||
transition-duration: 0s;
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
font-size:16px;
|
||||
padding:10px;
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
input[type="text"] {
|
||||
font-family: "Fira Code", monospace;
|
||||
background-color:#AAAAAA;
|
||||
color:black;
|
||||
background-color: #aaaaaa;
|
||||
color: black;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor:pointer;
|
||||
background-color:#AAAAAA;
|
||||
cursor: pointer;
|
||||
background-color: #aaaaaa;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
flex-basis: 50%;
|
||||
transition-duration:0s;
|
||||
background-color:#AAAAAA;
|
||||
transition-duration: 0s;
|
||||
background-color: #aaaaaa;
|
||||
color: black;
|
||||
outline: 1px solid #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.fileUpload {
|
||||
background-color:#AAAAAA;
|
||||
transition-duration:250ms;
|
||||
background-color: #aaaaaa;
|
||||
transition-duration: 250ms;
|
||||
|
||||
&:hover {
|
||||
transition-duration:0s;
|
||||
background-color:#AAAAAA;
|
||||
transition-duration: 0s;
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file {
|
||||
background-color: #AAAAAA66;
|
||||
border: 1px solid #AAAAAA;
|
||||
background-color: #aaaaaa66;
|
||||
border: 1px solid #aaaaaa;
|
||||
|
||||
input[type=text] {
|
||||
input[type="text"] {
|
||||
font-family: "Fira Code", monospace;
|
||||
background-color:#AAAAAA;
|
||||
color:black;
|
||||
background-color: #aaaaaa;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
* {
|
||||
/* nice scrollbars aren't needed on mobile so */
|
||||
@media screen and (min-width:500px) {
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width:5px;
|
||||
}
|
||||
/* nice scrollbars aren't needed on mobile so */
|
||||
@media screen and (min-width: 500px) {
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color:#AAAAAA;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color:#DDDDDD;
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #dddddd;
|
||||
|
||||
&:hover {
|
||||
background-color:#FFFFFF;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#topbar {
|
||||
background-color: #DDDDDD;
|
||||
background-color: #dddddd;
|
||||
}
|
||||
|
||||
.error {
|
||||
.code {
|
||||
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