import * as z from 'zod'; import { auth } from '~~/lib/auth'; import { hasUsableAdministrator } from '~~/server/lib/account-security'; import { db } from '~~/server/lib/db'; /** * Deletes the signed in user's account and everything that belongs to it, * after they proved it is really them. */ export default defineEventHandler(async (event) => { await protectRoute(event); const userId = event.context.user!.id as string; const result = await readValidatedBody(event, (body) => z .object({ password: z.string().min(1), // required when two-factor is on: a code from the authenticator // app, or one of the backup codes code: z.string().min(1).optional(), backupCode: z.boolean().optional(), }) .safeParse(body), ); if (!result.success) { throw createError({ statusCode: 400, message: result.error.issues[0]!.message, }); } const { password, code, backupCode } = result.data; // read from the database, the session can lag behind by a few minutes const account = await db.query.users.findFirst({ where: { id: userId, }, }); const twoFactorEnabled = !!account?.twoFactorEnabled; // Checked again when the account is deleted. Doing it here as well keeps // a deletion that is refused anyway from using up a backup code. if (account?.role === 'admin' && !await db.transaction(tx => hasUsableAdministrator(tx, userId))) { throw createError({ statusCode: 409, statusMessage: 'The last usable admin cannot be deleted', data: { code: 'LAST_ADMIN', }, }); } if (twoFactorEnabled && code === undefined) { throw createError({ statusCode: 400, statusMessage: 'Two-factor code required', data: { code: 'TWO_FACTOR_REQUIRED', }, }); } try { await auth.api.verifyPassword({ body: { password, }, headers: event.headers, }); if (twoFactorEnabled && backupCode) { await auth.api.verifyBackupCode({ body: { code: code!, }, headers: event.headers, }); } else if (twoFactorEnabled) { await auth.api.verifyTOTP({ body: { code: code!, }, headers: event.headers, }); } } catch (error: any) { const errorCode = error?.body?.code as string | undefined; if (errorCode === undefined) { console.error('Failed to verify credentials:', error); } throw createError({ statusCode: errorCode === undefined ? 500 : 400, statusMessage: 'Failed to delete account', data: { code: errorCode ?? 'INTERNAL_ERROR', }, }); } const deleted = await deleteAccount(userId); if (!deleted.ok) { throw createError({ statusCode: 409, statusMessage: "The last usable admin cannot be deleted", data: { code: deleted.error }, }); } // the session is gone, but the browser would keep trusting its cached // copy for a few minutes unless the cookies go too const { authCookies } = await auth.$context; for (const cookie of Object.values(authCookies)) { deleteCookie(event, cookie.name, { path: cookie.attributes.path, secure: cookie.attributes.secure, httpOnly: cookie.attributes.httpOnly, }); } return { ok: true }; });