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:
64
express/auth/types.ts
Normal file
64
express/auth/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// types.ts
|
||||
//
|
||||
// Authentication types and Zod schemas.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
// Branded type for token IDs (the hash, not the raw token)
|
||||
export type TokenId = string & { readonly __brand: "TokenId" };
|
||||
|
||||
// Token types for different purposes
|
||||
export const tokenTypeParser = z.enum([
|
||||
"session",
|
||||
"password_reset",
|
||||
"email_verify",
|
||||
]);
|
||||
export type TokenType = z.infer<typeof tokenTypeParser>;
|
||||
|
||||
// Authentication method - how the token was delivered
|
||||
export const authMethodParser = z.enum(["cookie", "bearer"]);
|
||||
export type AuthMethod = z.infer<typeof authMethodParser>;
|
||||
|
||||
// Session data schema - what gets stored
|
||||
export const sessionDataParser = z.object({
|
||||
tokenId: z.string().min(1),
|
||||
userId: z.string().min(1),
|
||||
tokenType: tokenTypeParser,
|
||||
authMethod: authMethodParser,
|
||||
createdAt: z.coerce.date(),
|
||||
expiresAt: z.coerce.date(),
|
||||
lastUsedAt: z.coerce.date().optional(),
|
||||
userAgent: z.string().optional(),
|
||||
ipAddress: z.string().optional(),
|
||||
isUsed: z.boolean().optional(), // For one-time tokens
|
||||
});
|
||||
|
||||
export type SessionData = z.infer<typeof sessionDataParser>;
|
||||
|
||||
// Input validation schemas for auth endpoints
|
||||
export const loginInputParser = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const registerInputParser = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
displayName: z.string().optional(),
|
||||
});
|
||||
|
||||
export const forgotPasswordInputParser = z.object({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export const resetPasswordInputParser = z.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
// Token lifetimes in milliseconds
|
||||
export const tokenLifetimes: Record<TokenType, number> = {
|
||||
session: 30 * 24 * 60 * 60 * 1000, // 30 days
|
||||
password_reset: 1 * 60 * 60 * 1000, // 1 hour
|
||||
email_verify: 24 * 60 * 60 * 1000, // 24 hours
|
||||
};
|
||||
Reference in New Issue
Block a user