1import db from '../db/db.server';
2import { sessions, users } from '../db/schema';
3import { eq } from 'drizzle-orm';
4import type { Session, User } from '../types/chat';
5import { createLogger } from '../utils/logger.server';
6
7const log = createLogger('session-server');
8
9// Helper: Compute SHA-256 hash of a message and encode it as a hex string.
10export async function sha256(message: string): Promise<string> {
11 const encoder = new TextEncoder();
12 const data = encoder.encode(message);
13 const hashBuffer = await crypto.subtle.digest('SHA-256', data);
14 const hashArray = Array.from(new Uint8Array(hashBuffer));
15 const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
16 return hashHex;
17}
18
19// Helper: Custom Base32 encoder using the standard alphabet.
20// We use this instead of @oslojs/encoding given Cloudflare Pages compatibility.
21const base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
22function base32Encode(data: Uint8Array): string {
23 let bits = 0;
24 let value = 0;
25 let output = "";
26 for (let i = 0; i < data.length; i++) {
27 value = (value << 8) | data[i];
28 bits += 8;
29 while (bits >= 5) {
30 const index = (value >>> (bits - 5)) & 0x1f;
31 output += base32Alphabet[index];
32 bits -= 5;
33 }
34 }
35 if (bits > 0) {
36 const index = (value << (5 - bits)) & 0x1f;
37 output += base32Alphabet[index];
38 }
39 return output;
40}
41
42// Generate a secure session token by generating 20 random bytes and Base32 encoding them.
43export function generateSessionToken(): string {
44 const randomBytesArray = new Uint8Array(20);
45 crypto.getRandomValues(randomBytesArray);
46 const token = base32Encode(randomBytesArray);
47 log.debug("Generated session token", { token });
48 return token;
49}
50
51// Create a session record once a valid token is available.
52// The session ID is the SHA-256 hash of the token.
53export async function createSession(token: string, userId: string): Promise<Session> {
54 log.debug("Creating session", { userId });
55 const sessionId = await sha256(token);
56 const createdAt = Date.now();
57 const expiresAt = createdAt + 24 * 60 * 60 * 1000; // 24 hours expiry
58 await db.insert(sessions).values({
59 id: sessionId,
60 userId,
61 createdAt,
62 expiresAt,
63 });
64 log.debug("Session created", { sessionId });
65 return { id: sessionId, userId, createdAt, expiresAt };
66}
67
68/**
69 * Type distributed through our API.
70 * Returns an object with both session and user if valid, or nulls if not.
71 * Note: This returns the full User type since it's used server-side in hooks.
72 */
73export type SessionValidationResult =
74 | { session: Session; user: User }
75 | { session: null; user: null };
76
77// Validate a session token by converting it to its SHA‑256 hash, checking expiration, and fetching the user.
78export async function validateSessionToken(token: string): Promise<SessionValidationResult> {
79 log.debug("Validating session token");
80 const sessionId = await sha256(token);
81 const session = await db.query.sessions.findFirst({
82 where: eq(sessions.id, sessionId)
83 });
84 if (!session) {
85 log.debug("Session not found", { sessionId });
86 return { session: null, user: null };
87 }
88 const now = Date.now();
89 if (session.expiresAt < now) {
90 log.debug("Session expired", { sessionId });
91 return { session: null, user: null };
92 }
93 const user = await db.query.users.findFirst({
94 where: eq(users.id, session.userId)
95 });
96 if (!user) {
97 log.debug("User not found for session", { sessionId });
98 return { session: null, user: null };
99 }
100 log.debug("Session validated", { sessionId, userId: user.id });
101 return { session, user };
102}
103
104// Invalidate a specific session by deleting it from the database.
105export async function invalidateSession(sessionId: string): Promise<void> {
106 log.debug("Invalidating session", { sessionId });
107 await db.delete(sessions).where(eq(sessions.id, sessionId));
108 log.debug("Session invalidated", { sessionId });
109}
110
111// Invalidate all sessions for a given user.
112export async function invalidateAllSessions(userId: string): Promise<void> {
113 log.debug("Invalidating all sessions for user", { userId });
114 await db.delete(sessions).where(eq(sessions.userId, userId));
115 log.debug("All sessions invalidated", { userId });
116}