feat: implement server-side session management with session ID storage and cleanup
Build And Push Image / docker (push) Successful in 2m51s Details

This commit is contained in:
Matt 2025-08-07 14:16:54 +02:00
parent fe5aed075f
commit af4fae6378
3 changed files with 109 additions and 58 deletions

View File

@ -307,19 +307,23 @@ export default defineEventHandler(async (event) => {
sessionSize: JSON.stringify(sessionData).length sessionSize: JSON.stringify(sessionData).length
}); });
// Create session with appropriate expiration // Create session with server-side storage
const sessionManager = createSessionManager(); const sessionManager = createSessionManager();
const maxAge = !!rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; // 30 days vs 7 days
try { try {
// Create the encrypted session data // Create session and get cookie string
const sessionData_json = JSON.stringify(sessionData); const cookieString = sessionManager.createSession(sessionData, !!rememberMe);
const encrypted = sessionManager.encrypt(sessionData_json);
console.log(`🍪 Setting session cookie (Remember Me: ${!!rememberMe}), size: ${encrypted.length} chars`); // Parse the cookie string to get the session ID
const cookieParts = cookieString.split(';')[0].split('=');
const sessionId = cookieParts[1];
// Use Nuxt's setCookie helper directly with the encrypted value console.log(`🍪 Setting session cookie (Remember Me: ${!!rememberMe}), session ID: ${sessionId.substring(0, 8)}...`);
setCookie(event, 'monacousa-session', encrypted, { console.log(`📏 Cookie size: ${sessionId.length} chars (much smaller!)`);
// Set the cookie using Nuxt's setCookie helper
const maxAge = !!rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; // 30 days vs 7 days
setCookie(event, 'monacousa-session', sessionId, {
httpOnly: true, httpOnly: true,
secure: true, secure: true,
sameSite: 'none', sameSite: 'none',

View File

@ -1,8 +1,15 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const sessionManager = createSessionManager(); const sessionManager = createSessionManager();
const destroyCookie = sessionManager.destroySession(); const cookieHeader = getHeader(event, 'cookie');
console.log('🚪 Logout requested');
// Clear the session from server-side store
const destroyCookie = sessionManager.destroySession(cookieHeader);
setHeader(event, 'Set-Cookie', destroyCookie); setHeader(event, 'Set-Cookie', destroyCookie);
console.log('✅ Logout successful');
return { success: true }; return { success: true };
}); });

View File

@ -1,85 +1,125 @@
import { serialize, parse } from 'cookie'; import { serialize, parse } from 'cookie';
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import type { SessionData } from '~/utils/types'; import type { SessionData } from '~/utils/types';
// In-memory session store (in production, you'd use Redis or a database)
const sessionStore = new Map<string, {
data: SessionData;
expiresAt: number;
rememberMe: boolean;
}>();
// Cleanup expired sessions every 5 minutes
setInterval(() => {
const now = Date.now();
for (const [sessionId, session] of sessionStore.entries()) {
if (now > session.expiresAt) {
sessionStore.delete(sessionId);
console.log('🧹 Cleaned up expired session:', sessionId.substring(0, 8) + '...');
}
}
}, 5 * 60 * 1000);
export class SessionManager { export class SessionManager {
private encryptionKey: Buffer;
private cookieName = 'monacousa-session'; private cookieName = 'monacousa-session';
constructor(encryptionKey: string) { constructor() {
this.encryptionKey = Buffer.from(encryptionKey, 'hex'); // No encryption key needed since we're only storing session IDs
} }
encrypt(data: string): string { private generateSessionId(): string {
const iv = randomBytes(16); return randomBytes(32).toString('hex');
const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
private decrypt(encryptedData: string): string {
const [ivHex, encrypted] = encryptedData.split(':');
const iv = Buffer.from(ivHex, 'hex');
const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} }
createSession(sessionData: SessionData, rememberMe: boolean = false): string { createSession(sessionData: SessionData, rememberMe: boolean = false): string {
const data = JSON.stringify(sessionData); const sessionId = this.generateSessionId();
const encrypted = this.encrypt(data);
const maxAge = rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; // 30 days vs 7 days const maxAge = rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; // 30 days vs 7 days
const expiresAt = Date.now() + (maxAge * 1000);
console.log(`🍪 Creating session cookie (Remember Me: ${rememberMe}) without explicit domain`); // Store session data server-side
sessionStore.set(sessionId, {
data: sessionData,
expiresAt,
rememberMe
});
return serialize(this.cookieName, encrypted, { console.log(`🍪 Creating session cookie (Remember Me: ${rememberMe}) with session ID: ${sessionId.substring(0, 8)}...`);
console.log(`📊 Session store size: ${sessionStore.size} sessions`);
return serialize(this.cookieName, sessionId, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: true,
sameSite: 'lax', sameSite: 'none',
maxAge, maxAge,
path: '/', path: '/',
}); });
} }
getSession(cookieHeader?: string): SessionData | null { getSession(cookieHeader?: string): SessionData | null {
if (!cookieHeader) return null; if (!cookieHeader) {
console.log('❌ No cookie header provided');
const cookies = parse(cookieHeader);
const sessionCookie = cookies[this.cookieName];
if (!sessionCookie) return null;
try {
const decrypted = this.decrypt(sessionCookie);
const sessionData = JSON.parse(decrypted) as SessionData;
// Check if session is expired
if (Date.now() > sessionData.tokens.expiresAt) {
return null;
}
return sessionData;
} catch (error) {
console.error('Failed to decrypt session:', error);
return null; return null;
} }
const cookies = parse(cookieHeader);
const sessionId = cookies[this.cookieName];
if (!sessionId) {
console.log('❌ No session cookie found');
return null;
}
console.log(`🔍 Looking up session: ${sessionId.substring(0, 8)}...`);
const sessionEntry = sessionStore.get(sessionId);
if (!sessionEntry) {
console.log('❌ Session not found in store');
return null;
}
// Check if session is expired
if (Date.now() > sessionEntry.expiresAt) {
console.log('❌ Session expired, removing from store');
sessionStore.delete(sessionId);
return null;
}
// Update last activity
sessionEntry.data.lastActivity = Date.now();
console.log('✅ Session found and valid for user:', sessionEntry.data.user.email);
return sessionEntry.data;
} }
destroySession(): string { 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)}...`);
}
}
return serialize(this.cookieName, '', { return serialize(this.cookieName, '', {
httpOnly: true, httpOnly: true,
secure: true, secure: true,
sameSite: 'lax', sameSite: 'none',
maxAge: 0, maxAge: 0,
path: '/', path: '/',
}); });
} }
// Helper method to get session stats
getSessionStats() {
return {
totalSessions: sessionStore.size,
activeSessions: Array.from(sessionStore.values()).filter(s => Date.now() < s.expiresAt).length
};
}
} }
export function createSessionManager(): SessionManager { export function createSessionManager(): SessionManager {
const config = useRuntimeConfig(); return new SessionManager();
return new SessionManager(config.encryptionKey);
} }