Add utils package with Web Crypto password hashing
- Create @reviq/utils package with PBKDF2-SHA256 password hashing compatible with Cloudflare Workers (uses crypto.subtle) - Update api-server and CLI to use new utils package for consistent password hashing format across the codebase - Add pino logging to api-server for better request debugging - Make login request tokens cryptographically secure base58 strings instead of database IDs - Add migration to make login_requests.token non-nullable with unique constraint - Fix RPCLink URL construction for client-side API calls - Add db:codegen script to root package.json Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,23 +11,13 @@ import { verifyPassword } from "../../utils/password.js";
|
||||
import { isDeviceTrusted } from "../../utils/session.js";
|
||||
import { os } from "../base.js";
|
||||
|
||||
/**
|
||||
* Check if a string is a valid login request ID (numeric)
|
||||
*/
|
||||
const isValidLoginRequestId = (value: string): boolean => {
|
||||
const num = Number(value);
|
||||
return !Number.isNaN(num) && Number.isInteger(num) && num > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Login with password handler
|
||||
* - Reads login request token from cookie
|
||||
* - If fake token (UUID): returns generic error for anti-enumeration
|
||||
* - If valid login request ID:
|
||||
* - 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
|
||||
* - 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 }) => {
|
||||
@@ -42,25 +32,14 @@ export const loginPassword = os.auth.loginPassword.handler(
|
||||
// Generic error message for anti-enumeration
|
||||
const INVALID_CREDENTIALS_ERROR = "Invalid email or password";
|
||||
|
||||
// No login request token or invalid format
|
||||
// No login request token
|
||||
if (!loginRequestToken) {
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: INVALID_CREDENTIALS_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if token is a fake token (UUID) or valid login request ID
|
||||
if (!isValidLoginRequestId(loginRequestToken)) {
|
||||
// Fake token - return generic error for anti-enumeration
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: INVALID_CREDENTIALS_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
// Valid login request ID - keep as string (Int8 type)
|
||||
const loginRequestId = loginRequestToken;
|
||||
|
||||
// Fetch login request with user data in single query (optimized JOIN)
|
||||
// Fetch login request with user data by token
|
||||
const result = await context.db
|
||||
.selectFrom("login_requests")
|
||||
.innerJoin("users", "users.id", "login_requests.user_id")
|
||||
@@ -73,7 +52,7 @@ export const loginPassword = os.auth.loginPassword.handler(
|
||||
"login_requests.completed_at",
|
||||
"users.password_hash",
|
||||
])
|
||||
.where("login_requests.id", "=", loginRequestId)
|
||||
.where("login_requests.token", "=", loginRequestToken)
|
||||
.executeTakeFirst();
|
||||
|
||||
// Login request not found
|
||||
@@ -124,7 +103,7 @@ export const loginPassword = os.auth.loginPassword.handler(
|
||||
.set({
|
||||
completed_at: new Date(),
|
||||
})
|
||||
.where("id", "=", loginRequestId)
|
||||
.where("id", "=", result.id)
|
||||
.execute();
|
||||
} else {
|
||||
// Device is untrusted - generate confirmation token and send email
|
||||
@@ -135,10 +114,10 @@ export const loginPassword = os.auth.loginPassword.handler(
|
||||
.set({
|
||||
token: confirmationToken,
|
||||
})
|
||||
.where("id", "=", loginRequestId)
|
||||
.where("id", "=", result.id)
|
||||
.execute();
|
||||
|
||||
// Send login confirmation email (stubbed for now)
|
||||
// Send login confirmation email
|
||||
await sendLoginConfirmationEmail(result.email, confirmationToken);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user