"use server"; import "server-only"; import { randomBytes } from "crypto"; import { query } from "@/lib/db"; import { serverWarn } from "@/lib/server-log"; import { requestPasswordReset as neonAuthRequestPasswordReset, setUserPassword as neonAuthSetUserPassword, } from "@/lib/auth"; import { getAdminUser } from "@/lib/admin-permissions"; import { getSession } from "@/lib/auth"; export type ResetAdminPasswordResult = | { success: true; tempPassword: string; method: "set" } | { success: true; method: "reset_link_sent"; message: string } | { success: false; error: string }; /** * Emergency recovery action — resets the password for the specified * email so the platform admin can hand a working credential to the * user. * * Tries, in order: * 1. `auth.admin.setUserPassword({ userId, newPassword })` — instant, * works when the caller has `role='admin'` in `neon_auth.user`. * 2. Falls back to `requestPasswordReset({ email, redirectTo })` — * public Neon Auth endpoint, always works, but the user has to * click the link in the email. The fallback covers the common * case where `provision-admin.ts` did not promote the caller to * `role='admin'` in Neon Auth. * * Authorization: platform_admin only. The dev-session platform_admin * shim is honored (same as the rest of the admin actions). */ export async function resetAdminPassword( email: string, ): Promise { await getSession(); // 1. Authz check. const me = await getAdminUser(); if (!me) { return { success: false, error: "Not authenticated." }; } if (me.role !== "platform_admin") { return { success: false, error: "Only platform admins can reset passwords.", }; } const normalizedEmail = email.trim().toLowerCase(); if (!normalizedEmail) { return { success: false, error: "Email is required." }; } // 2. Look up the Neon Auth user id by email. const { rows } = await query<{ id: string }>( `SELECT id FROM neon_auth.user WHERE email = $1 LIMIT 1`, [normalizedEmail], ); const user = rows[0]; if (!user) { return { success: false, error: `No Neon Auth user found for ${normalizedEmail}.`, }; } // 3. Generate a strong temp password server-side. 16 bytes → 32 // base64url chars; mix in a symbol so it satisfies typical // password policies. const tempPassword = `${randomBytes(18).toString("base64url")}!`; // 4. Try the privileged admin endpoint. try { const adminResult = await neonAuthSetUserPassword({ userId: user.id, newPassword: tempPassword, }); if (adminResult?.error) { const code = adminResult.error.code ?? ""; const canFallback = code === "FORBIDDEN" || code === "UNAUTHORIZED" || code === "INTERNAL_SERVER_ERROR"; serverWarn( "[resetAdminPassword] setUserPassword failed (code=%s), will%s fall back: %s", code, canFallback ? "" : " NOT", adminResult.error.message ?? "", ); if (!canFallback) { return { success: false, error: adminResult.error.message ?? `setUserPassword failed: ${code}`, }; } // fall through to the public reset-link path below } else { // Success — flip must_change_password so the user is forced // to pick a new password on next sign-in. try { await query( `UPDATE admin_users SET must_change_password = true WHERE user_id = $1`, [user.id], ); } catch (flagErr) { serverWarn( "[resetAdminPassword] failed to set must_change_password (non-fatal):", flagErr, ); } return { success: true, tempPassword, method: "set" }; } } catch (err) { console.error("[resetAdminPassword] setUserPassword threw:", err); // network / unexpected — try the fallback below } // 5. Fallback: send a password-reset email via the public endpoint. try { const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:4000"; const resetResult = await neonAuthRequestPasswordReset({ email: normalizedEmail, redirectTo: `${siteUrl}/reset-password`, }); if (resetResult?.error) { return { success: false, error: resetResult.error.message ?? resetResult.error.code ?? "Failed to send reset email", }; } return { success: true, method: "reset_link_sent", message: `Set-password is not available for this account. Sent a password-reset email to ${normalizedEmail} instead — share that link with the user.`, }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : String(err), }; } }