diff --git a/.gitignore b/.gitignore index 5522d8e..534f807 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules .data out dist -tsconfig.tsbuildinfo \ No newline at end of file +tsconfig.tsbuildinfo +.svelte-kit diff --git a/config.json b/config.json index ad1a786..5bceb8f 100644 --- a/config.json +++ b/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 -} \ No newline at end of file +} diff --git a/package.json b/package.json index 15f91f9..ce6e53c 100644 --- a/package.json +++ b/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" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ffa49c..aa13e73 100644 --- a/pnpm-lock.yaml +++ b/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 diff --git a/src/server/routes/api/v0/adminRoutes.ts b/src/api/v0/adminRoutes.ts similarity index 93% rename from src/server/routes/api/v0/adminRoutes.ts rename to src/api/v0/adminRoutes.ts index d251b1f..7bb92dd 100644 --- a/src/server/routes/api/v0/adminRoutes.ts +++ b/src/api/v0/adminRoutes.ts @@ -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: { diff --git a/src/server/routes/api/v0/authRoutes.ts b/src/api/v0/authRoutes.ts similarity index 88% rename from src/server/routes/api/v0/authRoutes.ts rename to src/api/v0/authRoutes.ts index 7b9cbff..7c1dc27 100644 --- a/src/server/routes/api/v0/authRoutes.ts +++ b/src/api/v0/authRoutes.ts @@ -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") diff --git a/src/server/routes/api/v0/fileApiRoutes.ts b/src/api/v0/fileApiRoutes.ts similarity index 93% rename from src/server/routes/api/v0/fileApiRoutes.ts rename to src/api/v0/fileApiRoutes.ts index 9b59fff..58e979e 100644 --- a/src/server/routes/api/v0/fileApiRoutes.ts +++ b/src/api/v0/fileApiRoutes.ts @@ -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: { diff --git a/src/api/v0/index.ts b/src/api/v0/index.ts new file mode 100644 index 0000000..81e0991 --- /dev/null +++ b/src/api/v0/index.ts @@ -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) +} diff --git a/src/api/v0/primaryApi.ts b/src/api/v0/primaryApi.ts new file mode 100644 index 0000000..4fff3cb --- /dev/null +++ b/src/api/v0/primaryApi.ts @@ -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 +} diff --git a/src/api/v1/account.ts b/src/api/v1/account.ts new file mode 100644 index 0000000..a8e1d6f --- /dev/null +++ b/src/api/v1/account.ts @@ -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 & { + 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` +// @Jack5079 make typings better if possible + +type Validator< + T extends keyof Partial, + 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> = + | { + acceptsNull: true + validator: Validator + } + | { + acceptsNull?: false + validator: Validator + } + +const validators: { + [T in keyof Partial]: + | Validator + | ValidatorWithSettings +} = { + 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`, + `Hello there! Your email address (${target.email}) has been disconnected from the monofile account ${target.username}. 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`, + `Hello there! You are recieving this message because you decided to link your email, ${ + params.email.split("@")[0] + }@${ + params.email.split("@")[1] + }, to your account, ${ + target.username + }. If you would like to continue, please click here, 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`, + `Hello there! Your password on your account, ${target.username}, has been updated` + + `${ + actor != target + ? ` by ${actor.username}` + : "" + }. ` + + `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`, + `Hello there! Your username on your account, ${target.username}, has been updated` + + `${ + actor != target + ? ` by ${actor.username}` + : "" + } to ${params.username}. ` + + `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 + + 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, ${acc.username}, 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 +} diff --git a/src/api/v1/file/index.ts b/src/api/v1/file/index.ts new file mode 100644 index 0000000..4542147 --- /dev/null +++ b/src/api/v1/file/index.ts @@ -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 +} diff --git a/src/api/v1/file/individual.ts b/src/api/v1/file/individual.ts new file mode 100644 index 0000000..692faf9 --- /dev/null +++ b/src/api/v1/file/individual.ts @@ -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 +} diff --git a/src/api/v1/index.ts b/src/api/v1/index.ts new file mode 100644 index 0000000..fc083c5 --- /dev/null +++ b/src/api/v1/index.ts @@ -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) +} diff --git a/src/server/routes/api/v1/session.ts b/src/api/v1/session.ts similarity index 75% rename from src/server/routes/api/v1/session.ts rename to src/api/v1/session.ts index 590d1f3..f16dd0f 100644 --- a/src/server/routes/api/v1/session.ts +++ b/src/api/v1/session.ts @@ -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, }) }) diff --git a/src/server/routes/api/web/api.json b/src/api/web/api.json similarity index 100% rename from src/server/routes/api/web/api.json rename to src/api/web/api.json diff --git a/src/server/routes/api/web/go.ts b/src/api/web/go.ts similarity index 67% rename from src/server/routes/api/web/go.ts rename to src/api/web/go.ts index b05345f..f484904 100644 --- a/src/server/routes/api/web/go.ts +++ b/src/api/web/go.ts @@ -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 -} \ No newline at end of file +} diff --git a/src/api/web/index.ts b/src/api/web/index.ts new file mode 100644 index 0000000..fc39afb --- /dev/null +++ b/src/api/web/index.ts @@ -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) +} diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..8adbd19 --- /dev/null +++ b/src/app.html @@ -0,0 +1,13 @@ + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/server/cli.ts b/src/cli.ts similarity index 56% rename from src/server/cli.ts rename to src/cli.ts index d2bcf58..586763e 100644 --- a/src/server/cli.ts +++ b/src/cli.ts @@ -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 of the file you'd like to download") - .option("-o, --output ", 'Folder or filename to output to') + .option("-o, --output ", "Folder or filename to output to") .action(async (id, options) => { - - await (new Promise(resolve => setTimeout(() => resolve(), 1000))) + await new Promise((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("", "Path to the file you'd like to upload") - .option("-id, --fileid ", 'Custom file ID to use') + .option("-id, --fileid ", "Custom file ID to use") .action(async (file, options) => { - - await (new Promise(resolve => setTimeout(() => resolve(), 1000))) + await new Promise((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() \ No newline at end of file +program.parse() diff --git a/src/consts.d.ts b/src/consts.d.ts new file mode 100644 index 0000000..714c3be --- /dev/null +++ b/src/consts.d.ts @@ -0,0 +1 @@ +declare const MONOFILE_VERSION: string diff --git a/src/download.html b/src/download.html deleted file mode 100644 index dd0847a..0000000 --- a/src/download.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - $FileId - - - - - - - - - - - - - - - -
-
-

- $FileName -

-

- $FileSize  —  uploaded by $Uploader -

- - - - - -
-
-
- - - diff --git a/src/error.html b/src/error.html index 47bfd60..6685e6a 100644 --- a/src/error.html +++ b/src/error.html @@ -1,41 +1,21 @@ - - - - - - - - - - + + + - - $code - - - + /> + %sveltekit.status% +

- $code -  $text + %sveltekit.status% +  %sveltekit.error.message%

- diff --git a/src/index.html b/src/index.html deleted file mode 100644 index c3d2319..0000000 --- a/src/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - monofile - - - - - - - - diff --git a/src/server/lib/DiscordAPI/DiscordRequests.ts b/src/lib/DiscordAPI/DiscordRequests.ts similarity index 100% rename from src/server/lib/DiscordAPI/DiscordRequests.ts rename to src/lib/DiscordAPI/DiscordRequests.ts diff --git a/src/server/lib/DiscordAPI/index.ts b/src/lib/DiscordAPI/index.ts similarity index 100% rename from src/server/lib/DiscordAPI/index.ts rename to src/lib/DiscordAPI/index.ts diff --git a/src/server/lib/accounts.ts b/src/lib/accounts.ts similarity index 100% rename from src/server/lib/accounts.ts rename to src/lib/accounts.ts diff --git a/src/server/lib/auth.ts b/src/lib/auth.ts similarity index 100% rename from src/server/lib/auth.ts rename to src/lib/auth.ts diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 0000000..2736dae --- /dev/null +++ b/src/lib/errors.ts @@ -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) +} diff --git a/src/server/lib/files.ts b/src/lib/files.ts similarity index 57% rename from src/server/lib/files.ts rename to src/lib/files.ts index 8edd2ac..d810d09 100644 --- a/src/server/lib/files.ts +++ b/src/lib/files.ts @@ -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) { +function multiAssert( + conditions: Map +) { 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 { - if (this.aborter) - this.aborter.abort() + async _destroy( + error: Error | null, + callback: (error?: Error | null | undefined) => void + ): Promise { + 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 { - // 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 { + private newmessage_debounce: boolean = true + + private async startMessage(): Promise { 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 = Object.create(null) // { [key: string]: FilePointer } = {} data_directory: string = `${process.cwd()}/.data` - locks: Record = {} // I'll, like, do something more proper later + locks: Record = 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 { 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 { 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) diff --git a/src/server/lib/mail.ts b/src/lib/mail.ts similarity index 66% rename from src/server/lib/mail.ts rename to src/lib/mail.ts index afab792..ba65fd2 100644 --- a/src/server/lib/mail.ts +++ b/src/lib/mail.ts @@ -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(), byUser: new Map()} - ])) as Record, byUser: Map }> + Intents.map((e) => [ + e, + { + byId: new Map(), + byUser: new Map(), + }, + ]) + ) as Record< + Intent, + { byId: Map; byUser: Map } + > // 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 } } - -} \ No newline at end of file +} diff --git a/src/server/lib/middleware.ts b/src/lib/middleware.ts similarity index 93% rename from src/server/lib/middleware.ts rename to src/lib/middleware.ts index a7a9cba..c200779 100644 --- a/src/server/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -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" diff --git a/src/server/lib/ratelimit.ts b/src/lib/ratelimit.ts similarity index 100% rename from src/server/lib/ratelimit.ts rename to src/lib/ratelimit.ts diff --git a/src/monofile.tsx b/src/monofile.tsx new file mode 100644 index 0000000..3442c1d --- /dev/null +++ b/src/monofile.tsx @@ -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 diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..5381ec8 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,19 @@ + + + + + + + {$page.status} + + +

+ {$page.status} +  {$page.error.message} +

diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..945e38e --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,33 @@ + + + + monofile + + + + + + + +
+ + + +
diff --git a/src/routes/+page.ts b/src/routes/+page.ts new file mode 100644 index 0000000..62ad4e4 --- /dev/null +++ b/src/routes/+page.ts @@ -0,0 +1 @@ +export const ssr = false diff --git a/src/routes/[...monofile]/+server.ts b/src/routes/[...monofile]/+server.ts new file mode 100644 index 0000000..f45342f --- /dev/null +++ b/src/routes/[...monofile]/+server.ts @@ -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 diff --git a/src/routes/api/+layout.server.ts b/src/routes/api/+layout.server.ts new file mode 100644 index 0000000..9b7b293 --- /dev/null +++ b/src/routes/api/+layout.server.ts @@ -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() ? diff --git a/src/routes/api/v1/file/[fileId]/+server.ts b/src/routes/api/v1/file/[fileId]/+server.ts new file mode 100644 index 0000000..0aa5248 --- /dev/null +++ b/src/routes/api/v1/file/[fileId]/+server.ts @@ -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 diff --git a/src/svelte/elem/PulldownManager.svelte b/src/routes/components/PulldownManager.svelte similarity index 100% rename from src/svelte/elem/PulldownManager.svelte rename to src/routes/components/PulldownManager.svelte diff --git a/src/svelte/elem/Topbar.svelte b/src/routes/components/Topbar.svelte similarity index 100% rename from src/svelte/elem/Topbar.svelte rename to src/routes/components/Topbar.svelte diff --git a/src/svelte/elem/UploadWindow.svelte b/src/routes/components/UploadWindow.svelte similarity index 97% rename from src/svelte/elem/UploadWindow.svelte rename to src/routes/components/UploadWindow.svelte index 91d073b..4a31b21 100644 --- a/src/svelte/elem/UploadWindow.svelte +++ b/src/routes/components/UploadWindow.svelte @@ -137,7 +137,7 @@ } -
+

monofile {#if notificationPermission === "default"} @@ -375,4 +375,4 @@ >

-
+

diff --git a/src/routes/components/prompts/OptionPicker.svelte b/src/routes/components/prompts/OptionPicker.svelte new file mode 100644 index 0000000..583b1e4 --- /dev/null +++ b/src/routes/components/prompts/OptionPicker.svelte @@ -0,0 +1,108 @@ + + +{#if activeModal} +
+ + +
+{/if} diff --git a/src/routes/components/prompts/account.ts b/src/routes/components/prompts/account.ts new file mode 100644 index 0000000..203dc48 --- /dev/null +++ b/src/routes/components/prompts/account.ts @@ -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() + }) + } + }) +} diff --git a/src/routes/components/prompts/admin.ts b/src/routes/components/prompts/admin.ts new file mode 100644 index 0000000..da5c03f --- /dev/null +++ b/src/routes/components/prompts/admin.ts @@ -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() + }) + } + }) + } + }) +} diff --git a/src/routes/components/prompts/uploads.ts b/src/routes/components/prompts/uploads.ts new file mode 100644 index 0000000..7e5e258 --- /dev/null +++ b/src/routes/components/prompts/uploads.ts @@ -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 + } + } + }) +} diff --git a/src/routes/components/pulldowns/Accounts.svelte b/src/routes/components/pulldowns/Accounts.svelte new file mode 100644 index 0000000..23bab14 --- /dev/null +++ b/src/routes/components/pulldowns/Accounts.svelte @@ -0,0 +1,401 @@ + + + + + {#if $account} +
+

+ Hey there, @{$account.username} +

+ +
+
+

Account

+
+ + + + + + + + {#if !$account.admin} + + {/if} + +
+

Uploads

+
+ + + + + +
+

Customization

+
+ + + + + + + + {#if $refreshNeeded} + + {/if} + +
+

Sessions

+
+ + + + + + {#if $account.admin} +
+

Admin

+
+ + + + + + + + + + + + + {/if} +

+
{$account.id} +

+
+
+ {:else} +
+
+

monofile accounts

+

Gain control of your uploads.

+ + {#if targetAction} +
+ {#if !$serverStats?.accounts.registrationEnabled && targetAction == "create"} +
+
+

+ Account registration has been disabled + by this instance's owner +

+
+
+ {/if} + + {#if authError} +
+
+

+ {authError.status} + {authError.message} +

+
+
+ {/if} + + + + + + {#if targetAction == "login"} + + {/if} +
+ {:else} +
+ + +
+ {/if} +
+
+ {/if} +
diff --git a/src/routes/components/pulldowns/Files.svelte b/src/routes/components/pulldowns/Files.svelte new file mode 100644 index 0000000..8bfa9ac --- /dev/null +++ b/src/routes/components/pulldowns/Files.svelte @@ -0,0 +1,100 @@ + + + + + + {#if $account?.username}
+ + +
+ + {#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)} +
+ + +
+
+

{file.filename}

+

+ {file.visibility  + {file.id}  —  {file.mime.split(";")[0]} + {#if file.reserved} +
+ uploading  Uploading... + {/if} + {#if file.tag} +
+ tag  + {file.tag} + {/if} +

+
+ +
+
+ {/each} +
+
+ {:else} +
+
+

Log in to view uploads

+ +
+
+ {/if} + diff --git a/src/svelte/elem/pulldowns/Help.svelte b/src/routes/components/pulldowns/Help.svelte similarity index 100% rename from src/svelte/elem/pulldowns/Help.svelte rename to src/routes/components/pulldowns/Help.svelte diff --git a/src/svelte/elem/pulldowns/Pulldown.svelte b/src/routes/components/pulldowns/Pulldown.svelte similarity index 100% rename from src/svelte/elem/pulldowns/Pulldown.svelte rename to src/routes/components/pulldowns/Pulldown.svelte diff --git a/src/routes/components/stores.ts b/src/routes/components/stores.ts new file mode 100644 index 0000000..a39f1b2 --- /dev/null +++ b/src/routes/components/stores.ts @@ -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() +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() diff --git a/src/svelte/elem/transition/_void.ts b/src/routes/components/transition/_void.ts similarity index 100% rename from src/svelte/elem/transition/_void.ts rename to src/routes/components/transition/_void.ts diff --git a/src/svelte/elem/transition/padding_scaleY.ts b/src/routes/components/transition/padding_scaleY.ts similarity index 100% rename from src/svelte/elem/transition/padding_scaleY.ts rename to src/routes/components/transition/padding_scaleY.ts diff --git a/src/svelte/elem/uploader/AttachmentZone.svelte b/src/routes/components/uploader/AttachmentZone.svelte similarity index 100% rename from src/svelte/elem/uploader/AttachmentZone.svelte rename to src/routes/components/uploader/AttachmentZone.svelte diff --git a/src/routes/download/[fileId]/+page.server.ts b/src/routes/download/[fileId]/+page.server.ts new file mode 100644 index 0000000..f264d3f --- /dev/null +++ b/src/routes/download/[fileId]/+page.server.ts @@ -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 diff --git a/src/routes/download/[fileId]/+page.svelte b/src/routes/download/[fileId]/+page.svelte new file mode 100644 index 0000000..e34879a --- /dev/null +++ b/src/routes/download/[fileId]/+page.svelte @@ -0,0 +1,75 @@ + + + + {data.id} + + + + + + + + + + + + {#if data.mime.startsWith("image/")} + {#if data.largeImage} + + {/if} + + {/if} + {#if data.mime.startsWith("video/")} + + + + + + + + {#if data.size >= 26214400} + + + {/if} + {/if} + + +
+
+

{data.filename}

+

+ {bytes(data.size)}  —  uploaded by {data.owner} +

+ + {#if data.mime.startsWith("image/")} +
+ {:else if data.mime.startsWith("video/")} +
+ {:else if data.mime.startsWith("audio/")} +
+ {/if} + + + +
+
+
diff --git a/src/server/index.ts b/src/server/index.ts deleted file mode 100644 index 74c2cff..0000000 --- a/src/server/index.ts +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/server/lib/errors.ts b/src/server/lib/errors.ts deleted file mode 100644 index 4f40d8c..0000000 --- a/src/server/lib/errors.ts +++ /dev/null @@ -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) - )) ?? "
$code $text
" - ).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) -} diff --git a/src/server/routes/api.ts b/src/server/routes/api.ts deleted file mode 100644 index a7a44a7..0000000 --- a/src/server/routes/api.ts +++ /dev/null @@ -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) - } - } -} diff --git a/src/server/routes/api/v0/api.json b/src/server/routes/api/v0/api.json deleted file mode 100644 index ad0bffb..0000000 --- a/src/server/routes/api/v0/api.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "v0", - "baseURL": "/", - "mount": [ - { "file": "primaryApi", "to": "/" }, - { "file": "adminRoutes", "to": "/admin" }, - { "file": "authRoutes", "to": "/auth" }, - { "file": "fileApiRoutes", "to": "/files" } - ] -} \ No newline at end of file diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts deleted file mode 100644 index 204d420..0000000 --- a/src/server/routes/api/v0/primaryApi.ts +++ /dev/null @@ -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 -} diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts deleted file mode 100644 index 6c031b4..0000000 --- a/src/server/routes/api/v1/account.ts +++ /dev/null @@ -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 & { 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` -// @Jack5079 make typings better if possible - -type Validator, 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[K] - } : {}), ctx: Context) => Accounts.Account[T] | Message - -// this type is so stupid stg -type ValidatorWithSettings> = { - acceptsNull: true, - validator: Validator -} | { - acceptsNull?: false, - validator: Validator -} - -const validators: { - [T in keyof Partial]: - Validator | ValidatorWithSettings -} = { - 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`, - `Hello there! Your email address (${target.email}) has been disconnected from the monofile account ${target.username}. 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`, - `Hello there! You are recieving this message because you decided to link your email, ${ - params.email.split("@")[0] - }@${ - params.email.split("@")[1] - }, to your account, ${ - target.username - }. If you would like to continue, please click here, 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`, - `Hello there! Your password on your account, ${target.username}, has been updated` - + `${actor != target ? ` by ${actor.username}` : ""}. ` - + `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`, - `Hello there! Your username on your account, ${target.username}, has been updated` - + `${actor != target ? ` by ${actor.username}` : ""} to ${params.username}. ` - + `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, - acceptsNull: false - }) as ValidatorWithSettings - - 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, ${ - acc.username - }, 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 -} diff --git a/src/server/routes/api/v1/api.json b/src/server/routes/api/v1/api.json deleted file mode 100644 index 7e5affe..0000000 --- a/src/server/routes/api/v1/api.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "v1", - "baseURL": "/api/v1", - "mount": [ - "account", - "session", - { - "file": "file/index", - "to": "/file" - }, - { - "file": "file/individual", - "to": "/file" - } - ] -} \ No newline at end of file diff --git a/src/server/routes/api/v1/file/index.ts b/src/server/routes/api/v1/file/index.ts deleted file mode 100644 index 5a2ee98..0000000 --- a/src/server/routes/api/v1/file/index.ts +++ /dev/null @@ -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 -} diff --git a/src/server/routes/api/v1/file/individual.ts b/src/server/routes/api/v1/file/individual.ts deleted file mode 100644 index 05621af..0000000 --- a/src/server/routes/api/v1/file/individual.ts +++ /dev/null @@ -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 -} diff --git a/src/server/routes/api/web/preview.ts b/src/server/routes/api/web/preview.ts deleted file mode 100644 index 052a2cc..0000000 --- a/src/server/routes/api/web/preview.ts +++ /dev/null @@ -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( - "", - (file.mime.startsWith("image/") - ? `` - : file.mime.startsWith("video/") - ? ` - - - - ` + - // quick lazy fix as a fallback - // maybe i'll improve this later, but probably not. - ((file.sizeInBytes || 0) >= 26214400 - ? ` - - ` - : "") - : "") + - (fileOwner?.embed?.largeImage && - file.visibility != "anonymous" && - file.mime.startsWith("image/") - ? `` - : "") + - `\n` - ) - .replace( - "", - file.mime.startsWith("image/") - ? `
` - : file.mime.startsWith("video/") - ? `
` - : file.mime.startsWith("audio/") - ? `
` - : "" - ) - .replaceAll( - "$Uploader", - !file.owner || file.visibility == "anonymous" - ? "Anonymous" - : `@${fileOwner?.username || "Deleted User"}` - ) - ) - } else return ServeError(ctx, 404, "file not found") - }) - - return router -} \ No newline at end of file diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json deleted file mode 100644 index a5a0c6a..0000000 --- a/src/server/tsconfig.json +++ /dev/null @@ -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 ''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" } - ] -} diff --git a/src/style/_base.scss b/src/style/_base.scss index 7aceb3d..6ddf38f 100644 --- a/src/style/_base.scss +++ b/src/style/_base.scss @@ -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; } -} \ No newline at end of file + + ::-webkit-scrollbar-track { + background-color: #222222; + } + + ::-webkit-scrollbar-thumb { + background-color: #333; + + &:hover { + background-color: #373737; + } + } +} diff --git a/src/style/app/uploader/add_new_files.scss b/src/style/app/uploader/add_new_files.scss index 5e9db3a..0a5bf9f 100644 --- a/src/style/app/uploader/add_new_files.scss +++ b/src/style/app/uploader/add_new_files.scss @@ -1,4 +1,4 @@ -#uploadWindow { +main { #add_new_files { background-color:#191919; border: 1px solid gray; @@ -112,4 +112,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/style/app/uploader/file.scss b/src/style/app/uploader/file.scss index 9b0a813..dd3ff42 100644 --- a/src/style/app/uploader/file.scss +++ b/src/style/app/uploader/file.scss @@ -1,6 +1,6 @@ // should probably start using mixins for thingss like this -#uploadWindow { +main { .file { background-color:#191919; border: 1px solid gray; diff --git a/src/style/app/uploads.scss b/src/style/app/uploads.scss index ee53f35..c969cc1 100644 --- a/src/style/app/uploads.scss +++ b/src/style/app/uploads.scss @@ -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; } -} \ No newline at end of file +} diff --git a/src/style/downloads.scss b/src/style/downloads.scss index 81ff36e..9e6df19 100644 --- a/src/style/downloads.scss +++ b/src/style/downloads.scss @@ -19,8 +19,8 @@ } } -#uploadWindow { +main { img, video, audio { width:100%; } -} \ No newline at end of file +} diff --git a/src/style/themes/classy.scss b/src/style/themes/classy.scss index 7bdd382..4a84dd9 100644 --- a/src/style/themes/classy.scss +++ b/src/style/themes/classy.scss @@ -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; } -} \ No newline at end of file +} diff --git a/src/svelte/App.svelte b/src/svelte/App.svelte deleted file mode 100644 index c864eb4..0000000 --- a/src/svelte/App.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - -
- - - -
\ No newline at end of file diff --git a/src/svelte/elem/prompts/OptionPicker.svelte b/src/svelte/elem/prompts/OptionPicker.svelte deleted file mode 100644 index bf45376..0000000 --- a/src/svelte/elem/prompts/OptionPicker.svelte +++ /dev/null @@ -1,80 +0,0 @@ - - -{#if activeModal} -
- - -
-{/if} \ No newline at end of file diff --git a/src/svelte/elem/prompts/account.ts b/src/svelte/elem/prompts/account.ts deleted file mode 100644 index dadccae..0000000 --- a/src/svelte/elem/prompts/account.ts +++ /dev/null @@ -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() - }) - } - }) -} diff --git a/src/svelte/elem/prompts/admin.ts b/src/svelte/elem/prompts/admin.ts deleted file mode 100644 index 3b71701..0000000 --- a/src/svelte/elem/prompts/admin.ts +++ /dev/null @@ -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() - }) - - } - }) - } - }) -} \ No newline at end of file diff --git a/src/svelte/elem/prompts/uploads.ts b/src/svelte/elem/prompts/uploads.ts deleted file mode 100644 index 6fca833..0000000 --- a/src/svelte/elem/prompts/uploads.ts +++ /dev/null @@ -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 - - } - - } - - }) -} \ No newline at end of file diff --git a/src/svelte/elem/pulldowns/Accounts.svelte b/src/svelte/elem/pulldowns/Accounts.svelte deleted file mode 100644 index 5fd9121..0000000 --- a/src/svelte/elem/pulldowns/Accounts.svelte +++ /dev/null @@ -1,243 +0,0 @@ - - - - - {#if $account} -
-

- Hey there, @{$account.username} -

- -
- -
-

Account

-
- - - - - - - - {#if !$account.admin} - - {/if} - -
-

Uploads

-
- - - - - -
-

Customization

-
- - - - - - - - {#if $refreshNeeded} - - {/if} - -
-

Sessions

-
- - - - - - {#if $account.admin} - -
-

Admin

-
- - - - - - - - - - - - - - {/if} -


{$account.id}

-
-
- {:else} -
-
-

monofile accounts

-

Gain control of your uploads.

- - {#if targetAction} - -
- {#if !$serverStats?.accounts.registrationEnabled && targetAction == "create"} -
-
-

Account registration has been disabled by this instance's owner

-
-
- {/if} - - {#if authError} -
-
-

{authError.status} {authError.message}

-
-
- {/if} - - - - - - {#if targetAction == "login"} - - {/if} - -
- - {:else} - -
- - -
- - {/if} -
-
- {/if} -
\ No newline at end of file diff --git a/src/svelte/elem/pulldowns/Files.svelte b/src/svelte/elem/pulldowns/Files.svelte deleted file mode 100644 index e607820..0000000 --- a/src/svelte/elem/pulldowns/Files.svelte +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - {#if $account?.username}
- - -
- - {#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)} -
- -
-
-

{file.filename}

-

- {file.visibility||"public"}  - {file.id}  —  {file.mime.split(";")[0]} - {#if file.reserved} -
- uploading  - Uploading... - {/if} - {#if file.tag} -
- tag  - {file.tag} - {/if} -

-
- -
-
- {/each} -
-
- {:else} -
-
-

Log in to view uploads

- -
-
- {/if} - - \ No newline at end of file diff --git a/src/svelte/elem/stores.ts b/src/svelte/elem/stores.ts deleted file mode 100644 index 7eb9589..0000000 --- a/src/svelte/elem/stores.ts +++ /dev/null @@ -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() -export let account = writable() -export let serverStats = writable() -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() \ No newline at end of file diff --git a/src/svelte/global.d.ts b/src/svelte/global.d.ts deleted file mode 100644 index 0e72969..0000000 --- a/src/svelte/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/src/svelte/index.ts b/src/svelte/index.ts deleted file mode 100644 index 7accca1..0000000 --- a/src/svelte/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import App from "./App.svelte" - -new App({ - target: document.body -}) diff --git a/src/svelte/tsconfig.json b/src/svelte/tsconfig.json deleted file mode 100644 index 4e01004..0000000 --- a/src/svelte/tsconfig.json +++ /dev/null @@ -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" } - ] -} \ No newline at end of file diff --git a/assets/apple-touch-icon.png b/static/assets/apple-touch-icon.png similarity index 100% rename from assets/apple-touch-icon.png rename to static/assets/apple-touch-icon.png diff --git a/assets/banner.png b/static/assets/banner.png similarity index 100% rename from assets/banner.png rename to static/assets/banner.png diff --git a/assets/banner.xcf b/static/assets/banner.xcf similarity index 100% rename from assets/banner.xcf rename to static/assets/banner.xcf diff --git a/assets/fonts/FiraCode-Bold.ttf b/static/assets/fonts/FiraCode-Bold.ttf similarity index 100% rename from assets/fonts/FiraCode-Bold.ttf rename to static/assets/fonts/FiraCode-Bold.ttf diff --git a/assets/fonts/FiraCode-Light.ttf b/static/assets/fonts/FiraCode-Light.ttf similarity index 100% rename from assets/fonts/FiraCode-Light.ttf rename to static/assets/fonts/FiraCode-Light.ttf diff --git a/assets/fonts/FiraCode-Medium.ttf b/static/assets/fonts/FiraCode-Medium.ttf similarity index 100% rename from assets/fonts/FiraCode-Medium.ttf rename to static/assets/fonts/FiraCode-Medium.ttf diff --git a/assets/fonts/FiraCode-Regular.ttf b/static/assets/fonts/FiraCode-Regular.ttf similarity index 100% rename from assets/fonts/FiraCode-Regular.ttf rename to static/assets/fonts/FiraCode-Regular.ttf diff --git a/assets/fonts/FiraCode-SemiBold.ttf b/static/assets/fonts/FiraCode-SemiBold.ttf similarity index 100% rename from assets/fonts/FiraCode-SemiBold.ttf rename to static/assets/fonts/FiraCode-SemiBold.ttf diff --git a/assets/fonts/Inconsolata-Black.ttf b/static/assets/fonts/Inconsolata-Black.ttf similarity index 100% rename from assets/fonts/Inconsolata-Black.ttf rename to static/assets/fonts/Inconsolata-Black.ttf diff --git a/assets/fonts/Inconsolata-Bold.ttf b/static/assets/fonts/Inconsolata-Bold.ttf similarity index 100% rename from assets/fonts/Inconsolata-Bold.ttf rename to static/assets/fonts/Inconsolata-Bold.ttf diff --git a/assets/fonts/Inconsolata-ExtraBold.ttf b/static/assets/fonts/Inconsolata-ExtraBold.ttf similarity index 100% rename from assets/fonts/Inconsolata-ExtraBold.ttf rename to static/assets/fonts/Inconsolata-ExtraBold.ttf diff --git a/assets/fonts/Inconsolata-ExtraLight.ttf b/static/assets/fonts/Inconsolata-ExtraLight.ttf similarity index 100% rename from assets/fonts/Inconsolata-ExtraLight.ttf rename to static/assets/fonts/Inconsolata-ExtraLight.ttf diff --git a/assets/fonts/Inconsolata-Light.ttf b/static/assets/fonts/Inconsolata-Light.ttf similarity index 100% rename from assets/fonts/Inconsolata-Light.ttf rename to static/assets/fonts/Inconsolata-Light.ttf diff --git a/assets/fonts/Inconsolata-Medium.ttf b/static/assets/fonts/Inconsolata-Medium.ttf similarity index 100% rename from assets/fonts/Inconsolata-Medium.ttf rename to static/assets/fonts/Inconsolata-Medium.ttf diff --git a/assets/fonts/Inconsolata-Regular.ttf b/static/assets/fonts/Inconsolata-Regular.ttf similarity index 100% rename from assets/fonts/Inconsolata-Regular.ttf rename to static/assets/fonts/Inconsolata-Regular.ttf diff --git a/assets/fonts/Inconsolata-SemiBold.ttf b/static/assets/fonts/Inconsolata-SemiBold.ttf similarity index 100% rename from assets/fonts/Inconsolata-SemiBold.ttf rename to static/assets/fonts/Inconsolata-SemiBold.ttf diff --git a/assets/fonts/SourceSansPro-Bold.ttf b/static/assets/fonts/SourceSansPro-Bold.ttf similarity index 100% rename from assets/fonts/SourceSansPro-Bold.ttf rename to static/assets/fonts/SourceSansPro-Bold.ttf diff --git a/assets/fonts/SourceSansPro-BoldItalic.ttf b/static/assets/fonts/SourceSansPro-BoldItalic.ttf similarity index 100% rename from assets/fonts/SourceSansPro-BoldItalic.ttf rename to static/assets/fonts/SourceSansPro-BoldItalic.ttf diff --git a/assets/fonts/SourceSansPro-Italic.ttf b/static/assets/fonts/SourceSansPro-Italic.ttf similarity index 100% rename from assets/fonts/SourceSansPro-Italic.ttf rename to static/assets/fonts/SourceSansPro-Italic.ttf diff --git a/assets/fonts/SourceSansPro-Regular.ttf b/static/assets/fonts/SourceSansPro-Regular.ttf similarity index 100% rename from assets/fonts/SourceSansPro-Regular.ttf rename to static/assets/fonts/SourceSansPro-Regular.ttf diff --git a/assets/fonts/SourceSansPro-SemiBold.ttf b/static/assets/fonts/SourceSansPro-SemiBold.ttf similarity index 100% rename from assets/fonts/SourceSansPro-SemiBold.ttf rename to static/assets/fonts/SourceSansPro-SemiBold.ttf diff --git a/assets/fonts/SourceSansPro-SemiBoldItalic.ttf b/static/assets/fonts/SourceSansPro-SemiBoldItalic.ttf similarity index 100% rename from assets/fonts/SourceSansPro-SemiBoldItalic.ttf rename to static/assets/fonts/SourceSansPro-SemiBoldItalic.ttf diff --git a/assets/fonts/fira_code.css b/static/assets/fonts/fira_code.css similarity index 61% rename from assets/fonts/fira_code.css rename to static/assets/fonts/fira_code.css index 52e189a..a7514ab 100644 --- a/assets/fonts/fira_code.css +++ b/static/assets/fonts/fira_code.css @@ -1,34 +1,34 @@ @font-face { font-family: "Fira Code"; - src: url("/static/assets/fonts/FiraCode-Light.ttf"); + src: url("/assets/fonts/FiraCode-Light.ttf"); font-weight: 300; font-style: normal; } @font-face { font-family: "Fira Code"; - src: url("/static/assets/fonts/FiraCode-Regular.ttf"); + src: url("/assets/fonts/FiraCode-Regular.ttf"); font-weight: 400; font-style: normal; } @font-face { font-family: "Fira Code"; - src: url("/static/assets/fonts/FiraCode-Medium.ttf"); + src: url("/assets/fonts/FiraCode-Medium.ttf"); font-weight: 500; font-style: normal; } @font-face { font-family: "Fira Code"; - src: url("/static/assets/fonts/FiraCode-SemiBold.ttf"); + src: url("/assets/fonts/FiraCode-SemiBold.ttf"); font-weight: 600; font-style: normal; } @font-face { font-family: "Fira Code"; - src: url("/static/assets/fonts/FiraCode-Bold.ttf"); + src: url("/assets/fonts/FiraCode-Bold.ttf"); font-weight: 700; font-style: normal; -} \ No newline at end of file +} diff --git a/assets/fonts/inconsolata.css b/static/assets/fonts/inconsolata.css similarity index 60% rename from assets/fonts/inconsolata.css rename to static/assets/fonts/inconsolata.css index 618becd..47b905d 100644 --- a/assets/fonts/inconsolata.css +++ b/static/assets/fonts/inconsolata.css @@ -1,55 +1,55 @@ @font-face { font-family: "Inconsolata"; - src: url("/static/assets/fonts/Inconsolata-ExtraLight.ttf"); + src: url("/assets/fonts/Inconsolata-ExtraLight.ttf"); font-weight: 200; font-style: normal; } @font-face { font-family: "Inconsolata"; - src: url("/static/assets/fonts/Inconsolata-Light.ttf"); + src: url("/assets/fonts/Inconsolata-Light.ttf"); font-weight: 300; font-style: normal; } @font-face { font-family: "Inconsolata"; - src: url("/static/assets/fonts/Inconsolata-Regular.ttf"); + src: url("/assets/fonts/Inconsolata-Regular.ttf"); font-weight: 400; font-style: normal; } @font-face { font-family: "Inconsolata"; - src: url("/static/assets/fonts/Inconsolata-Medium.ttf"); + src: url("/assets/fonts/Inconsolata-Medium.ttf"); font-weight: 500; font-style: normal; } @font-face { font-family: "Inconsolata"; - src: url("/static/assets/fonts/Inconsolata-SemiBold.ttf"); + src: url("/assets/fonts/Inconsolata-SemiBold.ttf"); font-weight: 600; font-style: normal; } @font-face { font-family: "Inconsolata"; - src: url("/static/assets/fonts/Inconsolata-Bold.ttf"); + src: url("/assets/fonts/Inconsolata-Bold.ttf"); font-weight: 700; font-style: normal; } @font-face { font-family: "Inconsolata"; - src: url("/static/assets/fonts/Inconsolata-ExtraBold.ttf"); + src: url("/assets/fonts/Inconsolata-ExtraBold.ttf"); font-weight: 800; font-style: normal; } @font-face { font-family: "Inconsolata"; - src: url("/static/assets/fonts/Inconsolata-Black.ttf"); + src: url("/assets/fonts/Inconsolata-Black.ttf"); font-weight: 900; font-style: normal; -} \ No newline at end of file +} diff --git a/assets/fonts/license b/static/assets/fonts/license similarity index 100% rename from assets/fonts/license rename to static/assets/fonts/license diff --git a/assets/fonts/source_sans.css b/static/assets/fonts/source_sans.css similarity index 59% rename from assets/fonts/source_sans.css rename to static/assets/fonts/source_sans.css index ef8e7e3..b788d99 100644 --- a/assets/fonts/source_sans.css +++ b/static/assets/fonts/source_sans.css @@ -1,41 +1,41 @@ @font-face { font-family: "Source Sans Pro"; - src: url("/static/assets/fonts/SourceSansPro-Regular.ttf"); + src: url("/assets/fonts/SourceSansPro-Regular.ttf"); font-weight: 400; font-style: normal; } @font-face { font-family: "Source Sans Pro"; - src: url("/static/assets/fonts/SourceSansPro-Italic.ttf"); + src: url("/assets/fonts/SourceSansPro-Italic.ttf"); font-weight: 400; font-style: italic; } @font-face { font-family: "Source Sans Pro"; - src: url("/static/assets/fonts/SourceSansPro-SemiBold.ttf"); + src: url("/assets/fonts/SourceSansPro-SemiBold.ttf"); font-weight: 600; font-style: normal; } @font-face { font-family: "Source Sans Pro"; - src: url("/static/assets/fonts/SourceSansPro-SemiBoldItalic.ttf"); + src: url("/assets/fonts/SourceSansPro-SemiBoldItalic.ttf"); font-weight: 600; font-style: italic; } @font-face { font-family: "Source Sans Pro"; - src: url("/static/assets/fonts/SourceSansPro-Bold.ttf"); + src: url("/assets/fonts/SourceSansPro-Bold.ttf"); font-weight: 700; font-style: normal; } @font-face { font-family: "Source Sans Pro"; - src: url("/static/assets/fonts/SourceSansPro-BoldItalic.ttf"); + src: url("/assets/fonts/SourceSansPro-BoldItalic.ttf"); font-weight: 700; font-style: italic; -} \ No newline at end of file +} diff --git a/assets/icons/README.md b/static/assets/icons/README.md similarity index 100% rename from assets/icons/README.md rename to static/assets/icons/README.md diff --git a/assets/icons/admin/change_file_id.svg b/static/assets/icons/admin/change_file_id.svg similarity index 100% rename from assets/icons/admin/change_file_id.svg rename to static/assets/icons/admin/change_file_id.svg diff --git a/assets/icons/admin/delete_file.svg b/static/assets/icons/admin/delete_file.svg similarity index 100% rename from assets/icons/admin/delete_file.svg rename to static/assets/icons/admin/delete_file.svg diff --git a/assets/icons/admin/elevate_user.svg b/static/assets/icons/admin/elevate_user.svg similarity index 100% rename from assets/icons/admin/elevate_user.svg rename to static/assets/icons/admin/elevate_user.svg diff --git a/assets/icons/anonymous.svg b/static/assets/icons/anonymous.svg similarity index 100% rename from assets/icons/anonymous.svg rename to static/assets/icons/anonymous.svg diff --git a/assets/icons/change_email.svg b/static/assets/icons/change_email.svg similarity index 100% rename from assets/icons/change_email.svg rename to static/assets/icons/change_email.svg diff --git a/assets/icons/change_password.svg b/static/assets/icons/change_password.svg similarity index 100% rename from assets/icons/change_password.svg rename to static/assets/icons/change_password.svg diff --git a/assets/icons/change_username.svg b/static/assets/icons/change_username.svg similarity index 100% rename from assets/icons/change_username.svg rename to static/assets/icons/change_username.svg diff --git a/assets/icons/delete.svg b/static/assets/icons/delete.svg similarity index 100% rename from assets/icons/delete.svg rename to static/assets/icons/delete.svg diff --git a/assets/icons/delete_account.svg b/static/assets/icons/delete_account.svg similarity index 100% rename from assets/icons/delete_account.svg rename to static/assets/icons/delete_account.svg diff --git a/assets/icons/disconnect_email.svg b/static/assets/icons/disconnect_email.svg similarity index 100% rename from assets/icons/disconnect_email.svg rename to static/assets/icons/disconnect_email.svg diff --git a/assets/icons/error.svg b/static/assets/icons/error.svg similarity index 100% rename from assets/icons/error.svg rename to static/assets/icons/error.svg diff --git a/assets/icons/file.svg b/static/assets/icons/file.svg similarity index 100% rename from assets/icons/file.svg rename to static/assets/icons/file.svg diff --git a/assets/icons/file_icon.svg b/static/assets/icons/file_icon.svg similarity index 100% rename from assets/icons/file_icon.svg rename to static/assets/icons/file_icon.svg diff --git a/assets/icons/image.svg b/static/assets/icons/image.svg similarity index 100% rename from assets/icons/image.svg rename to static/assets/icons/image.svg diff --git a/assets/icons/link.svg b/static/assets/icons/link.svg similarity index 100% rename from assets/icons/link.svg rename to static/assets/icons/link.svg diff --git a/assets/icons/logout.svg b/static/assets/icons/logout.svg similarity index 100% rename from assets/icons/logout.svg rename to static/assets/icons/logout.svg diff --git a/assets/icons/logout_all.svg b/static/assets/icons/logout_all.svg similarity index 100% rename from assets/icons/logout_all.svg rename to static/assets/icons/logout_all.svg diff --git a/assets/icons/mail.svg b/static/assets/icons/mail.svg similarity index 100% rename from assets/icons/mail.svg rename to static/assets/icons/mail.svg diff --git a/assets/icons/more.svg b/static/assets/icons/more.svg similarity index 100% rename from assets/icons/more.svg rename to static/assets/icons/more.svg diff --git a/assets/icons/multiselect.svg b/static/assets/icons/multiselect.svg similarity index 100% rename from assets/icons/multiselect.svg rename to static/assets/icons/multiselect.svg diff --git a/assets/icons/paint.svg b/static/assets/icons/paint.svg similarity index 100% rename from assets/icons/paint.svg rename to static/assets/icons/paint.svg diff --git a/assets/icons/person.svg b/static/assets/icons/person.svg similarity index 100% rename from assets/icons/person.svg rename to static/assets/icons/person.svg diff --git a/assets/icons/pound.svg b/static/assets/icons/pound.svg similarity index 100% rename from assets/icons/pound.svg rename to static/assets/icons/pound.svg diff --git a/assets/icons/private.svg b/static/assets/icons/private.svg similarity index 100% rename from assets/icons/private.svg rename to static/assets/icons/private.svg diff --git a/assets/icons/public.svg b/static/assets/icons/public.svg similarity index 100% rename from assets/icons/public.svg rename to static/assets/icons/public.svg diff --git a/assets/icons/refresh.svg b/static/assets/icons/refresh.svg similarity index 100% rename from assets/icons/refresh.svg rename to static/assets/icons/refresh.svg diff --git a/assets/icons/small_image.svg b/static/assets/icons/small_image.svg similarity index 100% rename from assets/icons/small_image.svg rename to static/assets/icons/small_image.svg diff --git a/assets/icons/tag.svg b/static/assets/icons/tag.svg similarity index 100% rename from assets/icons/tag.svg rename to static/assets/icons/tag.svg diff --git a/assets/icons/tag_remove.svg b/static/assets/icons/tag_remove.svg similarity index 100% rename from assets/icons/tag_remove.svg rename to static/assets/icons/tag_remove.svg diff --git a/assets/icons/update.svg b/static/assets/icons/update.svg similarity index 100% rename from assets/icons/update.svg rename to static/assets/icons/update.svg diff --git a/assets/moller.png b/static/assets/moller.png similarity index 100% rename from assets/moller.png rename to static/assets/moller.png diff --git a/assets/monofileLogo.svg b/static/assets/monofileLogo.svg similarity index 100% rename from assets/monofileLogo.svg rename to static/assets/monofileLogo.svg diff --git a/assets/icons/icon.svg b/static/favicon.svg similarity index 100% rename from assets/icons/icon.svg rename to static/favicon.svg diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..0cd2460 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,3 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte" +const config = { preprocess: vitePreprocess() } +export default config diff --git a/tsconfig.json b/tsconfig.json index 38121b9..74e040f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,31 @@ +// { +// "compilerOptions": { +// // "rootDir": ".", +// "outDir": "dist", +// "resolveJsonModule": true, +// "composite": true, +// "skipLibCheck": true, +// "types": ["vite/client"], +// "module": "ESNext" +// } +// } + { - "compilerOptions": { - "rootDir": ".", - "outDir": ".", - "resolveJsonModule": true, - "composite": true, - "skipLibCheck": true - }, - "files": [ - "package.json", "config.json" - ] + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in } diff --git a/vite.config.ts b/vite.config.ts index 7cae786..717eba2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,21 +1,40 @@ +// import { defineConfig } from "vite" +// import { svelte } from "@sveltejs/vite-plugin-svelte" +// import autoPreprocess from "svelte-preprocess" +// import { resolve } from "path" +// import devServer from "@hono/vite-dev-server" +// import pkg from "./package.json" assert { type: "json" } + +// export default defineConfig({ +// build: { +// target: "esnext", +// outDir: "./dist", +// // assetsDir: "static/vite", +// // rollupOptions: { +// // input: { +// // main: resolve(__dirname, "src/index.html"), +// // download: resolve(__dirname, "src/download.html"), +// // error: resolve(__dirname, "src/error.html"), +// // }, +// // }, +// }, +// define: { +// MONOFILE_VERSION: JSON.stringify(pkg.version), +// }, +// plugins: [ +// svelte({ +// preprocess: autoPreprocess(), +// }), +// ], +// }) + +import { sveltekit } from "@sveltejs/kit/vite" import { defineConfig } from "vite" -import { svelte } from "@sveltejs/vite-plugin-svelte" -import autoPreprocess from "svelte-preprocess" -import { resolve } from "path" +import pkg from "./package.json" assert { type: "json" } + export default defineConfig({ - root: "./src", - build: { - outDir: "../dist", - assetsDir: "static/vite", - rollupOptions: { - input: { - main: resolve(__dirname, "src/index.html"), - download: resolve(__dirname, "src/download.html"), - error: resolve(__dirname, "src/error.html"), - }, - }, + plugins: [sveltekit()], + define: { + MONOFILE_VERSION: JSON.stringify(pkg.version), }, - plugins: [svelte({ - preprocess: autoPreprocess() - })], })