feat: implement server-side session management with session ID storage and cleanup
Build And Push Image / docker (push) Successful in 2m51s
Details
Build And Push Image / docker (push) Successful in 2m51s
Details
This commit is contained in:
parent
fe5aed075f
commit
af4fae6378
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue