mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import { and, eq, lte, ne } from "drizzle-orm";
|
|
import { sessions } from "../db/schema.js";
|
|
import type { DatabaseContext } from "./database-context.js";
|
|
import { rowsAffected } from "./mutation-result.js";
|
|
import { insertReturning } from "./returning.js";
|
|
|
|
export type SessionRecord = typeof sessions.$inferSelect;
|
|
export type NewSessionRecord = typeof sessions.$inferInsert;
|
|
|
|
const SESSION_ACTIVITY_PERSIST_INTERVAL_MS = 60_000;
|
|
|
|
export class SessionRepository {
|
|
constructor(
|
|
private readonly context: DatabaseContext,
|
|
private readonly onWrite?: () => void | Promise<void>,
|
|
private readonly onActivity:
|
|
(() => void | Promise<void>) | undefined = onWrite,
|
|
) {}
|
|
|
|
async create(session: NewSessionRecord): Promise<SessionRecord> {
|
|
const rows = await insertReturning(this.context, sessions, session);
|
|
await this.afterWrite();
|
|
return rows[0];
|
|
}
|
|
|
|
async listAll(): Promise<SessionRecord[]> {
|
|
return this.context.drizzle.select().from(sessions);
|
|
}
|
|
|
|
async findById(id: string): Promise<SessionRecord | null> {
|
|
const rows = await this.context.drizzle
|
|
.select()
|
|
.from(sessions)
|
|
.where(eq(sessions.id, id))
|
|
.limit(1);
|
|
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
async listByUserId(userId: string): Promise<SessionRecord[]> {
|
|
return this.context.drizzle
|
|
.select()
|
|
.from(sessions)
|
|
.where(eq(sessions.userId, userId));
|
|
}
|
|
|
|
async listExpired(now = new Date()): Promise<SessionRecord[]> {
|
|
return this.context.drizzle
|
|
.select()
|
|
.from(sessions)
|
|
.where(lte(sessions.expiresAt, now.toISOString()));
|
|
}
|
|
|
|
async touch(
|
|
id: string,
|
|
lastActiveAt = new Date().toISOString(),
|
|
minIntervalMs = SESSION_ACTIVITY_PERSIST_INTERVAL_MS,
|
|
): Promise<boolean> {
|
|
const cutoff = new Date(
|
|
new Date(lastActiveAt).getTime() - minIntervalMs,
|
|
).toISOString();
|
|
const result = await this.context.drizzle
|
|
.update(sessions)
|
|
.set({ lastActiveAt })
|
|
.where(and(eq(sessions.id, id), lte(sessions.lastActiveAt, cutoff)));
|
|
|
|
if (rowsAffected(result) === 0) return false;
|
|
await this.onActivity?.();
|
|
return true;
|
|
}
|
|
|
|
async updateToken(
|
|
id: string,
|
|
jwtToken: string,
|
|
lastActiveAt = new Date().toISOString(),
|
|
): Promise<void> {
|
|
await this.context.drizzle
|
|
.update(sessions)
|
|
.set({ jwtToken, lastActiveAt })
|
|
.where(eq(sessions.id, id));
|
|
await this.afterWrite();
|
|
}
|
|
|
|
async revoke(id: string): Promise<boolean> {
|
|
const result = await this.context.drizzle
|
|
.delete(sessions)
|
|
.where(eq(sessions.id, id));
|
|
|
|
await this.afterWrite();
|
|
return rowsAffected(result) > 0;
|
|
}
|
|
|
|
async revokeAllForUser(
|
|
userId: string,
|
|
exceptSessionId?: string,
|
|
): Promise<number> {
|
|
const where = exceptSessionId
|
|
? and(eq(sessions.userId, userId), ne(sessions.id, exceptSessionId))
|
|
: eq(sessions.userId, userId);
|
|
|
|
const result = await this.context.drizzle.delete(sessions).where(where);
|
|
|
|
await this.afterWrite();
|
|
return rowsAffected(result);
|
|
}
|
|
|
|
async deleteExpired(now = new Date()): Promise<number> {
|
|
const result = await this.context.drizzle
|
|
.delete(sessions)
|
|
.where(lte(sessions.expiresAt, now.toISOString()));
|
|
|
|
await this.afterWrite();
|
|
return rowsAffected(result);
|
|
}
|
|
|
|
private async afterWrite(): Promise<void> {
|
|
await this.onWrite?.();
|
|
}
|
|
}
|