- Add successResponseSchema to common.ts for explicit success responses
- Update all auth, me, orgs, and admin procedures to return { success: true }
- Update contract.ts to use successResponseSchema instead of z.void()
- Add ast-grep rule to prevent future z.void() usage in contracts
- Add build:packages script to root package.json
- Fix test file lint errors with eslint-disable comments
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
/**
|
|
* Login with password procedure
|
|
* Second step in the login flow - verifies password and completes/confirms login
|
|
*/
|
|
|
|
import { ORPCError } from "@orpc/server";
|
|
import { COOKIE_NAMES, getCookie } from "../../utils/cookies.js";
|
|
import { sendLoginConfirmationEmail } from "../../utils/email.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(result.email, result.token);
|
|
}
|
|
|
|
return { success: true };
|
|
},
|
|
);
|