4bba529ba8
Support TOTP 2fa, allow for changing password, add session management and account removal.
140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import { eq } from 'drizzle-orm';
|
|
import * as z from 'zod';
|
|
import {
|
|
agents,
|
|
files,
|
|
models,
|
|
providers,
|
|
settings,
|
|
toolCalls,
|
|
topics,
|
|
users,
|
|
} from '~~/drizzle/schema';
|
|
import { auth } from '~~/lib/auth';
|
|
import { db } from '~~/server/lib/db';
|
|
|
|
/**
|
|
* Deletes the signed in user's account and everything that belongs to it.
|
|
*
|
|
* The app tables only store a plain user id, nothing cascades from the users
|
|
* table, so every one of them is cleared here. It all happens in one
|
|
* transaction: either the whole account is gone or nothing changed.
|
|
*
|
|
* Objects in file storage are left alone on purpose.
|
|
*/
|
|
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;
|
|
|
|
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',
|
|
},
|
|
});
|
|
}
|
|
|
|
await db.transaction(async (tx) => {
|
|
// messages, their parts, generations and attachments cascade from topics
|
|
await tx.delete(topics).where(eq(topics.userId, userId));
|
|
await tx.delete(agents).where(eq(agents.userId, userId));
|
|
await tx.delete(models).where(eq(models.userId, userId));
|
|
await tx.delete(providers).where(eq(providers.userId, userId));
|
|
// nothing points a tool call back at its message, so these never cascade
|
|
await tx.delete(toolCalls).where(eq(toolCalls.userId, userId));
|
|
// embeddings go with their file
|
|
await tx.delete(files).where(eq(files.userId, userId));
|
|
await tx.delete(settings).where(eq(settings.userId, userId));
|
|
|
|
// sessions, accounts and two-factor rows cascade from the user
|
|
await tx.delete(users).where(eq(users.id, userId));
|
|
});
|
|
|
|
// 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,
|
|
sameSite: cookie.attributes.sameSite,
|
|
httpOnly: cookie.attributes.httpOnly,
|
|
});
|
|
}
|
|
|
|
return { ok: true };
|
|
});
|