import { and, eq, sql } from "drizzle-orm"; import * as z from "zod"; import { providers, recoveryLinks } from "~~/drizzle/schema"; import { lockAccount, revokeTwoFactorTrust } from "~~/server/lib/account-security"; import { auth } from "~~/lib/auth"; import { authTransaction } from "~~/server/lib/auth-transaction"; export default defineEventHandler(async (event) => { const { user } = await protectRoute(event); const result = await readValidatedBody(event, (body) => z.object({ currentPassword: z.string().min(1).max(128), newPassword: z.string().min(8).max(128), providerKeys: z.array(z.object({ id: z.string(), originalApiKey: z.string().min(1), apiKey: z.string().min(1), })), }).safeParse(body)); if (!result.success) { throw createError({ statusCode: 400, message: result.error.issues[0]?.message }); } const { currentPassword, newPassword, providerKeys } = result.data; const replacements = new Map(providerKeys.map(key => [key.id, key])); let cookies: string[]; try { cookies = await authTransaction(async (tx) => { await lockAccount(tx, user.id); const existing = await tx.select().from(providers).where(eq(providers.userId, user.id)); const withKey = existing.filter(provider => !!provider.config.apiKey); const complete = replacements.size === providerKeys.length && replacements.size === withKey.length && withKey.every(provider => replacements.get(provider.id)?.originalApiKey === provider.config.apiKey); if (!complete) { throw createError({ statusCode: 409, statusMessage: "Provider keys out of date", data: { code: "PROVIDER_KEYS_OUT_OF_DATE" }, }); } const { headers } = await auth.api.changePassword({ body: { currentPassword, newPassword, revokeOtherSessions: true }, headers: event.headers, returnHeaders: true, }); await tx.delete(recoveryLinks).where(eq(recoveryLinks.userId, user.id)); await revokeTwoFactorTrust(tx, user.id); // Invalidate pending config writes even for providers without a key. const updatedAt = new Date(Math.max(Date.now(), ...existing.map(provider => provider.updatedAt.getTime() + 1))); await tx.update(providers).set({ updatedAt }).where(eq(providers.userId, user.id)); for (const key of providerKeys) { await tx.update(providers).set({ config: sql`jsonb_set(${providers.config}, '{apiKey}', ${JSON.stringify(key.apiKey)}::jsonb)`, updatedAt, }).where(and(eq(providers.id, key.id), eq(providers.userId, user.id))); } return headers.getSetCookie(); }); } catch (error) { if (error instanceof Error && "body" in error) { const body = z.object({ code: z.string(), message: z.string().optional() }).safeParse(error.body); if (body.success) { throw createError({ statusCode: 400, message: body.data.message, data: { code: body.data.code } }); } } throw error; } // No replacement session reaches the browser until its transaction commits. for (const cookie of cookies) { appendResponseHeader(event, "set-cookie", cookie); } return { ok: true }; });