/** * Forgot password handler * Public procedure (no authentication required) * * Anti-enumeration: Always returns success even if user doesn't exist * This prevents attackers from determining which emails are registered */ import { TOKEN_DURATIONS } from "../../utils/cookies.js"; import { generateExpiry, generateSecureBase58Token, } from "../../utils/crypto.js"; import { sendPasswordResetEmail } from "../../utils/email.js"; import { os } from "../base.js"; export const forgotPassword = os.auth.forgotPassword.handler( async ({ input, context }) => { const { email } = input; // Normalize email to lowercase const normalizedEmail = email.toLowerCase(); // Look up user by email const user = await context.db .selectFrom("users") .select(["id", "email"]) .where("email", "=", normalizedEmail) .executeTakeFirst(); // If user exists, create password reset token and send email if (user) { // Delete any existing password reset tokens for this user (security measure) await context.db .deleteFrom("password_resets") .where("user_id", "=", user.id) .execute(); // Generate secure base58 token const token = generateSecureBase58Token(); // Create password reset record with 1 hour expiry const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET); await context.db .insertInto("password_resets") .values({ user_id: user.id, token, expires_at: expiresAt, }) .execute(); // Send password reset email (stubbed) await sendPasswordResetEmail(user.email, token); } // Always return success (anti-enumeration) // Don't reveal whether the email exists or not }, );