API: - POST /auth/signup — creates a house + user_profile (transactional), hashes the password with argon2, sets a JWT in an httpOnly cookie - POST /auth/login — verifies credentials (generic 401 for both wrong email and wrong password, doesn't leak which), sets the cookie - POST /auth/logout — clears the cookie - GET /auth/me — current profile, behind requireAuth middleware - requireAuth verifies the JWT and re-checks tokenVersion against the DB, so a stateless JWT can still be invalidated (password change / logout-everywhere, not built yet but the field is in place) Schema: user_profiles gets password_hash + token_version (not in the original spec doc — required for auth). New migration, with COMMENT ON for the new columns per the established pattern. Decisions from the auth planning discussion: JWT in httpOnly cookie (not server-side sessions), first profile created also creates its house, argon2 for hashing. argon2 pinned to 0.31.2 (not ^, deliberately): 0.45.1 segfaults at runtime on this Windows machine — reproduced consistently across bash (sandboxed and unsandboxed) and PowerShell, while 0.31.2 works fine with the same API. Documented in the README as a trap for future upgrades, since `tsc`/`prisma generate` succeeding doesn't catch a runtime native-binding crash. Tests: Mocha (unit-style, apps/api/test/auth.test.ts) and a Cucumber feature (apps/api/features/auth.feature) covering the full signup → authenticated flow, duplicate email, wrong password. Both share test-support/reset-db.ts (TRUNCATE ... CASCADE) to start each test/scenario from a clean slate. Test-only argon2 cost parameters (NODE_ENV=test) keep the suite fast — argon2's real cost is deliberately expensive, which made hashing dozens of times per run slow and occasionally timeout-flaky at default cost. CI: added a Postgres service container to lint-and-test (previously none — tests didn't touch a real DB), runs `prisma migrate deploy` before the test steps. Verified end-to-end manually against the dev server (curl): signup, duplicate email (409), wrong password (401), valid login (200), validation errors (400), /me with and without cookie, logout (204) — all behave as intended. Full suite (lint, mocha, cucumber, build) run multiple times locally with no flakiness after the timeout/cost fixes.
30 lines
1 KiB
TypeScript
30 lines
1 KiB
TypeScript
import jwt from "jsonwebtoken";
|
|
import { env } from "../config/env.js";
|
|
|
|
export interface AuthTokenPayload {
|
|
userProfileId: number;
|
|
tokenVersion: number;
|
|
}
|
|
|
|
export function signAuthToken(payload: AuthTokenPayload): string {
|
|
// "sub" follows the JWT convention (RFC 7519) of identifying the
|
|
// principal as a string; userProfileId/tokenVersion are our own claims.
|
|
return jwt.sign(
|
|
{ sub: String(payload.userProfileId), tokenVersion: payload.tokenVersion },
|
|
env.JWT_SECRET,
|
|
{ expiresIn: env.JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] },
|
|
);
|
|
}
|
|
|
|
export function verifyAuthToken(token: string): AuthTokenPayload {
|
|
const decoded = jwt.verify(token, env.JWT_SECRET);
|
|
const userProfileId = typeof decoded === "object" ? Number(decoded.sub) : Number.NaN;
|
|
if (
|
|
typeof decoded !== "object" ||
|
|
Number.isNaN(userProfileId) ||
|
|
typeof decoded.tokenVersion !== "number"
|
|
) {
|
|
throw new Error("Malformed auth token payload");
|
|
}
|
|
return { userProfileId, tokenVersion: decoded.tokenVersion };
|
|
}
|