Add authentication system with session-based auth

Implements full auth flows with opaque tokens (not JWT) for easy revocation:
- Login/logout with cookie or bearer token support
- Registration with email verification
- Password reset with one-time tokens
- scrypt password hashing (no external deps)

New files in express/auth/:
- token.ts: 256-bit token generation, SHA-256 hashing
- password.ts: scrypt hashing with timing-safe verification
- types.ts: Session schemas, token types, input validation
- store.ts: AuthStore interface + InMemoryAuthStore
- service.ts: AuthService with all auth operations
- routes.ts: 6 auth endpoints

Modified:
- types.ts: Added user field to Call, requireAuth/requirePermission helpers
- app.ts: JSON body parsing, populates call.user, handles auth errors
- services.ts: Added services.auth
- routes.ts: Includes auth routes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 13:59:02 -06:00
parent 788ea2ab19
commit c246e0384f
11 changed files with 898 additions and 10 deletions

View File

@@ -10,6 +10,8 @@ import { routes } from "./routes";
import { services } from "./services";
// import { URLPattern } from 'node:url';
import {
AuthenticationRequired,
AuthorizationDenied,
type Call,
type InternalHandler,
type Method,
@@ -22,6 +24,9 @@ import {
const app = express();
// Parse JSON request bodies
app.use(express.json());
services.logging.log({ source: "logging", text: ["1"] });
const processedRoutes: { [K in Method]: ProcessedRoute[] } = {
GET: [],
@@ -52,26 +57,42 @@ routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => {
}
console.log("request.originalUrl", request.originalUrl);
console.log("beavis");
// const p = new URL(request.originalUrl);
// const path = p.pathname;
// console.log("p, path", p, path)
console.log("ok");
// Authenticate the request
const user = await services.auth.validateRequest(request);
const req: Call = {
pattern: route.path,
// path,
path: request.originalUrl,
method,
parameters: { one: 1, two: 2 },
request,
user,
};
const retval = await route.handler(req);
return retval;
try {
const retval = await route.handler(req);
return retval;
} catch (error) {
// Handle authentication errors
if (error instanceof AuthenticationRequired) {
return {
code: httpCodes.clientErrors.Unauthorized,
contentType: contentTypes.application.json,
result: JSON.stringify({
error: "Authentication required",
}),
};
}
if (error instanceof AuthorizationDenied) {
return {
code: httpCodes.clientErrors.Forbidden,
contentType: contentTypes.application.json,
result: JSON.stringify({ error: "Access denied" }),
};
}
throw error;
}
};
for (const [_idx, method] of methodList.entries()) {