Implement auth procedures with code review fixes

Add complete auth backend (Workstream D):
- Auth middleware for session/API key authentication
- Signup with password or passkey (WebAuthn)
- Login flow with device trust and email confirmation
- Password reset and email verification
- Session management and logout

Utilities created:
- cookies.ts: Cookie helpers and configuration
- crypto.ts: Token generation and hashing
- password.ts: zxcvbn validation, argon2id hashing
- geo.ts: IP/location extraction from headers
- email.ts: Stubbed email sending
- session.ts: Session creation and device trust

Code review improvements applied:
- Use ORPCError instead of Error in procedures
- Add ast-grep rule to enforce ORPCError usage
- Remove error info leakage (generic messages)
- Optimize N+1 query with JOIN in login-password
- Extract signupWithPassword/signupWithPasskey for testability
- Add 15-minute WebAuthn challenge expiry check
- Strengthen CookieOptions type definitions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 15:19:15 +08:00
parent 8de88472b1
commit 829d365e80
24 changed files with 1739 additions and 47 deletions

View File

@@ -0,0 +1,42 @@
export interface GeoInfo {
ip: string | null;
city: string | null;
region: string | null;
country: string | null;
}
/**
* Extract geolocation info from request headers
* Supports Cloudflare headers in production, falls back to standard headers
* @param headers - Request headers
* @returns Geolocation information extracted from headers
*/
export const getGeoInfo = (headers: Headers): GeoInfo => {
// Try Cloudflare headers first (production)
const cfIP = headers.get("CF-Connecting-IP");
const cfCountry = headers.get("CF-IPCountry");
const cfCity = headers.get("CF-IPCity");
const cfRegion = headers.get("CF-Region");
// Fallback to X-Forwarded-For or X-Real-IP
const forwardedFor = headers.get("X-Forwarded-For");
const realIP = headers.get("X-Real-IP");
const ip = cfIP ?? realIP ?? forwardedFor?.split(",")[0]?.trim() ?? null;
return {
ip,
city: cfCity ?? null,
region: cfRegion ?? null,
country: cfCountry ?? null,
};
};
/**
* Extract User-Agent from request headers
* @param headers - Request headers
* @returns User-Agent string or "Unknown" if not present
*/
export const getUserAgent = (headers: Headers): string => {
return headers.get("User-Agent") ?? "Unknown";
};