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