gadget/docs/archive/services/session.ts

202 lines
5.2 KiB
TypeScript

// 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<void> {
this.log.info("service started");
// Start session cleanup cron job
this.startCleanupJob();
}
async stop(): Promise<void> {
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<ISession> {
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<ISession | null> {
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<ISession | null> {
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<boolean> {
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<number> {
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<ISession> {
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();