From 00fcf4580fcefbc2e954c941489ced54896a5a19 Mon Sep 17 00:00:00 2001 From: stringsplit <77242831+nbitzz@users.noreply.github.com> Date: Sat, 9 Mar 2024 12:43:05 -0800 Subject: [PATCH] hhhh --- src/server/lib/mail.ts | 61 ++++++++++++++++++++++++++++ src/server/lib/middleware.ts | 12 +++++- src/server/routes/api/v1/account.ts | 60 +++++++++++++++++++-------- src/server/routes/api/v1/session.ts | 11 ++--- src/server/routes/api/web/api.json | 3 +- src/server/routes/api/web/go.ts | 41 +++++++++++++++++++ src/server/routes/api/web/preview.ts | 3 +- 7 files changed, 165 insertions(+), 26 deletions(-) create mode 100644 src/server/routes/api/web/go.ts diff --git a/src/server/lib/mail.ts b/src/server/lib/mail.ts index 7f7f266..afab792 100644 --- a/src/server/lib/mail.ts +++ b/src/server/lib/mail.ts @@ -1,6 +1,7 @@ import { createTransport } from "nodemailer" import "dotenv/config" import config from "../../../config.json" assert {type:"json"} +import { generateFileId } from "./files.js" let mailConfig = config.mail, transport = createTransport({ @@ -33,4 +34,64 @@ export function sendMail(to: string, subject: string, content: string) { `` )}

If you do not believe that you are the intended recipient of this email, please disregard this message.`, }) +} + +export namespace CodeMgr { + + export const Intents = [ + "verifyEmail", + "recoverAccount" + ] as const + + export type Intent = typeof Intents[number] + + 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 }> + + // this is stupid whyd i write this + + export class Code { + readonly id: string = generateFileId(12) + readonly for: string + + readonly intent: Intent + + readonly expiryClear: NodeJS.Timeout + + readonly data: any + + 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); + + let byUser = codes[intent].byUser.get(this.for) + if (!byUser) { + byUser = [] + codes[intent].byUser.set(this.for, byUser); + } + + byUser.push(this) + } + + terminate() { + 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) + } + + check(forUser: string) { + return forUser === this.for + } + } + } \ No newline at end of file diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index c63feec..a7a9cba 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -1,7 +1,8 @@ import * as Accounts from "./accounts.js" -import { Handler as RequestHandler } from "hono" +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" /** * @description Middleware which adds an account, if any, to ctx.get("account") @@ -92,6 +93,15 @@ 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 +}) + type SchemeType = "array" | "object" | "string" | "number" | "boolean" interface SchemeObject { diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index 5b2da93..6c031b4 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -1,7 +1,7 @@ // Modules -import { Hono } from "hono" +import { type Context, Hono } from "hono" import { getCookie, setCookie } from "hono/cookie" // Libs @@ -12,12 +12,13 @@ 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 { sendMail } from "../../../lib/mail.js" +import { CodeMgr, sendMail } from "../../../lib/mail.js" import Configuration from "../../../../../config.json" assert {type:"json"} @@ -29,7 +30,7 @@ const router = new Hono<{ }>() type UserUpdateParameters = Partial & { password: string, currentPassword?: string }> -type Message = [200 | 400 | 401 | 403 | 501, 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 @@ -42,12 +43,15 @@ type Validator, ValueNotNull extends b */ (actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & (ValueNotNull extends true ? { [K in keyof Pick]-? : UserUpdateParameters[K] - } : {})) => Accounts.Account[T] | Message + } : {}), ctx: Context) => Accounts.Account[T] | Message // this type is so stupid stg -interface ValidatorWithSettings> { - acceptsNull?: boolean, - validator: Validator // i give upp ill fix this later +type ValidatorWithSettings> = { + acceptsNull: true, + validator: Validator +} | { + acceptsNull?: false, + validator: Validator } const validators: { @@ -61,7 +65,7 @@ const validators: { }, email: { acceptsNull: true, - validator: (actor, target, params) => { + 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"] @@ -76,6 +80,34 @@ const validators: { } 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) { @@ -144,6 +176,7 @@ const validators: { } }, 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] == @@ -236,13 +269,8 @@ export default function (files: Files) { return Accounts.create(body.username, body.password) .then((account) => { - setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), { - path: "/", - sameSite: "Strict", - secure: true, - httpOnly: true - }) - return ctx.status(200) + login(ctx, account) + return ctx.text("logged in") }) .catch(() => { return ServeError(ctx, 500, "internal server error") @@ -280,7 +308,7 @@ export default function (files: Files) { return [ x, - validator.validator(actor, target, body) + validator.validator(actor, target, body as any, ctx) ] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]] }) diff --git a/src/server/routes/api/v1/session.ts b/src/server/routes/api/v1/session.ts index a0a0c15..590d1f3 100644 --- a/src/server/routes/api/v1/session.ts +++ b/src/server/routes/api/v1/session.ts @@ -11,6 +11,7 @@ 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" @@ -45,13 +46,9 @@ export default function (files: Files) { ServeError(ctx, 400, "username or password incorrect") return } - setCookie(ctx, "auth", auth.create(account.id, 3 * 24 * 60 * 60 * 1000), { - path: "/", - sameSite: "Strict", - secure: true, - httpOnly: true - }) - ctx.status(200) + + login(ctx, account.id) + return ctx.text("logged in") }) router.get("/", requiresAccount, ctx => { diff --git a/src/server/routes/api/web/api.json b/src/server/routes/api/web/api.json index b517aca..a20237d 100644 --- a/src/server/routes/api/web/api.json +++ b/src/server/routes/api/web/api.json @@ -2,6 +2,7 @@ "name": "web", "baseURL": "/", "mount": [ - { "file": "preview", "to": "/download" } + { "file": "preview", "to": "/download" }, + "go" ] } \ No newline at end of file diff --git a/src/server/routes/api/web/go.ts b/src/server/routes/api/web/go.ts new file mode 100644 index 0000000..b05345f --- /dev/null +++ b/src/server/routes/api/web/go.ts @@ -0,0 +1,41 @@ +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 { Hono } from "hono" +import { getAccount, login } from "../../../lib/middleware.js" +export let router = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() + +export default function (files: Files) { + router.get("/verify/:code", getAccount, async (ctx) => { + let currentAccount = ctx.get("account") + let code = CodeMgr.codes.verifyEmail.byId.get(ctx.req.param("code")) + + if (code) { + if (currentAccount != undefined && !code.check(currentAccount.id)) { + return ServeError(ctx, 403, "you are logged in on a different account") + } + + if (!currentAccount) { + login(ctx, code.for) + let ac = Accounts.getFromId(code.for) + if (ac) currentAccount = ac + else return ServeError(ctx, 401, "could not locate account") + } + + currentAccount.email = code.data + await Accounts.save() + + return ctx.redirect('/') + } else return ServeError(ctx, 404, "code not found") + }) + + return router +} \ No newline at end of file diff --git a/src/server/routes/api/web/preview.ts b/src/server/routes/api/web/preview.ts index 909f4cf..8370697 100644 --- a/src/server/routes/api/web/preview.ts +++ b/src/server/routes/api/web/preview.ts @@ -5,6 +5,7 @@ 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 @@ -12,7 +13,7 @@ export let router = new Hono<{ }>() export default function (files: Files) { - router.get("/:fileId", async (ctx) => { + 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")