Files
publisher-dashboard/apps/api-server/src/procedures/auth/forgot-password.ts
igm e43c006bb1 Fix merge conflicts and add withTransaction helper
- Add withTransaction helper that gracefully handles nested transactions
  (reuses existing transaction in tests, starts new one otherwise)
- Update auth procedures to use withTransaction instead of direct .transaction()
- Add email config to all e2e test contexts (required by merged code)
- Remove duplicate verification token code from signup procedure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 17:07:14 +08:00

74 lines
2.1 KiB
TypeScript

/**
* 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 { withTransaction } from "@reviq/db";
import { sendPasswordResetEmail } from "@reviq/emails";
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import {
generateExpiry,
generateSecureBase58Token,
} from "../../utils/crypto.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) {
// Generate secure base58 token
const token = generateSecureBase58Token();
// Create password reset record with 1 hour expiry
const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET);
// Delete old tokens and insert new one in transaction
await withTransaction(context.db, async (trx) => {
// Delete any existing password reset tokens for this user (security measure)
await trx
.deleteFrom("password_resets")
.where("user_id", "=", user.id)
.execute();
await trx
.insertInto("password_resets")
.values({
user_id: user.id,
token,
expires_at: expiresAt,
})
.execute();
});
// Send password reset email
await sendPasswordResetEmail({
client: context.email.client,
fromAddress: context.email.fromAddress,
baseUrl: context.email.baseUrl,
email: user.email,
token,
expiryHours: 1,
});
}
// Always return success (anti-enumeration)
// Don't reveal whether the email exists or not
return { success: true };
},
);