// src/services/session.ts // Copyright (C) 2026 DTP Technologies, LLC // All Rights Reserved import crypto from "crypto"; import dayjs from "dayjs"; import { DtpService } from "../lib/service.js"; import Session, { ISession } from "../models/session.js"; import User from "../models/user.js"; import ChatSession from "../models/chat-session.js"; export interface SessionOptions { expiresHours: number; maxInactiveHours: number; } export class SessionService extends DtpService { get name(): string { return "SessionService"; } get slug(): string { return "session"; } private defaultExpiresHours = 24; constructor() { super(); } async start(): Promise { this.log.info("service started"); // Start session cleanup cron job this.startCleanupJob(); } async stop(): Promise { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.log.info("service stopped"); } private cleanupInterval: NodeJS.Timeout | null = null; private startCleanupJob(): void { // Clean up expired sessions every hour this.cleanupInterval = setInterval( async () => { try { const result = await Session.deleteMany({ expiresAt: { $lt: new Date() }, }); if (result.deletedCount > 0) { this.log.info(`Cleaned up ${result.deletedCount} expired sessions`); } } catch (error) { this.log.error("Session cleanup failed", { error }); } }, 60 * 60 * 1000, ); // 1 hour } async create( userId: string, ipAddress?: string, userAgent?: string, options?: SessionOptions, ): Promise { const NOW = new Date(); const expiresHours = options?.expiresHours || this.defaultExpiresHours; const session = new Session(); session.userId = new (await import("mongoose")).Types.ObjectId(userId); session.token = crypto.randomBytes(64).toString("hex"); session.createdAt = NOW; session.expiresAt = dayjs(NOW).add(expiresHours, "hour").toDate(); session.lastActivityAt = NOW; session.ipAddress = ipAddress; session.userAgent = userAgent; await session.save(); this.log.debug("Session created", { userId, sessionId: session._id }); return session; } async findById(sessionId: string): Promise { const session = await Session.findById(sessionId).populate("userId"); if (!session) { this.log.debug("Session not found", { sessionId }); return null; } // Check if session is expired if (session.expiresAt < new Date()) { this.log.debug("Session expired", { sessionId: session._id }); await Session.deleteOne({ _id: session._id }); return null; } // Update last activity session.lastActivityAt = new Date(); await session.save(); this.log.debug("Session found", { sessionId: session._id }); return session; } async findByToken(token: string): Promise { const session = await Session.findOne({ token }).populate("userId"); if (!session) { return null; } // Check if session is expired if (session.expiresAt < new Date()) { this.log.debug("Session expired", { sessionId: session._id }); await Session.deleteOne({ _id: session._id }); return null; } // Update last activity session.lastActivityAt = new Date(); await session.save(); return session; } async revoke(token: string): Promise { const result = await Session.deleteOne({ token }); if (result.deletedCount > 0) { this.log.debug("Session revoked", { token }); return true; } return false; } async revokeAllForUser(userId: string): Promise { const result = await Session.deleteMany({ userId }); this.log.debug("Revoked all sessions for user", { userId, count: result.deletedCount, }); return result.deletedCount; } async extend(session: ISession, hours?: number): Promise { const extendHours = hours || this.defaultExpiresHours; session.expiresAt = dayjs(session.expiresAt) .add(extendHours, "hour") .toDate(); session.lastActivityAt = new Date(); await session.save(); this.log.debug("Session extended", { sessionId: session._id }); return session; } async getUserFromSession(session: ISession) { if (!session.userId) { return null; } // userId is already populated from findByToken return session.userId as unknown as typeof User.prototype; } async listByProject( projectId: string, userId: string, ): Promise< Array<{ _id: string; name: string; lastMessageAt: Date | null; turnCount: number; toolCallCount: number; createdAt: Date; }> > { const sessions = await ChatSession.find({ user: userId, project: projectId, }) .sort({ lastMessageAt: -1, createdAt: -1 }) .lean(); return sessions.map((s) => ({ _id: s._id.toString(), name: s.name, lastMessageAt: s.lastMessageAt ?? null, turnCount: s.stats.turnCount, toolCallCount: s.stats.toolCallCount, createdAt: s.createdAt, })); } } export default new SessionService();