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,32 @@
/**
* Logout procedure - revokes the current session and clears the session cookie
*/
import type { AuthenticatedContext } from "../../context.js";
import { implement } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { COOKIE_NAMES, deleteCookie } from "../../utils/cookies.js";
const os = implement(contract);
/**
* Logout handler
* - Requires authentication (user must be logged in)
* - Revokes the current session by setting revoked_at to now()
* - Clears the session cookie from the response
*/
export const logout = os.auth.logout.handler(
async ({ context }: { context: unknown }) => {
const ctx = context as AuthenticatedContext;
// Revoke the current session
await ctx.db
.updateTable("sessions")
.set({ revoked_at: new Date() })
.where("id", "=", String(ctx.session.id))
.execute();
// Clear the session cookie
deleteCookie(ctx.resHeaders, COOKIE_NAMES.SESSION_TOKEN);
},
);