diff --git a/dbal/ts/src/core/entities/session/clean-expired.ts b/dbal/ts/src/core/entities/session/clean-expired.ts new file mode 100644 index 000000000..2c0f9f6e3 --- /dev/null +++ b/dbal/ts/src/core/entities/session/clean-expired.ts @@ -0,0 +1,31 @@ +/** + * @file clean-expired.ts + * @description Clean expired sessions operation + */ +import type { Result } from '../types'; +import type { InMemoryStore } from '../store/in-memory-store'; + +/** + * Clean up expired sessions + * @returns Number of sessions removed + */ +export async function cleanExpiredSessions(store: InMemoryStore): Promise> { + const now = new Date(); + const expiredIds: string[] = []; + + for (const [id, session] of store.sessions) { + if (session.expiresAt < now) { + expiredIds.push(id); + } + } + + for (const id of expiredIds) { + const session = store.sessions.get(id); + if (session) { + store.sessionTokens.delete(session.token); + store.sessions.delete(id); + } + } + + return { success: true, data: expiredIds.length }; +} diff --git a/dbal/ts/src/core/entities/session/create-session.ts b/dbal/ts/src/core/entities/session/create-session.ts new file mode 100644 index 000000000..db6f57667 --- /dev/null +++ b/dbal/ts/src/core/entities/session/create-session.ts @@ -0,0 +1,39 @@ +/** + * @file create-session.ts + * @description Create session operation + */ +import type { Session, CreateSessionInput, Result } from '../types'; +import type { InMemoryStore } from '../store/in-memory-store'; + +/** + * Create a new session in the store + */ +export async function createSession( + store: InMemoryStore, + input: CreateSessionInput +): Promise> { + if (!input.userId) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: 'User ID required' } }; + } + if (!store.users.has(input.userId)) { + return { success: false, error: { code: 'NOT_FOUND', message: 'User not found' } }; + } + if (input.ttlSeconds <= 0) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: 'TTL must be positive' } }; + } + + const session: Session = { + id: store.generateId('session'), + userId: input.userId, + token: store.generateToken(), + expiresAt: new Date(Date.now() + input.ttlSeconds * 1000), + ipAddress: input.ipAddress ?? '', + userAgent: input.userAgent ?? '', + createdAt: new Date(), + }; + + store.sessions.set(session.id, session); + store.sessionTokens.set(session.token, session.id); + + return { success: true, data: session }; +} diff --git a/dbal/ts/src/core/entities/session/delete-session.ts b/dbal/ts/src/core/entities/session/delete-session.ts new file mode 100644 index 000000000..04609b635 --- /dev/null +++ b/dbal/ts/src/core/entities/session/delete-session.ts @@ -0,0 +1,25 @@ +/** + * @file delete-session.ts + * @description Delete session operation + */ +import type { Result } from '../types'; +import type { InMemoryStore } from '../store/in-memory-store'; + +/** + * Delete a session by ID (logout) + */ +export async function deleteSession(store: InMemoryStore, id: string): Promise> { + if (!id) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: 'ID required' } }; + } + + const session = store.sessions.get(id); + if (!session) { + return { success: false, error: { code: 'NOT_FOUND', message: `Session not found: ${id}` } }; + } + + store.sessionTokens.delete(session.token); + store.sessions.delete(id); + + return { success: true, data: true }; +} diff --git a/dbal/ts/src/core/entities/session/extend-session.ts b/dbal/ts/src/core/entities/session/extend-session.ts new file mode 100644 index 000000000..e310bbf49 --- /dev/null +++ b/dbal/ts/src/core/entities/session/extend-session.ts @@ -0,0 +1,34 @@ +/** + * @file extend-session.ts + * @description Extend session expiration operation + */ +import type { Session, Result } from '../types'; +import type { InMemoryStore } from '../store/in-memory-store'; + +/** + * Extend a session's expiration time + */ +export async function extendSession( + store: InMemoryStore, + id: string, + additionalSeconds: number +): Promise> { + if (!id) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: 'ID required' } }; + } + if (additionalSeconds <= 0) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: 'Additional seconds must be positive' } }; + } + + const session = store.sessions.get(id); + if (!session) { + return { success: false, error: { code: 'NOT_FOUND', message: `Session not found: ${id}` } }; + } + + if (session.expiresAt < new Date()) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: 'Cannot extend expired session' } }; + } + + session.expiresAt = new Date(session.expiresAt.getTime() + additionalSeconds * 1000); + return { success: true, data: session }; +} diff --git a/dbal/ts/src/core/entities/session/get-session.ts b/dbal/ts/src/core/entities/session/get-session.ts new file mode 100644 index 000000000..1bc5351b4 --- /dev/null +++ b/dbal/ts/src/core/entities/session/get-session.ts @@ -0,0 +1,38 @@ +/** + * @file get-session.ts + * @description Get session operations + */ +import type { Session, Result } from '../types'; +import type { InMemoryStore } from '../store/in-memory-store'; + +/** + * Get a session by ID + */ +export async function getSession(store: InMemoryStore, id: string): Promise> { + if (!id) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: 'ID required' } }; + } + + const session = store.sessions.get(id); + if (!session) { + return { success: false, error: { code: 'NOT_FOUND', message: `Session not found: ${id}` } }; + } + + return { success: true, data: session }; +} + +/** + * Get a session by token + */ +export async function getSessionByToken(store: InMemoryStore, token: string): Promise> { + if (!token) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: 'Token required' } }; + } + + const id = store.sessionTokens.get(token); + if (!id) { + return { success: false, error: { code: 'NOT_FOUND', message: 'Session not found' } }; + } + + return getSession(store, id); +} diff --git a/dbal/ts/src/core/entities/session/index.ts b/dbal/ts/src/core/entities/session/index.ts new file mode 100644 index 000000000..5f307968a --- /dev/null +++ b/dbal/ts/src/core/entities/session/index.ts @@ -0,0 +1,10 @@ +/** + * @file index.ts + * @description Barrel export for session operations + */ +export { createSession } from './create-session'; +export { getSession, getSessionByToken } from './get-session'; +export { extendSession } from './extend-session'; +export { deleteSession } from './delete-session'; +export { listSessions } from './list-sessions'; +export { cleanExpiredSessions } from './clean-expired'; diff --git a/dbal/ts/src/core/entities/session/list-sessions.ts b/dbal/ts/src/core/entities/session/list-sessions.ts new file mode 100644 index 000000000..38e00e907 --- /dev/null +++ b/dbal/ts/src/core/entities/session/list-sessions.ts @@ -0,0 +1,36 @@ +/** + * @file list-sessions.ts + * @description List sessions with filtering + */ +import type { Session, ListOptions, Result } from '../types'; +import type { InMemoryStore } from '../store/in-memory-store'; + +/** + * List sessions with filtering and pagination + */ +export async function listSessions( + store: InMemoryStore, + options: ListOptions = {} +): Promise> { + const { filter = {}, page = 1, limit = 20 } = options; + const now = new Date(); + + let sessions = Array.from(store.sessions.values()); + + // Apply filters + if (filter.userId !== undefined) { + sessions = sessions.filter((s) => s.userId === filter.userId); + } + if (filter.activeOnly) { + sessions = sessions.filter((s) => s.expiresAt > now); + } + + // Sort by created_at descending (newest first) + sessions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + + // Apply pagination + const start = (page - 1) * limit; + const paginated = sessions.slice(start, start + limit); + + return { success: true, data: paginated }; +}