Files
publisher-dashboard/apps/api-server/src/procedures/auth/create-login-request.ts
RevIQ ddd7c0c03b Add generateSecureBase58Token to shared utils with login_ prefix
- Create packages/utils/src/generate-base58-token.ts with typed prefix support
- Function returns `${TPrefix}${string}` for type-safe prefixed tokens
- Add isBase58() validator and parseBase58Token() helper
- Add comprehensive tests (13 test cases)

- Update login request tokens to use "login_" prefix
- Fix login-password.ts to not replace token (cookie/DB mismatch bug)
- Migrate all token generation from generateSecureToken (hex) to
  generateSecureBase58Token (base58)
- Remove duplicate token generation from api-server/utils/crypto.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 19:31:30 +08:00

144 lines
3.8 KiB
TypeScript

/**
* Create login request procedure
* First step in the login flow - validates email and returns available auth methods
*/
import {
COOKIE_DURATIONS,
COOKIE_NAMES,
COOKIE_OPTIONS,
getCookie,
setCookie,
} from "../../utils/cookies.js";
import {
generateDeviceFingerprint,
generateExpiry,
generateSecureBase58Token,
} from "../../utils/crypto.js";
import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
import { isDeviceTrusted } from "../../utils/session.js";
import { os } from "../base.js";
/**
* Create login request handler
* - Normalizes email to lowercase
* - Reads/generates device fingerprint cookie
* - Looks up user by email
* - If user exists: checks device trust, passkey, password; creates login request
* - If user doesn't exist: generates fake token for anti-enumeration
* - Returns auth method availability and device trust status
*/
export const createLoginRequest = os.auth.createLoginRequest.handler(
async ({ input, context }) => {
const { email: rawEmail } = input;
// Normalize email to lowercase
const email = rawEmail.toLowerCase();
// Read or generate device fingerprint
let deviceFingerprint = getCookie(
context.reqHeaders,
COOKIE_NAMES.DEVICE_FINGERPRINT,
);
if (!deviceFingerprint) {
deviceFingerprint = generateDeviceFingerprint();
setCookie(
context.resHeaders,
COOKIE_NAMES.DEVICE_FINGERPRINT,
deviceFingerprint,
COOKIE_OPTIONS.device,
);
}
// Look up user by email
const user = await context.db
.selectFrom("users")
.select(["id", "password_hash"])
.where("email", "=", email)
.executeTakeFirst();
// User doesn't exist - return fake response for anti-enumeration
if (!user) {
// Generate placeholder token (base58) for anti-enumeration
// This prevents attackers from knowing if an email exists based on response
const placeholderToken = generateSecureBase58Token("login_");
// Set placeholder login request token cookie
setCookie(
context.resHeaders,
COOKIE_NAMES.LOGIN_REQUEST_TOKEN,
placeholderToken,
COOKIE_OPTIONS.loginRequest,
);
return {
hasPasskey: false,
hasPassword: false,
isTrustedDevice: false,
email,
};
}
// User exists - gather real auth information
const userId = user.id;
// Check if device is trusted
const isTrustedDevice = await isDeviceTrusted(
context.db,
userId,
deviceFingerprint,
);
// Check if user has passkey
const passkey = await context.db
.selectFrom("passkeys")
.select(["id"])
.where("user_id", "=", userId)
.executeTakeFirst();
const hasPasskey = !!passkey;
// Check if user has password
const hasPassword = user.password_hash !== null;
// Get geo info and user agent
const geo = getGeoInfo(context.reqHeaders);
const userAgent = getUserAgent(context.reqHeaders);
// Create login request with secure token
const expiresAt = generateExpiry(COOKIE_DURATIONS.LOGIN_REQUEST);
const token = generateSecureBase58Token("login_");
await context.db
.insertInto("login_requests")
.values({
user_id: userId,
email,
token,
device_fingerprint: deviceFingerprint,
ip_address: geo.ip,
city: geo.city,
region: geo.region,
country: geo.country,
user_agent: userAgent,
expires_at: expiresAt,
})
.execute();
// Set login request token cookie with the secure token
setCookie(
context.resHeaders,
COOKIE_NAMES.LOGIN_REQUEST_TOKEN,
token,
COOKIE_OPTIONS.loginRequest,
);
return {
hasPasskey,
hasPassword,
isTrustedDevice,
email,
};
},
);