- 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>
74 lines
2.1 KiB
TypeScript
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 };
|
|
},
|
|
);
|