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:
160
express/auth/store.ts
Normal file
160
express/auth/store.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user