Files
diachron/express/auth/store.ts
Michael Wolf c246e0384f 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>
2026-01-03 13:59:02 -06:00

161 lines
5.1 KiB
TypeScript

// store.ts
//
// Authentication storage interface and in-memory implementation.
// The interface allows easy migration to PostgreSQL later.
import { User, type UserId } from "../user";
import { generateToken, hashToken } from "./token";
import type { AuthMethod, SessionData, TokenId, TokenType } from "./types";
// Data for creating a new session (tokenId generated internally)
export type CreateSessionData = {
userId: string;
tokenType: TokenType;
authMethod: AuthMethod;
expiresAt: Date;
userAgent?: string;
ipAddress?: string;
};
// Data for creating a new user
export type CreateUserData = {
email: string;
passwordHash: string;
displayName?: string;
};
// Abstract interface for auth storage - implement for PostgreSQL later
export interface AuthStore {
// Session operations
createSession(
data: CreateSessionData,
): Promise<{ token: string; session: SessionData }>;
getSession(tokenId: TokenId): Promise<SessionData | null>;
updateLastUsed(tokenId: TokenId): Promise<void>;
deleteSession(tokenId: TokenId): Promise<void>;
deleteUserSessions(userId: UserId): Promise<number>;
// User operations
getUserByEmail(email: string): Promise<User | null>;
getUserById(userId: UserId): Promise<User | null>;
createUser(data: CreateUserData): Promise<User>;
getUserPasswordHash(userId: UserId): Promise<string | null>;
setUserPassword(userId: UserId, passwordHash: string): Promise<void>;
updateUserEmailVerified(userId: UserId): Promise<void>;
}
// In-memory implementation for development
export class InMemoryAuthStore implements AuthStore {
private sessions: Map<string, SessionData> = new Map();
private users: Map<string, User> = new Map();
private usersByEmail: Map<string, string> = new Map();
private passwordHashes: Map<string, string> = new Map();
private emailVerified: Map<string, boolean> = new Map();
async createSession(
data: CreateSessionData,
): Promise<{ token: string; session: SessionData }> {
const token = generateToken();
const tokenId = hashToken(token);
const session: SessionData = {
tokenId,
userId: data.userId,
tokenType: data.tokenType,
authMethod: data.authMethod,
createdAt: new Date(),
expiresAt: data.expiresAt,
userAgent: data.userAgent,
ipAddress: data.ipAddress,
};
this.sessions.set(tokenId, session);
return { token, session };
}
async getSession(tokenId: TokenId): Promise<SessionData | null> {
const session = this.sessions.get(tokenId);
if (!session) return null;
// Check expiration
if (new Date() > session.expiresAt) {
this.sessions.delete(tokenId);
return null;
}
return session;
}
async updateLastUsed(tokenId: TokenId): Promise<void> {
const session = this.sessions.get(tokenId);
if (session) {
session.lastUsedAt = new Date();
}
}
async deleteSession(tokenId: TokenId): Promise<void> {
this.sessions.delete(tokenId);
}
async deleteUserSessions(userId: UserId): Promise<number> {
let count = 0;
for (const [tokenId, session] of this.sessions) {
if (session.userId === userId) {
this.sessions.delete(tokenId);
count++;
}
}
return count;
}
async getUserByEmail(email: string): Promise<User | null> {
const userId = this.usersByEmail.get(email.toLowerCase());
if (!userId) return null;
return this.users.get(userId) ?? null;
}
async getUserById(userId: UserId): Promise<User | null> {
return this.users.get(userId) ?? null;
}
async createUser(data: CreateUserData): Promise<User> {
const user = User.create(data.email, {
displayName: data.displayName,
status: "pending", // Pending until email verified
});
this.users.set(user.id, user);
this.usersByEmail.set(data.email.toLowerCase(), user.id);
this.passwordHashes.set(user.id, data.passwordHash);
this.emailVerified.set(user.id, false);
return user;
}
async getUserPasswordHash(userId: UserId): Promise<string | null> {
return this.passwordHashes.get(userId) ?? null;
}
async setUserPassword(userId: UserId, passwordHash: string): Promise<void> {
this.passwordHashes.set(userId, passwordHash);
}
async updateUserEmailVerified(userId: UserId): Promise<void> {
this.emailVerified.set(userId, true);
// Update user status to active
const user = this.users.get(userId);
if (user) {
// Create new user with active status
const updatedUser = User.create(user.email, {
id: user.id,
displayName: user.displayName,
status: "active",
roles: [...user.roles],
permissions: [...user.permissions],
});
this.users.set(userId, updatedUser);
}
}
}