2025-08-06 14:31:16 +02:00
|
|
|
import { serialize, parse } from 'cookie';
|
2025-08-07 14:16:54 +02:00
|
|
|
import { randomBytes } from 'crypto';
|
2025-08-06 14:31:16 +02:00
|
|
|
import type { SessionData } from '~/utils/types';
|
|
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
// In-memory session store (in production, you'd use Redis or a database)
|
|
|
|
|
const sessionStore = new Map<string, {
|
|
|
|
|
data: SessionData;
|
|
|
|
|
expiresAt: number;
|
|
|
|
|
rememberMe: boolean;
|
|
|
|
|
}>();
|
2025-08-06 14:31:16 +02:00
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
// Cleanup expired sessions every 5 minutes
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
const now = Date.now();
|
2025-08-07 15:14:02 +02:00
|
|
|
let cleanedCount = 0;
|
2025-08-07 14:16:54 +02:00
|
|
|
for (const [sessionId, session] of sessionStore.entries()) {
|
|
|
|
|
if (now > session.expiresAt) {
|
|
|
|
|
sessionStore.delete(sessionId);
|
2025-08-07 15:14:02 +02:00
|
|
|
cleanedCount++;
|
2025-08-07 14:16:54 +02:00
|
|
|
}
|
2025-08-06 14:31:16 +02:00
|
|
|
}
|
2025-08-07 15:14:02 +02:00
|
|
|
if (cleanedCount > 0) {
|
|
|
|
|
console.log(`🧹 Cleaned up ${cleanedCount} expired sessions`);
|
|
|
|
|
}
|
2025-08-07 14:16:54 +02:00
|
|
|
}, 5 * 60 * 1000);
|
|
|
|
|
|
|
|
|
|
export class SessionManager {
|
|
|
|
|
private cookieName = 'monacousa-session';
|
2025-08-06 14:31:16 +02:00
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
constructor() {
|
|
|
|
|
// No encryption key needed since we're only storing session IDs
|
2025-08-06 14:31:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
private generateSessionId(): string {
|
|
|
|
|
return randomBytes(32).toString('hex');
|
2025-08-06 14:31:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-07 03:43:25 +02:00
|
|
|
createSession(sessionData: SessionData, rememberMe: boolean = false): string {
|
2025-08-07 14:16:54 +02:00
|
|
|
const sessionId = this.generateSessionId();
|
2025-08-07 03:43:25 +02:00
|
|
|
const maxAge = rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; // 30 days vs 7 days
|
2025-08-07 14:16:54 +02:00
|
|
|
const expiresAt = Date.now() + (maxAge * 1000);
|
|
|
|
|
|
|
|
|
|
// Store session data server-side
|
|
|
|
|
sessionStore.set(sessionId, {
|
|
|
|
|
data: sessionData,
|
|
|
|
|
expiresAt,
|
|
|
|
|
rememberMe
|
|
|
|
|
});
|
2025-08-07 03:43:25 +02:00
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
return serialize(this.cookieName, sessionId, {
|
2025-08-06 14:31:16 +02:00
|
|
|
httpOnly: true,
|
2025-08-07 14:16:54 +02:00
|
|
|
secure: true,
|
|
|
|
|
sameSite: 'none',
|
2025-08-07 03:43:25 +02:00
|
|
|
maxAge,
|
2025-08-06 14:31:16 +02:00
|
|
|
path: '/',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSession(cookieHeader?: string): SessionData | null {
|
2025-08-07 14:16:54 +02:00
|
|
|
if (!cookieHeader) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-08-06 14:31:16 +02:00
|
|
|
|
|
|
|
|
const cookies = parse(cookieHeader);
|
2025-08-07 14:16:54 +02:00
|
|
|
const sessionId = cookies[this.cookieName];
|
2025-08-06 14:31:16 +02:00
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
if (!sessionId) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-08-06 14:31:16 +02:00
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
const sessionEntry = sessionStore.get(sessionId);
|
|
|
|
|
if (!sessionEntry) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-08-06 14:31:16 +02:00
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
// Check if session is expired
|
|
|
|
|
if (Date.now() > sessionEntry.expiresAt) {
|
|
|
|
|
sessionStore.delete(sessionId);
|
2025-08-06 14:31:16 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
2025-08-07 14:16:54 +02:00
|
|
|
|
|
|
|
|
// Update last activity
|
|
|
|
|
sessionEntry.data.lastActivity = Date.now();
|
|
|
|
|
|
|
|
|
|
return sessionEntry.data;
|
2025-08-06 14:31:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-07 14:16:54 +02:00
|
|
|
destroySession(cookieHeader?: string): string {
|
|
|
|
|
if (cookieHeader) {
|
|
|
|
|
const cookies = parse(cookieHeader);
|
|
|
|
|
const sessionId = cookies[this.cookieName];
|
|
|
|
|
|
|
|
|
|
if (sessionId && sessionStore.has(sessionId)) {
|
|
|
|
|
sessionStore.delete(sessionId);
|
|
|
|
|
console.log(`🗑️ Destroyed session: ${sessionId.substring(0, 8)}...`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-06 14:31:16 +02:00
|
|
|
return serialize(this.cookieName, '', {
|
|
|
|
|
httpOnly: true,
|
|
|
|
|
secure: true,
|
2025-08-07 14:16:54 +02:00
|
|
|
sameSite: 'none',
|
2025-08-06 14:31:16 +02:00
|
|
|
maxAge: 0,
|
|
|
|
|
path: '/',
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-08-07 14:16:54 +02:00
|
|
|
|
|
|
|
|
// Helper method to get session stats
|
|
|
|
|
getSessionStats() {
|
|
|
|
|
return {
|
|
|
|
|
totalSessions: sessionStore.size,
|
|
|
|
|
activeSessions: Array.from(sessionStore.values()).filter(s => Date.now() < s.expiresAt).length
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-08-06 14:31:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createSessionManager(): SessionManager {
|
2025-08-07 14:16:54 +02:00
|
|
|
return new SessionManager();
|
2025-08-06 14:31:16 +02:00
|
|
|
}
|