seed data
This commit is contained in:
26
adapters/http/RequestContext.ts
Normal file
26
adapters/http/RequestContext.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
|
||||
export type HttpRequestContext = {
|
||||
req: Request;
|
||||
res: Response;
|
||||
};
|
||||
|
||||
const requestContextStorage = new AsyncLocalStorage<HttpRequestContext>();
|
||||
|
||||
export function tryGetHttpRequestContext(): HttpRequestContext | null {
|
||||
return requestContextStorage.getStore() ?? null;
|
||||
}
|
||||
|
||||
export function getHttpRequestContext(): HttpRequestContext {
|
||||
const ctx = tryGetHttpRequestContext();
|
||||
if (!ctx) {
|
||||
throw new Error('HttpRequestContext is not available (missing requestContextMiddleware)');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function requestContextMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
requestContextStorage.run({ req, res }, next);
|
||||
}
|
||||
@@ -1,46 +1,145 @@
|
||||
/**
|
||||
* Adapter: CookieIdentitySessionAdapter
|
||||
*
|
||||
* Manages user session using cookies. This is a placeholder implementation.
|
||||
*/
|
||||
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 currentSession: AuthSession | null = null;
|
||||
private readonly sessionsByToken = new Map<string, StoredSession>();
|
||||
|
||||
constructor(private readonly logger: Logger) {
|
||||
this.logger.info('CookieIdentitySessionAdapter initialized.');
|
||||
// In a real application, you would load the session from a cookie here
|
||||
// For demo, we'll start with no session.
|
||||
this.logger.info('[CookieIdentitySessionAdapter] initialized (in-memory cookie sessions).');
|
||||
}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSession | null> {
|
||||
this.logger.debug('[CookieIdentitySessionAdapter] Getting current session.');
|
||||
return Promise.resolve(this.currentSession);
|
||||
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> {
|
||||
this.logger.debug(`[CookieIdentitySessionAdapter] Creating session for user: ${user.id}`);
|
||||
const newSession: AuthSession = {
|
||||
user: user,
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 3600 * 1000, // 1 hour expiration
|
||||
token: `mock-token-${user.id}-${Date.now()}`,
|
||||
const issuedAt = Date.now();
|
||||
const expiresAt = issuedAt + SESSION_TTL_MS;
|
||||
|
||||
const token = `gp_${randomUUID()}`;
|
||||
|
||||
const session: AuthSession = {
|
||||
user,
|
||||
issuedAt,
|
||||
expiresAt,
|
||||
token,
|
||||
};
|
||||
this.currentSession = newSession;
|
||||
// In a real app, you'd set a secure, HTTP-only cookie here.
|
||||
this.logger.info(`[CookieIdentitySessionAdapter] Session created for user ${user.id}.`);
|
||||
return Promise.resolve(newSession);
|
||||
|
||||
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> {
|
||||
this.logger.debug('[CookieIdentitySessionAdapter] Clearing session.');
|
||||
this.currentSession = null;
|
||||
// In a real app, you'd clear the session cookie here.
|
||||
this.logger.info('[CookieIdentitySessionAdapter] Session cleared.');
|
||||
return Promise.resolve();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user