Import source: backend/src/middleware/session

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:12:28 +03:30
parent 04a7c5ee93
commit 9ee12a84da
+86
View File
@@ -0,0 +1,86 @@
import { randomBytes } from 'node:crypto';
import type { Request, RequestHandler } from 'express';
import { sha256 } from '../utils/crypto.js';
import { env } from '../config/env.js';
import { store } from '../services/store.js';
import type { DatabaseShape } from '../types.js';
export const SESSION_COOKIE = 'cshare_sid';
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
const ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000; // Rotate session once a day
// Map of sessionId -> { clientHash: string, expiresAt: number }
const sessionCache = new Map<string, { clientHash: string, expiresAt: number }>();
function getClientFingerprint(req: Request): string {
const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown';
const ua = req.headers['user-agent'] as string ?? 'unknown';
return sha256(`${ip}:${ua}`);
}
/** Issues a long-lived HTTP-only, SameSite=Strict secure session cookie on first visit, bound to client IP/UA, and rotates periodically. */
export const sessionMiddleware: RequestHandler = (req, res, next) => {
const currentSid = req.cookies?.[SESSION_COOKIE];
const clientHash = getClientFingerprint(req);
const now = Date.now();
if (!currentSid) {
const sid = randomBytes(32).toString('hex');
res.cookie(SESSION_COOKIE, sid, {
httpOnly: true,
sameSite: 'strict',
secure: env.nodeEnv === 'production',
maxAge: ONE_YEAR_MS,
path: '/',
});
req.cookies[SESSION_COOKIE] = sid;
sessionCache.set(sid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS });
} else {
if (!sessionCache.has(currentSid)) {
sessionCache.set(currentSid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS });
}
const cached = sessionCache.get(currentSid)!;
// Bind session to the client's fingerprint to prevent cookie hijacking
if (cached.clientHash !== clientHash) {
// Fingerprint mismatch — clear cookie, force new session
res.clearCookie(SESSION_COOKIE);
const sid = randomBytes(32).toString('hex');
res.cookie(SESSION_COOKIE, sid, {
httpOnly: true,
sameSite: 'strict',
secure: env.nodeEnv === 'production',
maxAge: ONE_YEAR_MS,
path: '/',
});
req.cookies[SESSION_COOKIE] = sid;
sessionCache.set(sid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS });
} else if (now > cached.expiresAt) {
// Periodic rotation logic: rotate the session cookie value to limit lifetime of any stolen session id
sessionCache.delete(currentSid);
const newSid = randomBytes(32).toString('hex');
res.cookie(SESSION_COOKIE, newSid, {
httpOnly: true,
sameSite: 'strict',
secure: env.nodeEnv === 'production',
maxAge: ONE_YEAR_MS,
path: '/',
});
req.cookies[SESSION_COOKIE] = newSid;
sessionCache.set(newSid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS });
// Migrate file ownership to the new session ID so uploads survive rotation.
store.update((db: DatabaseShape) => {
for (const file of db.files) {
if (file.sessionId === currentSid) {
file.sessionId = newSid;
}
}
}).catch(() => {});
}
}
next();
};
export function getSessionId(req: Request): string {
return req.cookies?.[SESSION_COOKIE] ?? '';
}