145 lines
4.2 KiB
TypeScript
145 lines
4.2 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
|
|
import type { AuthenticatedUser } from '@core/identity/application/ports/IdentityProviderPort';
|
|
import type { AuthSession, IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
|
|
import type { Logger } from '@core/shared/application';
|
|
import { tryGetHttpRequestContext } from '@adapters/http/RequestContext';
|
|
|
|
const COOKIE_NAME = 'gp_session';
|
|
const SESSION_TTL_MS = 3600 * 1000;
|
|
|
|
type StoredSession = AuthSession;
|
|
|
|
function readCookieHeader(headerValue: string | undefined): Record<string, string> {
|
|
if (!headerValue) return {};
|
|
const pairs = headerValue.split(';');
|
|
const record: Record<string, string> = {};
|
|
for (const pair of pairs) {
|
|
const trimmed = pair.trim();
|
|
if (!trimmed) continue;
|
|
const eq = trimmed.indexOf('=');
|
|
if (eq === -1) continue;
|
|
const name = trimmed.slice(0, eq).trim();
|
|
const value = trimmed.slice(eq + 1).trim();
|
|
if (!name) continue;
|
|
record[name] = decodeURIComponent(value);
|
|
}
|
|
return record;
|
|
}
|
|
|
|
function buildSetCookieHeader(options: {
|
|
name: string;
|
|
value: string;
|
|
maxAgeSeconds: number;
|
|
httpOnly: boolean;
|
|
sameSite: 'Lax' | 'Strict' | 'None';
|
|
secure: boolean;
|
|
path?: string;
|
|
}): string {
|
|
const parts: string[] = [];
|
|
parts.push(`${options.name}=${encodeURIComponent(options.value)}`);
|
|
parts.push(`Max-Age=${options.maxAgeSeconds}`);
|
|
parts.push(`Path=${options.path ?? '/'}`);
|
|
if (options.httpOnly) parts.push('HttpOnly');
|
|
parts.push(`SameSite=${options.sameSite}`);
|
|
if (options.secure) parts.push('Secure');
|
|
return parts.join('; ');
|
|
}
|
|
|
|
function appendSetCookieHeader(existing: string | string[] | undefined, next: string): string[] {
|
|
if (!existing) return [next];
|
|
if (Array.isArray(existing)) return [...existing, next];
|
|
return [existing, next];
|
|
}
|
|
|
|
export class CookieIdentitySessionAdapter implements IdentitySessionPort {
|
|
private readonly sessionsByToken = new Map<string, StoredSession>();
|
|
|
|
constructor(private readonly logger: Logger) {
|
|
this.logger.info('[CookieIdentitySessionAdapter] initialized (in-memory cookie sessions).');
|
|
}
|
|
|
|
async getCurrentSession(): Promise<AuthSession | null> {
|
|
const ctx = tryGetHttpRequestContext();
|
|
if (!ctx) {
|
|
// Called outside HTTP request (e.g. some unit tests). Behave as no session.
|
|
return null;
|
|
}
|
|
|
|
const cookies = readCookieHeader(ctx.req.headers.cookie);
|
|
const token = cookies[COOKIE_NAME];
|
|
if (!token) return null;
|
|
|
|
const session = this.sessionsByToken.get(token) ?? null;
|
|
if (!session) return null;
|
|
|
|
const now = Date.now();
|
|
if (session.expiresAt <= now) {
|
|
this.sessionsByToken.delete(token);
|
|
return null;
|
|
}
|
|
|
|
return session;
|
|
}
|
|
|
|
async createSession(user: AuthenticatedUser): Promise<AuthSession> {
|
|
const issuedAt = Date.now();
|
|
const expiresAt = issuedAt + SESSION_TTL_MS;
|
|
|
|
const token = `gp_${randomUUID()}`;
|
|
|
|
const session: AuthSession = {
|
|
user,
|
|
issuedAt,
|
|
expiresAt,
|
|
token,
|
|
};
|
|
|
|
this.sessionsByToken.set(token, session);
|
|
|
|
const ctx = tryGetHttpRequestContext();
|
|
if (ctx) {
|
|
const setCookie = buildSetCookieHeader({
|
|
name: COOKIE_NAME,
|
|
value: token,
|
|
maxAgeSeconds: Math.floor(SESSION_TTL_MS / 1000),
|
|
httpOnly: true,
|
|
sameSite: 'Lax',
|
|
secure: false,
|
|
});
|
|
|
|
const existing = ctx.res.getHeader('Set-Cookie');
|
|
ctx.res.setHeader('Set-Cookie', appendSetCookieHeader(existing as any, setCookie));
|
|
}
|
|
|
|
return session;
|
|
}
|
|
|
|
async clearSession(): Promise<void> {
|
|
const ctx = tryGetHttpRequestContext();
|
|
|
|
if (ctx) {
|
|
const cookies = readCookieHeader(ctx.req.headers.cookie);
|
|
const token = cookies[COOKIE_NAME];
|
|
if (token) {
|
|
this.sessionsByToken.delete(token);
|
|
}
|
|
|
|
const setCookie = buildSetCookieHeader({
|
|
name: COOKIE_NAME,
|
|
value: '',
|
|
maxAgeSeconds: 0,
|
|
httpOnly: true,
|
|
sameSite: 'Lax',
|
|
secure: false,
|
|
});
|
|
|
|
const existing = ctx.res.getHeader('Set-Cookie');
|
|
ctx.res.setHeader('Set-Cookie', appendSetCookieHeader(existing as any, setCookie));
|
|
return;
|
|
}
|
|
|
|
// No request context: nothing to clear from cookie; just clear all in-memory sessions.
|
|
this.sessionsByToken.clear();
|
|
}
|
|
} |