Files
publisher-dashboard/apps/api-server/src/procedures/auth/create-login-request.ts
RevIQ 319edf70db Fix IP address not being set on sessions from localhost
The extractClientIP() function only checked proxy headers (X-Forwarded-For,
CF-Connecting-IP, etc.) which don't exist when running locally without a proxy.

Changes:
- Add clientIP field to APIContext
- Use Bun's server.requestIP() to get client IP from direct socket connection
- Update getGeoInfo() to accept fallback IP parameter
- Pass context.clientIP to getGeoInfo() in auth procedures

Now sessions will have IP address set even for local development (::1 or 127.0.0.1).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 18:08:21 +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, context.clientIP);
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,
};
},
);