From 5672a6e0a5fb0670eb6d77f2273f213402a468ca Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:14:37 -0700 Subject: [PATCH 01/10] oh wait we dont need that --- .env.example | 3 +-- src/server/lib/config.ts | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 4ca9c33..fef25f4 100644 --- a/.env.example +++ b/.env.example @@ -8,8 +8,7 @@ DISCORD_TOKEN= MAX__DISCORD_FILES= MAX__DISCORD_FILE_SIZE= MAX__UPLOAD_ID_LENGTH= - -TARGET__GUILD= +Y TARGET__CHANNEL= ACCOUNTS__REGISTRATION_ENABLED= diff --git a/src/server/lib/config.ts b/src/server/lib/config.ts index 0fda6d3..aa01f21 100644 --- a/src/server/lib/config.ts +++ b/src/server/lib/config.ts @@ -9,7 +9,6 @@ export interface Configuration { maxDiscordFiles: number maxDiscordFileSize: number maxUploadIdLength: number - targetGuild: string targetChannel: string accounts: { registrationEnabled: boolean @@ -49,7 +48,6 @@ export default { maxDiscordFiles: Number(process.env.MAX__DISCORD_FILES), maxDiscordFileSize: Number(process.env.MAX__DISCORD_FILE_SIZE), maxUploadIdLength: Number(process.env.MAX__UPLOAD_ID_LENGTH), - targetGuild: process.env.TARGET__GUILD, targetChannel: process.env.TARGET__CHANNEL, accounts: { registrationEnabled: From ff46b0302636a5ef9129bd09e6e44ae27b8d05b5 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:14:53 -0700 Subject: [PATCH 02/10] Onhfuckfuck --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index fef25f4..76a2d76 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,6 @@ DISCORD_TOKEN= MAX__DISCORD_FILES= MAX__DISCORD_FILE_SIZE= MAX__UPLOAD_ID_LENGTH= -Y TARGET__CHANNEL= ACCOUNTS__REGISTRATION_ENABLED= From 364c3627a45cc3e6d0d336debf2710b254666bf2 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:24:50 -0700 Subject: [PATCH 03/10] /api/v1/info --- src/server/index.ts | 28 ++++++++++++++-------------- src/server/routes/api/v1/api.json | 1 + src/server/routes/api/v1/info.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 src/server/routes/api/v1/info.ts diff --git a/src/server/index.ts b/src/server/index.ts index 9cf2e34..a96c6da 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,13 +4,10 @@ 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, { ClientConfiguration } from "./lib/config.js" +import config from "./lib/config.js" const app = new Hono() @@ -60,16 +57,6 @@ if (config.forceSSL) { }) } -app.get("/server", (ctx) => - ctx.json({ - version: pkg.version, - files: Object.keys(files.files).length, - maxDiscordFiles: config.maxDiscordFiles, - maxDiscordFileSize: config.maxDiscordFileSize, - accounts: config.accounts, - } as ClientConfiguration) -) - // funcs // init data @@ -87,6 +74,19 @@ apiRouter.loadAPIMethods().then(() => { console.log("API OK!") // moved here to ensure it's matched last + app.get("/server", async (ctx) => + app.fetch( + new Request( + new URL( + "/api/v1/info", + ctx.req.raw.url + ).href, + ctx.req.raw + ), + ctx.env + ) + ) + app.get("/:fileId", async (ctx) => app.fetch( new Request( diff --git a/src/server/routes/api/v1/api.json b/src/server/routes/api/v1/api.json index 52553b5..fc46ef7 100644 --- a/src/server/routes/api/v1/api.json +++ b/src/server/routes/api/v1/api.json @@ -4,6 +4,7 @@ "mount": [ "account", "session", + "info", { "file": "file/index", "to": "/file" diff --git a/src/server/routes/api/v1/info.ts b/src/server/routes/api/v1/info.ts new file mode 100644 index 0000000..392f011 --- /dev/null +++ b/src/server/routes/api/v1/info.ts @@ -0,0 +1,29 @@ +import { Hono } from "hono" +import * as Accounts from "../../../lib/accounts.js" +import { HttpBindings } from "@hono/node-server" +import pkg from "../../../../../package.json" assert {type: "json"} +import config, { ClientConfiguration } from "../../../lib/config.js" +import type Files from "../../../lib/files.js" + +const router = new Hono<{ + Variables: { + account: Accounts.Account + }, + Bindings: HttpBindings +}>() + +export default function(files: Files) { + + router.get("/", async (ctx) => + ctx.json({ + version: pkg.version, + files: Object.keys(files.files).length, + totalSize: Object.values(files.files).filter(e => e.sizeInBytes).reduce((acc,cur)=>acc+cur.sizeInBytes!,0), + maxDiscordFiles: config.maxDiscordFiles, + maxDiscordFileSize: config.maxDiscordFileSize, + accounts: config.accounts + } as ClientConfiguration) + ) + + return router +} From e4a4e24d6ccf5ad07d6e45262488f792df7c55af Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:46:37 -0700 Subject: [PATCH 04/10] add some stuff --- src/server/lib/config.ts | 5 +++++ src/server/routes/api/v1/info.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/server/lib/config.ts b/src/server/lib/config.ts index aa01f21..e6103c0 100644 --- a/src/server/lib/config.ts +++ b/src/server/lib/config.ts @@ -15,6 +15,7 @@ export interface Configuration { requiredForUpload: boolean } mail: { + enabled: boolean transport: { host: string port: number @@ -31,6 +32,8 @@ export interface Configuration { export interface ClientConfiguration { version: string files: number + totalSize: number + mailEnabled: boolean maxDiscordFiles: number maxDiscordFileSize: number accounts: { @@ -56,6 +59,8 @@ export default { }, mail: { + enabled: ["HOST","PORT","SEND_FROM","USER","PASS"].every(e => Boolean(process.env[`MAIL__${e}`])), + transport: { host: process.env.MAIL__HOST, port: Number(process.env.MAIL__PORT), diff --git a/src/server/routes/api/v1/info.ts b/src/server/routes/api/v1/info.ts index 392f011..72606bd 100644 --- a/src/server/routes/api/v1/info.ts +++ b/src/server/routes/api/v1/info.ts @@ -21,7 +21,8 @@ export default function(files: Files) { totalSize: Object.values(files.files).filter(e => e.sizeInBytes).reduce((acc,cur)=>acc+cur.sizeInBytes!,0), maxDiscordFiles: config.maxDiscordFiles, maxDiscordFileSize: config.maxDiscordFileSize, - accounts: config.accounts + accounts: config.accounts, + mailEnabled: config.mail.enabled } as ClientConfiguration) ) From 8a26ace11f833c4f3b8d2b048b70be4c9574eff2 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:25:54 -0700 Subject: [PATCH 05/10] initial commit --- package-lock.json | 11 +++- package.json | 3 +- src/server/lib/files.ts | 56 +++++------------- src/server/lib/middleware.ts | 23 +++----- src/server/lib/schemas/accounts.ts | 21 +++++++ src/server/lib/schemas/files.ts | 18 ++++++ src/server/lib/schemas/index.ts | 2 + src/server/routes/api/v0/authRoutes.ts | 3 +- src/server/routes/api/v0/fileApiRoutes.ts | 4 -- src/server/routes/api/v1/account.ts | 70 ++++------------------- src/server/routes/api/v1/session.ts | 2 +- src/server/{ => tools}/cli.ts | 10 ++-- 12 files changed, 93 insertions(+), 130 deletions(-) create mode 100644 src/server/lib/schemas/accounts.ts create mode 100644 src/server/lib/schemas/files.ts create mode 100644 src/server/lib/schemas/index.ts rename src/server/{ => tools}/cli.ts (92%) diff --git a/package-lock.json b/package-lock.json index c882d2e..c21765a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,8 @@ "multer": "^1.4.5-lts.1", "node-fetch": "^3.3.2", "nodemailer": "^6.9.3", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "zod": "^3.23.5" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^2.4.6", @@ -1941,6 +1942,14 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/zod": { + "version": "3.23.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.5.tgz", + "integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 15f91f9..af8bc38 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "multer": "^1.4.5-lts.1", "node-fetch": "^3.3.2", "nodemailer": "^6.9.3", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "zod": "^3.23.5" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^2.4.6", diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index 0145e78..92b0d1b 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -8,22 +8,23 @@ import config, { Configuration } from "./config.js" import "dotenv/config" import * as Accounts from "./accounts.js" +import { z } from "zod" +import * as schemas from "./schemas/files.js" -export let id_check_regex = /[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/ export let alphanum = Array.from( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" ) // bad solution but whatever -export type FileVisibility = "public" | "anonymous" | "private" +export type FileVisibility = z.infer /** * @description Generates an alphanumeric string, used for files * @param length Length of the ID * @returns a random alphanumeric string */ -export function generateFileId(length: number = 5) { +export function generateFileId(length: number = 5): z.infer { let fid = "" for (let i = 0; i < length; i++) { fid += alphanum[crypto.randomInt(0, alphanum.length)] @@ -31,35 +32,7 @@ export function generateFileId(length: number = 5) { return fid } -/** - * @description Assert multiple conditions... this exists out of pure laziness - * @param conditions - */ - -function multiAssert( - conditions: Map -) { - for (let [cond, err] of conditions.entries()) { - if (cond) return err - } -} - -export type FileUploadSettings = Partial> & - Pick & { uploadId?: string } - -export interface FilePointer { - filename: string - mime: string - messageids: string[] - owner?: string - sizeInBytes?: number - tag?: string - visibility?: FileVisibility - reserved?: boolean - chunkSize?: number - lastModified?: number - md5?: string -} +export type FilePointer = z.infer export interface StatusCodeError { status: number @@ -470,9 +443,9 @@ export class UploadStream extends Writable { sizeInBytes: this.filled, visibility: ogf ? ogf.visibility - : this.owner - ? Accounts.getFromId(this.owner)?.defaultFileVisibility - : undefined, + : this.owner + && Accounts.getFromId(this.owner)?.defaultFileVisibility + || "public", // so that json.stringify doesnt include tag:undefined ...((ogf || {}).tag ? { tag: ogf.tag } : {}), @@ -527,12 +500,11 @@ export class UploadStream extends Writable { 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")) + + let check = schemas.FileId.safeParse(id); + + if (!check.success) + return this.destroy(new WebError(400, check.error.message)) if (this.files.files[id] && this.files.files[id].owner != this.owner) return this.destroy(new WebError(403, "you don't own this file")) @@ -717,4 +689,4 @@ export default class Files { throw err }) } -} +} \ No newline at end of file diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index a7a9cba..61e58da 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -3,6 +3,7 @@ import type { Context, Handler as RequestHandler } from "hono" import ServeError from "../lib/errors.js" import * as auth from "./auth.js" import { setCookie } from "hono/cookie" +import { ZodObject } from "zod" /** * @description Middleware which adds an account, if any, to ctx.get("account") @@ -102,20 +103,10 @@ export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", a httpOnly: true }) -type SchemeType = "array" | "object" | "string" | "number" | "boolean" - -interface SchemeObject { - type: "object" - children: { - [key: string]: SchemeParameter +export const scheme = function(scheme: ZodObject): RequestHandler { + return function(ctx, next) { + let chk = scheme.safeParse(ctx.req.json()) + if (chk.success) next() + else ServeError(ctx, 400, chk.error.message) } -} - -interface SchemeArray { - type: "array" - children: - | SchemeParameter /* All children of the array must be this type */ - | SchemeParameter[] /* Array must match this pattern */ -} - -type SchemeParameter = SchemeType | SchemeObject | SchemeArray +} \ No newline at end of file diff --git a/src/server/lib/schemas/accounts.ts b/src/server/lib/schemas/accounts.ts new file mode 100644 index 0000000..4d85cc8 --- /dev/null +++ b/src/server/lib/schemas/accounts.ts @@ -0,0 +1,21 @@ +import {z} from "zod" +import { FileVisibility } from "./files.js" + +export const StringPassword = z.string().min(8,"password must be at least 8 characters") +export const Password = + z.object({ + hash: z.string(), + salt: z.string() + }) +export const Username = + z.string().min(3, "username too short").max(20, "username too long").regex(/[A-Za-z0-9_\-\.]+/, "username contains invalid characters") +export const Account = + z.object({ + id: z.string(), + username: Username, + email: z.optional(z.string().email("must be an email")), + password: Password, + files: z.array(z.string()), + admin: z.boolean(), + defaultFileVisibility: FileVisibility + }) \ No newline at end of file diff --git a/src/server/lib/schemas/files.ts b/src/server/lib/schemas/files.ts new file mode 100644 index 0000000..cd23dcf --- /dev/null +++ b/src/server/lib/schemas/files.ts @@ -0,0 +1,18 @@ +import {z} from "zod" +import config from "../config.js" + +export const FileId = z.string().regex(/[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/).max(config.maxUploadIdLength) +export const FileVisibility = z.enum(["public", "anonymous", "private"]) +export const FileTag = z.string().toLowerCase().max(30, "tag length too long") +export const FilePointer = z.object({ + filename: z.string().max(256, "filename too long"), + mime: z.string().max(256, "mimetype too long"), + messageids: z.array(z.string()), + owner: z.optional(z.string()), + sizeInBytes: z.optional(z.number()), + tag: z.optional(FileTag), + visibility: z.optional(FileVisibility).default("public"), + chunkSize: z.optional(z.number()), + lastModified: z.optional(z.number()), + md5: z.optional(z.string()) +}) \ No newline at end of file diff --git a/src/server/lib/schemas/index.ts b/src/server/lib/schemas/index.ts new file mode 100644 index 0000000..4d030ab --- /dev/null +++ b/src/server/lib/schemas/index.ts @@ -0,0 +1,2 @@ +export * as AccountSchemas from "./accounts.js" +export * as FileSchemas from "./files.js" \ No newline at end of file diff --git a/src/server/routes/api/v0/authRoutes.ts b/src/server/routes/api/v0/authRoutes.ts index 1017d4c..cb09122 100644 --- a/src/server/routes/api/v0/authRoutes.ts +++ b/src/server/routes/api/v0/authRoutes.ts @@ -14,8 +14,7 @@ import config from "../../../lib/config.js" import ServeError from "../../../lib/errors.js" import Files, { FileVisibility, - generateFileId, - id_check_regex, + generateFileId } from "../../../lib/files.js" import { writeFile } from "fs/promises" diff --git a/src/server/routes/api/v0/fileApiRoutes.ts b/src/server/routes/api/v0/fileApiRoutes.ts index 9b59fff..434b817 100644 --- a/src/server/routes/api/v0/fileApiRoutes.ts +++ b/src/server/routes/api/v0/fileApiRoutes.ts @@ -68,10 +68,6 @@ export default function (files: Files) { let fp = files.files[e] - if (fp.reserved) { - return - } - switch (body.action) { case "delete": files.unlink(e, true) diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index ab6a41d..08c4ecc 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -5,7 +5,7 @@ import { getCookie, setCookie } from "hono/cookie" // Libs -import Files, { id_check_regex } from "../../../lib/files.js" +import Files from "../../../lib/files.js" import * as Accounts from "../../../lib/accounts.js" import * as auth from "../../../lib/auth.js" import { @@ -15,11 +15,14 @@ import { noAPIAccess, requiresAccount, requiresPermissions, + scheme, } from "../../../lib/middleware.js" import ServeError from "../../../lib/errors.js" import { CodeMgr, sendMail } from "../../../lib/mail.js" import Configuration from "../../../lib/config.js" +import { AccountSchemas, FileSchemas } from "../../../lib/schemas/index.js" +import { z } from "zod" const router = new Hono<{ Variables: { @@ -80,13 +83,7 @@ const validators: { | ValidatorWithSettings } = { defaultFileVisibility(actor, target, params) { - if ( - ["public", "private", "anonymous"].includes( - params.defaultFileVisibility - ) - ) - return params.defaultFileVisibility - else return [400, "invalid file visibility"] + return params.defaultFileVisibility }, email: { acceptsNull: true, @@ -109,8 +106,8 @@ const validators: { return undefined } - if (typeof params.email !== "string") - return [400, "email must be string"] + if (!z.string().email().safeParse(typeof params.email).success) + return [400, "bad email"] if (actor.admin) return params.email // send verification email @@ -150,9 +147,6 @@ const validators: { ) 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, @@ -173,25 +167,9 @@ const validators: { ) 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, @@ -207,12 +185,7 @@ const validators: { customCSS: { acceptsNull: true, validator: (actor, target, params) => { - if ( - !params.customCSS || - (params.customCSS.match(id_check_regex)?.[0] == - params.customCSS && - params.customCSS.length <= Configuration.maxUploadIdLength) - ) + if (FileSchemas.FileId.safeParse(params.customCSS).success) return params.customCSS else return [400, "bad file id"] }, @@ -272,7 +245,10 @@ function isMessage(object: any): object is Message { } export default function (files: Files) { - router.post("/", async (ctx) => { + router.post("/", scheme(z.object({ + username: AccountSchemas.Username, + password: AccountSchemas.StringPassword + })), async (ctx) => { const body = await ctx.req.json() if (!Configuration.accounts.registrationEnabled) { return ServeError(ctx, 403, "account registration disabled") @@ -290,28 +266,6 @@ export default function (files: Files) { ) } - 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) diff --git a/src/server/routes/api/v1/session.ts b/src/server/routes/api/v1/session.ts index 590d1f3..7006dad 100644 --- a/src/server/routes/api/v1/session.ts +++ b/src/server/routes/api/v1/session.ts @@ -6,7 +6,7 @@ import { getCookie, setCookie } from "hono/cookie" // Libs -import Files, { id_check_regex } from "../../../lib/files.js" +import Files from "../../../lib/files.js" import * as Accounts from "../../../lib/accounts.js" import * as auth from "../../../lib/auth.js" import { diff --git a/src/server/cli.ts b/src/server/tools/cli.ts similarity index 92% rename from src/server/cli.ts rename to src/server/tools/cli.ts index f6ce73c..6a7fca0 100644 --- a/src/server/cli.ts +++ b/src/server/tools/cli.ts @@ -1,19 +1,19 @@ import fs from "fs" import { stat } from "fs/promises" -import Files from "./lib/files.js" +import Files from "../lib/files.js" import { program } from "commander" import { basename } from "path" import { Writable } from "node:stream" -import config from "./lib/config.js" -import pkg from "../../package.json" assert { type: "json" } +import config from "../lib/config.js" +import pkg from "../../../package.json" assert { type: "json" } import { fileURLToPath } from "url" import { dirname } from "path" // init data const __dirname = dirname(fileURLToPath(import.meta.url)) -if (!fs.existsSync(__dirname + "/../../.data/")) - fs.mkdirSync(__dirname + "/../../.data/") +if (!fs.existsSync(__dirname + "/../../../.data/")) + fs.mkdirSync(__dirname + "/../../../.data/") // discord let files = new Files(config) From f0a245008202385d21d20ab8ab7b5f8b6016b578 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Tue, 30 Apr 2024 23:40:42 -0700 Subject: [PATCH 06/10] i think we're done actually --- src/server/index.ts | 4 +- src/server/lib/files.ts | 3 +- src/server/lib/middleware.ts | 25 ++- src/server/routes/api/v1/account.ts | 191 +++++++----------- src/server/routes/api/v1/api.json | 5 +- .../routes/api/v1/{info.ts => index.ts} | 0 src/server/routes/api/v1/session.ts | 17 +- 7 files changed, 107 insertions(+), 138 deletions(-) rename src/server/routes/api/v1/{info.ts => index.ts} (100%) diff --git a/src/server/index.ts b/src/server/index.ts index a96c6da..158b338 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from "url" import { dirname } from "path" import config from "./lib/config.js" -const app = new Hono() +const app = new Hono({strict: false}) app.get( "/static/assets/*", @@ -78,7 +78,7 @@ apiRouter.loadAPIMethods().then(() => { app.fetch( new Request( new URL( - "/api/v1/info", + "/api/v1", ctx.req.raw.url ).href, ctx.req.raw diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index 92b0d1b..bd38a4c 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -10,6 +10,7 @@ import "dotenv/config" import * as Accounts from "./accounts.js" import { z } from "zod" import * as schemas from "./schemas/files.js" +import { issuesToMessage } from "./middleware.js" export let alphanum = Array.from( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" @@ -504,7 +505,7 @@ export class UploadStream extends Writable { let check = schemas.FileId.safeParse(id); if (!check.success) - return this.destroy(new WebError(400, check.error.message)) + return this.destroy(new WebError(400, issuesToMessage(check.error.issues))) if (this.files.files[id] && this.files.files[id].owner != this.owner) return this.destroy(new WebError(403, "you don't own this file")) diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index 61e58da..c6ab333 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -3,7 +3,7 @@ import type { Context, Handler as RequestHandler } from "hono" import ServeError from "../lib/errors.js" import * as auth from "./auth.js" import { setCookie } from "hono/cookie" -import { ZodObject } from "zod" +import { z } from "zod" /** * @description Middleware which adds an account, if any, to ctx.get("account") @@ -38,7 +38,6 @@ export const requiresAdmin: RequestHandler = function (ctx, next) { * @param tokenPermissions Permissions which your route requires. * @returns Express middleware */ - export const requiresPermissions = function ( ...tokenPermissions: auth.TokenPermission[] ): RequestHandler { @@ -94,6 +93,18 @@ export const assertAPI = function ( } } +export const issuesToMessage = function(issues: z.ZodIssue[]) { + return issues.map(e => `${e.path}: ${e.code} :: ${e.message}`).join("; ") +} + +export const scheme = function(scheme: z.ZodTypeAny): RequestHandler { + return async function(ctx, next) { + let chk = scheme.safeParse(await ctx.req.json()) + if (chk.success) return next() + else return ServeError(ctx, 400, issuesToMessage(chk.error.issues)) + } +} + // Not really middleware but a utility export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), { @@ -101,12 +112,4 @@ export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", a sameSite: "Strict", secure: true, httpOnly: true -}) - -export const scheme = function(scheme: ZodObject): RequestHandler { - return function(ctx, next) { - let chk = scheme.safeParse(ctx.req.json()) - if (chk.success) next() - else ServeError(ctx, 400, chk.error.message) - } -} \ No newline at end of file +}) \ No newline at end of file diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index 08c4ecc..e4ea5a5 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -11,6 +11,7 @@ import * as auth from "../../../lib/auth.js" import { assertAPI, getAccount, + issuesToMessage, login, noAPIAccess, requiresAccount, @@ -43,8 +44,7 @@ type Message = [200 | 400 | 401 | 403 | 429 | 501, string] // @Jack5079 make typings better if possible type Validator< - T extends keyof Partial, - ValueNotNull extends boolean, + T extends keyof Partial > = /** * @param actor The account performing this action @@ -55,38 +55,33 @@ type Validator< actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & - (ValueNotNull extends true - ? { - [K in keyof Pick< - UserUpdateParameters, - T - >]-?: UserUpdateParameters[K] - } - : {}), + { + [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 - } +type SchemedValidator< + T extends keyof Partial +> = { + validator: Validator, + schema: z.ZodTypeAny +} const validators: { - [T in keyof Partial]: - | Validator - | ValidatorWithSettings + [T in keyof Partial]: SchemedValidator } = { - defaultFileVisibility(actor, target, params) { - return params.defaultFileVisibility + defaultFileVisibility: { + schema: FileSchemas.FileVisibility, + validator: (actor, target, params) => { + return params.defaultFileVisibility + } }, email: { - acceptsNull: true, + schema: AccountSchemas.Account.shape.email.optional(), validator: (actor, target, params, ctx) => { if ( !params.currentPassword || // actor on purpose here to allow admins @@ -139,82 +134,62 @@ const validators: { 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 (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 (Accounts.getFromUsername(params.username)) - return [400, "account with this username already exists"] - - 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, + password: { + schema: AccountSchemas.StringPassword, validator: (actor, target, params) => { - if (FileSchemas.FileId.safeParse(params.customCSS).success) - 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 + if ( + !params.currentPassword || // actor on purpose here to allow admins + (params.currentPassword && + Accounts.password.check(actor.id, params.currentPassword)) ) - ) - return [400, "bad embed color"] + return [401, "current password incorrect"] - if (params.embed.largeImage === undefined) { - params.embed.largeImage = target.embed?.largeImage - } else params.embed.largeImage = Boolean(params.embed.largeImage) + 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 params.embed + return Accounts.password.hash(params.password) + } }, - 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"] + username: { + schema: AccountSchemas.Username, + validator: (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 (Accounts.getFromUsername(params.username)) + return [400, "account with this username already exists"] + + 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 + } + }, + admin: { + schema: z.boolean(), + validator: (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"] + } }, } @@ -306,23 +281,11 @@ export default function (files: Files) { `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 + let validator = validators[x]! - if (!validator.acceptsNull && !v) - return [ - 400, - `the ${x} validator does not accept null values`, - ] as Message + let check = validator.schema.safeParse(v) + if (!check.success) + return [400, issuesToMessage(check.error.issues)] return [ x, @@ -391,7 +354,7 @@ export default function (files: Files) { }) }) - router.get("/css", async (ctx) => { + router.get("/:user/css", async (ctx) => { let acc = ctx.get("account") if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`) else return ctx.text("") diff --git a/src/server/routes/api/v1/api.json b/src/server/routes/api/v1/api.json index fc46ef7..25318b7 100644 --- a/src/server/routes/api/v1/api.json +++ b/src/server/routes/api/v1/api.json @@ -4,7 +4,10 @@ "mount": [ "account", "session", - "info", + { + "file": "index", + "to": "/" + }, { "file": "file/index", "to": "/file" diff --git a/src/server/routes/api/v1/info.ts b/src/server/routes/api/v1/index.ts similarity index 100% rename from src/server/routes/api/v1/info.ts rename to src/server/routes/api/v1/index.ts diff --git a/src/server/routes/api/v1/session.ts b/src/server/routes/api/v1/session.ts index 7006dad..cb2f4d6 100644 --- a/src/server/routes/api/v1/session.ts +++ b/src/server/routes/api/v1/session.ts @@ -12,9 +12,12 @@ import * as auth from "../../../lib/auth.js" import { getAccount, login, - requiresAccount + requiresAccount, + scheme } from "../../../lib/middleware.js" import ServeError from "../../../lib/errors.js" +import { AccountSchemas } from "../../../lib/schemas/index.js" +import { z } from "zod" const router = new Hono<{ Variables: { @@ -25,15 +28,11 @@ const router = new Hono<{ router.use(getAccount) export default function (files: Files) { - router.post("/", async (ctx, res) => { + router.post("/",scheme(z.object({ + username: AccountSchemas.Username, + password: AccountSchemas.StringPassword + })), async (ctx) => { const body = await ctx.req.json() - if ( - typeof body.username != "string" || - typeof body.password != "string" - ) { - ServeError(ctx, 400, "please provide a username or password") - return - } if (auth.validate(getCookie(ctx, "auth")!)) { ServeError(ctx, 400, "you are already logged in") From 5155e64b10d6d556b94523366024d7971ab6860a Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Wed, 1 May 2024 00:03:57 -0700 Subject: [PATCH 07/10] i think thats probably etter actually. --- src/server/lib/schemas/files.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/lib/schemas/files.ts b/src/server/lib/schemas/files.ts index cd23dcf..453e2ac 100644 --- a/src/server/lib/schemas/files.ts +++ b/src/server/lib/schemas/files.ts @@ -1,7 +1,10 @@ import {z} from "zod" import config from "../config.js" -export const FileId = z.string().regex(/[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/).max(config.maxUploadIdLength) +export const FileId = z.string() + .regex(/[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/,"file ID uses invalid characters") + .max(config.maxUploadIdLength,"file ID too long") + .min(1, "you... *need* a file ID") export const FileVisibility = z.enum(["public", "anonymous", "private"]) export const FileTag = z.string().toLowerCase().max(30, "tag length too long") export const FilePointer = z.object({ From 60b0308e31ef0b4d65dbd5c8006d6f758551b412 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Wed, 1 May 2024 03:23:12 -0700 Subject: [PATCH 08/10] definitely not done yet --- src/server/lib/DiscordAPI/index.ts | 2 +- src/server/lib/accounts.ts | 11 ++++-- src/server/lib/files.ts | 23 ++++++++---- src/server/lib/schemas/accounts.ts | 53 ++++++++++++++++++++++++++-- src/server/lib/schemas/files.ts | 2 +- src/server/routes/api/v1/account.ts | 30 +++++++++++----- src/server/routes/api/web/preview.ts | 6 ++-- 7 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/server/lib/DiscordAPI/index.ts b/src/server/lib/DiscordAPI/index.ts index 0d3decb..1694da5 100644 --- a/src/server/lib/DiscordAPI/index.ts +++ b/src/server/lib/DiscordAPI/index.ts @@ -7,7 +7,7 @@ import type { Configuration } from "../config.js" const EXPIRE_AFTER = 20 * 60 * 1000 const DISCORD_EPOCH = 1420070400000 // Converts a snowflake ID string into a JS Date object using the provided epoch (in ms), or Discord's epoch if not provided -function convertSnowflakeToDate( +export function convertSnowflakeToDate( snowflake: string | number, epoch = DISCORD_EPOCH ) { diff --git a/src/server/lib/accounts.ts b/src/server/lib/accounts.ts index efbbe03..7cd1386 100644 --- a/src/server/lib/accounts.ts +++ b/src/server/lib/accounts.ts @@ -2,12 +2,14 @@ import crypto from "crypto" import * as auth from "./auth.js"; import { readFile, writeFile } from "fs/promises" import { FileVisibility } from "./files.js"; +import { AccountSchemas } from "./schemas/index.js"; +import { z } from "zod" // this is probably horrible // but i don't even care anymore export let Accounts: Account[] = [] - +/* export interface Account { id : string username : string @@ -25,7 +27,9 @@ export interface Account { color? : string largeImage? : boolean } -} +}*/ + +export type Account = z.infer /** * @description Create a new account. @@ -45,7 +49,8 @@ export async function create(username:string,pwd:string,admin:boolean=false):Pro password: password.hash(pwd), files: [], admin: admin, - defaultFileVisibility: "public" + defaultFileVisibility: "public", + settings: AccountSchemas.Settings.User.parse({}) } ) diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index bd38a4c..3ada911 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -2,7 +2,7 @@ import { readFile, writeFile } from "node:fs/promises" 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 { Client as API, convertSnowflakeToDate } from "./DiscordAPI/index.js" import type { APIAttachment } from "discord-api-types/v10" import config, { Configuration } from "./config.js" import "dotenv/config" @@ -624,28 +624,37 @@ export default class Files { } /** - * @description Update a file from monofile 1.2 to allow for range requests with Content-Length to that file. + * @description Update a file from monofile 1.x to 2.x * @param uploadId Target file's ID */ async update(uploadId: string) { let target_file = this.files[uploadId] - let attachment_sizes = [] + let attachments: APIAttachment[] = [] for (let message of target_file.messageids) { let attachments = (await this.api.fetchMessage(message)).attachments for (let attachment of attachments) { - attachment_sizes.push(attachment.size) + attachments.push(attachment) } } if (!target_file.sizeInBytes) - target_file.sizeInBytes = attachment_sizes.reduce( - (a, b) => a + b, + target_file.sizeInBytes = attachments.reduce( + (a, b) => a + b.size, 0 ) - if (!target_file.chunkSize) target_file.chunkSize = attachment_sizes[0] + if (!target_file.chunkSize) target_file.chunkSize = attachments[0].size + + if (!target_file.lastModified) target_file.lastModified = convertSnowflakeToDate(target_file.messageids[target_file.messageids.length-1]).getTime() + + // this feels like needlessly heavy + // we should probably just do this in an actual readFile idk + if (!target_file.md5) { + let hash = crypto.createHash("md5"); + (await this.readFileStream(uploadId)).pipe(hash).once("end", () => target_file.md5 = hash.digest("hex")) + } } /** diff --git a/src/server/lib/schemas/accounts.ts b/src/server/lib/schemas/accounts.ts index 4d85cc8..57b87ae 100644 --- a/src/server/lib/schemas/accounts.ts +++ b/src/server/lib/schemas/accounts.ts @@ -1,5 +1,5 @@ import {z} from "zod" -import { FileVisibility } from "./files.js" +import { FileId, FileVisibility } from "./files.js" export const StringPassword = z.string().min(8,"password must be at least 8 characters") export const Password = @@ -8,7 +8,52 @@ export const Password = salt: z.string() }) export const Username = - z.string().min(3, "username too short").max(20, "username too long").regex(/[A-Za-z0-9_\-\.]+/, "username contains invalid characters") + z.string() + .min(3, "username too short") + .max(20, "username too long") + .regex(/^[A-Za-z0-9_\-\.]+$/, "username contains invalid characters") + +export namespace Settings { + export const Theme = z.discriminatedUnion("theme", [ + z.object({ + theme: z.literal("catppuccin"), + variant: z.enum(["latte","frappe","macchiato","mocha","adaptive"]), + accent: z.enum([ + "rosewater", + "flamingo", + "pink", + "mauve", + "red", + "maroon", + "peach", + "yellow", + "green", + "teal", + "sky", + "sapphire", + "blue", + "lavender" + ]) + }), + z.object({ + theme: z.literal("custom"), + id: FileId + }) + ]) + export const BarSide = z.enum(["top","left","bottom","right"]) + export const Interface = z.object({ + theme: Theme.default({theme: "catppuccin", variant: "adaptive", accent: "sky"}), + barSide: BarSide.default("left") + }) + export const Links = z.object({ + color: z.string().toLowerCase().length(6).regex(/^[a-f0-9]+$/,"illegal characters").optional(), + largeImage: z.boolean().default(false) + }) + export const User = z.object({ + interface: Interface.default({}), links: Links.default({}) + }) +} + export const Account = z.object({ id: z.string(), @@ -17,5 +62,7 @@ export const Account = password: Password, files: z.array(z.string()), admin: z.boolean(), - defaultFileVisibility: FileVisibility + defaultFileVisibility: FileVisibility, + + settings: Settings.User }) \ No newline at end of file diff --git a/src/server/lib/schemas/files.ts b/src/server/lib/schemas/files.ts index 453e2ac..63800b9 100644 --- a/src/server/lib/schemas/files.ts +++ b/src/server/lib/schemas/files.ts @@ -2,7 +2,7 @@ import {z} from "zod" import config from "../config.js" export const FileId = z.string() - .regex(/[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/,"file ID uses invalid characters") + .regex(/^[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+$/,"file ID uses invalid characters") .max(config.maxUploadIdLength,"file ID too long") .min(1, "you... *need* a file ID") export const FileVisibility = z.enum(["public", "anonymous", "private"]) diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index e4ea5a5..1009b19 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -81,7 +81,7 @@ const validators: { } }, email: { - schema: AccountSchemas.Account.shape.email.optional(), + schema: AccountSchemas.Account.shape.email.nullable(), validator: (actor, target, params, ctx) => { if ( !params.currentPassword || // actor on purpose here to allow admins @@ -191,6 +191,23 @@ const validators: { else return [400, "cannot demote an admin"] } }, + settings: { + schema: AccountSchemas.Settings.User.partial(), + validator: (actor, target, params) => { + let base = AccountSchemas.Settings.User.default({}).parse(target.settings) + + let visit = (bse: Record, nw: Record) => { + for (let [key,value] of Object.entries(nw)) { + if (typeof value == "object") visit(bse[key], value) + else bse[key] = value + } + } + + visit(base, params.settings) + + return AccountSchemas.Settings.User.parse(base) // so that toLowerCase is called again... yeah that's it + } + }, } router.use(getAccount) @@ -246,8 +263,9 @@ export default function (files: Files) { login(ctx, account) return ctx.text("logged in") }) - .catch(() => { - return ServeError(ctx, 500, "internal server error") + .catch((e) => { + console.error(e) + return ServeError(ctx, 500, e instanceof z.ZodError ? issuesToMessage(e.issues) : "internal server error") }) }) @@ -354,11 +372,5 @@ export default function (files: Files) { }) }) - router.get("/:user/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/web/preview.ts b/src/server/routes/api/web/preview.ts index 052a2cc..38d12ad 100644 --- a/src/server/routes/api/web/preview.ts +++ b/src/server/routes/api/web/preview.ts @@ -75,18 +75,18 @@ export default function (files: Files) { ` : "") : "") + - (fileOwner?.embed?.largeImage && + (fileOwner?.settings?.links?.largeImage && file.visibility != "anonymous" && file.mime.startsWith("image/") ? `` : "") + `\n` ) From 459c40beceedd3e3138ed18012e4a86168af7f38 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Wed, 1 May 2024 18:39:38 +0000 Subject: [PATCH 09/10] basic suspension implementation !! NOT READY WAIT FOR CLIENT-V2 SO WE CAN NUKE V0 --- src/server/lib/middleware.ts | 5 ++++- src/server/lib/schemas/accounts.ts | 9 +++++++-- src/server/routes/api/v1/account.ts | 11 ++++++++--- src/server/routes/api/v1/session.ts | 28 ++++++++++++++++------------ 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index c6ab333..f69c118 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -9,7 +9,10 @@ import { z } from "zod" * @description Middleware which adds an account, if any, to ctx.get("account") */ export const getAccount: RequestHandler = function (ctx, next) { - ctx.set("account", Accounts.getFromToken(auth.tokenFor(ctx)!)) + let account = Accounts.getFromToken(auth.tokenFor(ctx)!) + if (account?.suspension) + setCookie(ctx, "auth", "") + ctx.set("account", account) return next() } diff --git a/src/server/lib/schemas/accounts.ts b/src/server/lib/schemas/accounts.ts index 57b87ae..7ff9699 100644 --- a/src/server/lib/schemas/accounts.ts +++ b/src/server/lib/schemas/accounts.ts @@ -53,7 +53,11 @@ export namespace Settings { interface: Interface.default({}), links: Links.default({}) }) } - +export const Suspension = + z.object({ + reason: z.string(), + until: z.number().nullable() + }) export const Account = z.object({ id: z.string(), @@ -64,5 +68,6 @@ export const Account = admin: z.boolean(), defaultFileVisibility: FileVisibility, - settings: Settings.User + settings: Settings.User, + suspension: Suspension.optional() }) \ No newline at end of file diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index 1009b19..c183964 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -101,9 +101,7 @@ const validators: { return undefined } - if (!z.string().email().safeParse(typeof params.email).success) - return [400, "bad email"] - if (actor.admin) return params.email + if (actor.admin) return params.email || undefined // send verification email @@ -191,6 +189,13 @@ const validators: { else return [400, "cannot demote an admin"] } }, + suspension: { + schema: AccountSchemas.Suspension.nullable(), + validator: (actor, target, params) => { + if (!actor.admin) return [400, "only admins can modify suspensions"] + return params.suspension || undefined + } + }, settings: { schema: AccountSchemas.Settings.User.partial(), validator: (actor, target, params) => { diff --git a/src/server/routes/api/v1/session.ts b/src/server/routes/api/v1/session.ts index cb2f4d6..30401e9 100644 --- a/src/server/routes/api/v1/session.ts +++ b/src/server/routes/api/v1/session.ts @@ -34,16 +34,24 @@ export default function (files: Files) { })), async (ctx) => { const body = await ctx.req.json() - if (auth.validate(getCookie(ctx, "auth")!)) { - ServeError(ctx, 400, "you are already logged in") - return - } + if (ctx.get("account")) + return ServeError(ctx, 400, "you are already logged in") const account = Accounts.getFromUsername(body.username) if (!account || !Accounts.password.check(account.id, body.password)) { - ServeError(ctx, 400, "username or password incorrect") - return + return ServeError(ctx, 400, "username or password incorrect") + } + + if (account.suspension) { + if (account.suspension.until && Date.now() > account.suspension.until) delete account.suspension; + else return ServeError( + ctx, + 403, + `account ${account.suspension.until + ? `suspended until ${new Date(account.suspension.until).toUTCString()}` + : "suspended indefinitely" + }: ${account.suspension.reason}`) } login(ctx, account.id) @@ -59,12 +67,8 @@ export default function (files: Files) { }) }) - router.delete("/", (ctx) => { - if (!auth.validate(getCookie(ctx, "auth")!)) { - return ServeError(ctx, 401, "not logged in") - } - - auth.invalidate(getCookie(ctx, "auth")!) + router.delete("/", requiresAccount, (ctx) => { + auth.invalidate(auth.tokenFor(ctx)!) return ctx.text("logged out") }) From 17db90898f1134c192b00a662aa7046cb66f3177 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Wed, 1 May 2024 20:09:11 -0700 Subject: [PATCH 10/10] i dont know what to do ill figure it out later --- src/server/lib/accounts.ts | 4 +-- src/server/lib/files.ts | 40 ++++++++++++++++++++++++-- src/server/lib/middleware.ts | 1 + src/server/routes/api/v1/file/index.ts | 7 +++-- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/server/lib/accounts.ts b/src/server/lib/accounts.ts index 7cd1386..87c2357 100644 --- a/src/server/lib/accounts.ts +++ b/src/server/lib/accounts.ts @@ -149,7 +149,7 @@ export namespace files { * @param fileId The target file's ID * @returns Promise that resolves after accounts.json finishes writing */ - export function index(accountId:string,fileId:string) { + export function index(accountId:string,fileId:string,noWrite:boolean = false) { // maybe replace with a obj like // { x:true } // for faster lookups? not sure if it would be faster @@ -158,7 +158,7 @@ export namespace files { if (acc.files.find(e => e == fileId)) return acc.files.push(fileId) - return save() + if (!noWrite) return save() } /** diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index 3ada911..10c2a39 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -11,6 +11,7 @@ import * as Accounts from "./accounts.js" import { z } from "zod" import * as schemas from "./schemas/files.js" import { issuesToMessage } from "./middleware.js" +import file from "../routes/api/v1/file/index.js" export let alphanum = Array.from( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" @@ -695,8 +696,41 @@ export default class Files { delete this.files[uploadId] if (!noWrite) - this.write().catch((err) => { - throw err - }) + return this.write() + } + + async chown(uploadId: string, newOwner?: string, noWrite: boolean = false) { + let target = this.files[uploadId] + if (target.owner) { + let i = files.deindex(target.owner, uploadId, Boolean(newOwner && noWrite)) + if (i) await i + } + + target.owner = newOwner + if (newOwner) { + let i = files.index(newOwner, uploadId, noWrite) + if (i) await i + } + + if (!noWrite) + return this.write() + } + + async mv(uploadId: string, newId: string, noWrite: boolean = false) { + let target = this.files[uploadId] + if (target.owner) { + let owner = Accounts.getFromId(target.owner) + if (owner) { + owner.files.splice(owner.files.indexOf(uploadId), 1, newId) + if (!noWrite) + await Accounts.save() + } + } + + this.files[newId] = target + delete this.files[uploadId] + + if (!noWrite) + return this.write() } } \ No newline at end of file diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index f69c118..195ad92 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -103,6 +103,7 @@ export const issuesToMessage = function(issues: z.ZodIssue[]) { export const scheme = function(scheme: z.ZodTypeAny): RequestHandler { return async function(ctx, next) { let chk = scheme.safeParse(await ctx.req.json()) + ctx.set("parsedScheme", chk.data) if (chk.success) return next() else return ServeError(ctx, 400, issuesToMessage(chk.error.issues)) } diff --git a/src/server/routes/api/v1/file/index.ts b/src/server/routes/api/v1/file/index.ts index 5a2ee98..0bc47cb 100644 --- a/src/server/routes/api/v1/file/index.ts +++ b/src/server/routes/api/v1/file/index.ts @@ -4,17 +4,20 @@ 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 { getAccount, requiresAccount, requiresPermissions, scheme } 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" +import { z } from "zod" +import { FileSchemas } from "../../../../lib/schemas/index.js" const router = new Hono<{ Variables: { - account: Accounts.Account + account: Accounts.Account, + parsedSchema: any }, Bindings: HttpBindings }>()