/** * Login with password procedure * Second step in the login flow - verifies password and completes/confirms login */ import { ORPCError } from "@orpc/server"; import { sendLoginConfirmationEmail } from "@reviq/emails"; import { COOKIE_NAMES, getCookie } from "../../utils/cookies.js"; import { verifyPassword } from "../../utils/password.js"; import { isDeviceTrusted } from "../../utils/session.js"; import { os } from "../base.js"; /** * Login with password handler * - Reads login request token from cookie * - Validates login request exists and not expired * - Verifies password against stored hash * - If device is trusted: marks login request as completed * - If device is untrusted: generates confirmation token and sends email */ export const loginPassword = os.auth.loginPassword.handler( async ({ input, context }) => { const { password } = input; // Read login request token from cookie const loginRequestToken = getCookie( context.reqHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); // Generic error message for anti-enumeration const INVALID_CREDENTIALS_ERROR = "Invalid email or password"; // No login request token if (!loginRequestToken) { throw new ORPCError("BAD_REQUEST", { message: INVALID_CREDENTIALS_ERROR, }); } // Fetch login request with user data by token const result = await context.db .selectFrom("login_requests") .innerJoin("users", "users.id", "login_requests.user_id") .select([ "login_requests.id", "login_requests.user_id", "login_requests.email", "login_requests.token", "login_requests.device_fingerprint", "login_requests.expires_at", "login_requests.completed_at", "users.password_hash", ]) .where("login_requests.token", "=", loginRequestToken) .executeTakeFirst(); // Login request not found if (!result) { throw new ORPCError("BAD_REQUEST", { message: INVALID_CREDENTIALS_ERROR, }); } // Check if login request is expired if (new Date() > new Date(result.expires_at)) { throw new ORPCError("BAD_REQUEST", { message: "Login request has expired. Please start the login process again.", }); } // User has no password set if (!result.password_hash) { throw new ORPCError("BAD_REQUEST", { message: INVALID_CREDENTIALS_ERROR, }); } // Verify password const passwordValid = await verifyPassword(password, result.password_hash); if (!passwordValid) { throw new ORPCError("BAD_REQUEST", { message: INVALID_CREDENTIALS_ERROR, }); } // Password is valid - check if device is trusted // If no device fingerprint, treat as untrusted const deviceTrusted = result.device_fingerprint ? await isDeviceTrusted( context.db, result.user_id, result.device_fingerprint, ) : false; if (deviceTrusted) { // Device is trusted - complete login immediately await context.db .updateTable("login_requests") .set({ completed_at: new Date(), }) .where("id", "=", result.id) .execute(); } else { // Device is untrusted - send confirmation email with existing token // The same base58 token is used for both cookie lookup and email confirmation await sendLoginConfirmationEmail({ client: context.email.client, fromAddress: context.email.fromAddress, baseUrl: context.email.baseUrl, email: result.email, token: result.token, expiryMinutes: 15, }); } return { success: true }; }, );