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