1import { sessions } from '../db/convex.server';
2import type { Session, SafeUser } from '../types/chat';
3import { createLogger } from '../utils/logger.server';
4
5const log = createLogger('session-server');
6
7// Helper: Compute SHA-256 hash of a message and encode it as a hex string.
8export async function sha256(message: string): Promise<string> {
9 const encoder = new TextEncoder();
10 const data = encoder.encode(message);
11 const hashBuffer = await crypto.subtle.digest('SHA-256', data);
12 const hashArray = Array.from(new Uint8Array(hashBuffer));
13 const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
14 return hashHex;
15}
16
17// Helper: Custom Base32 encoder using the standard alphabet.
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 log.debug('Generated session token', { tokenPrefix: token.slice(0, 4) + '***' });
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 log.debug('Creating session', { userId });
52 const tokenHash = await sha256(token);
53 const createdAt = Date.now();
54 const expiresAt = createdAt + 7 * 24 * 60 * 60 * 1000; // 7 days expiry
55
56 await sessions.create(tokenHash, userId, expiresAt);
57
58 log.debug('Session created', { tokenHash });
59 return { id: tokenHash, userId, createdAt, expiresAt };
60}
61
62/**
63 * Type distributed through our API.
64 * Returns an object with both session and user if valid, or nulls if not.
65 */
66export type SessionValidationResult =
67 { session: Session; user: SafeUser } | { session: null; user: null };
68
69// Validate a session token by converting it to its SHA-256 hash, checking expiration, and fetching the user.
70export async function validateSessionToken(token: string): Promise<SessionValidationResult> {
71 log.debug('Validating session token');
72 const tokenHash = await sha256(token);
73
74 const result = await sessions.validate(tokenHash);
75
76 if (!result.session || !result.user) {
77 log.debug('Session not found or invalid', { tokenHash });
78 return { session: null, user: null };
79 }
80
81 // Map Convex types to our local types - using native Convex IDs
82 const session: Session = {
83 id: result.session.tokenHash,
84 userId: result.user._id,
85 createdAt: result.session.createdAt,
86 expiresAt: result.session.expiresAt
87 };
88
89 const user: SafeUser = {
90 id: result.user._id,
91 nickname: result.user.nickname,
92 status: result.user.status as SafeUser['status'],
93 avatarUrl: result.user.avatarUrl,
94 lastSeen: result.user.lastSeen,
95 lastReadMentionTimestamp: result.user.lastReadMentionTimestamp
96 };
97
98 log.debug('Session validated', { tokenHash, userId: user.id });
99 return { session, user };
100}
101
102// Invalidate a specific session by deleting it from the database.
103export async function invalidateSession(tokenHash: string): Promise<void> {
104 log.debug('Invalidating session', { tokenHash });
105 await sessions.remove(tokenHash);
106 log.debug('Session invalidated', { tokenHash });
107}