Update paths in sync.sh, master/main.go, and CLAUDE.md to reflect the directory rename. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
165 lines
5.2 KiB
TypeScript
165 lines
5.2 KiB
TypeScript
// store.ts
|
|
//
|
|
// Authentication storage interface and in-memory implementation.
|
|
// The interface allows easy migration to PostgreSQL later.
|
|
|
|
import { AuthenticatedUser, type 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 = AuthenticatedUser.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 = AuthenticatedUser.create(user.email, {
|
|
id: user.id,
|
|
displayName: user.displayName,
|
|
status: "active",
|
|
roles: [...user.roles],
|
|
permissions: [...user.permissions],
|
|
});
|
|
this.users.set(userId, updatedUser);
|
|
}
|
|
}
|
|
}
|