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] 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")