111 lines
2.6 KiB
TypeScript
111 lines
2.6 KiB
TypeScript
import { randomBytes } from 'crypto';
|
|
import { getDataSource } from '../db/database';
|
|
import { SessionTokenEntity } from '../entities/SessionTokenEntity';
|
|
import { getUserById } from '../cqrs';
|
|
import type { AuthUserPayload } from '../cqrs/types';
|
|
|
|
const DEFAULT_TOKEN_TTL_MS = 10 * 365 * 24 * 60 * 60 * 1000;
|
|
|
|
export interface IssuedSessionToken {
|
|
token: string;
|
|
userId: string;
|
|
issuedAt: number;
|
|
expiresAt: number;
|
|
}
|
|
|
|
export interface AuthenticatedSession {
|
|
token: string;
|
|
user: AuthUserPayload;
|
|
issuedAt: number;
|
|
expiresAt: number;
|
|
}
|
|
|
|
function getTokenRepository() {
|
|
return getDataSource().getRepository(SessionTokenEntity);
|
|
}
|
|
|
|
export function getSessionTokenTtlMs(): number {
|
|
const configured = Number(process.env.SESSION_TOKEN_TTL_MS);
|
|
|
|
return Number.isFinite(configured) && configured > 0
|
|
? configured
|
|
: DEFAULT_TOKEN_TTL_MS;
|
|
}
|
|
|
|
export async function issueSessionToken(userId: string): Promise<IssuedSessionToken> {
|
|
const token = randomBytes(32).toString('hex');
|
|
const issuedAt = Date.now();
|
|
const expiresAt = issuedAt + getSessionTokenTtlMs();
|
|
const repo = getTokenRepository();
|
|
|
|
await repo.save(repo.create({
|
|
token,
|
|
userId,
|
|
issuedAt,
|
|
expiresAt
|
|
}));
|
|
|
|
return { token, userId, issuedAt, expiresAt };
|
|
}
|
|
|
|
export async function consumeSessionToken(token: string): Promise<AuthenticatedSession | null> {
|
|
const normalized = token.trim();
|
|
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
|
|
const repo = getTokenRepository();
|
|
const record = await repo.findOne({ where: { token: normalized } });
|
|
|
|
if (!record || record.expiresAt < Date.now()) {
|
|
if (record) {
|
|
await repo.delete({ token: normalized });
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const user = await getUserById(record.userId);
|
|
|
|
if (!user) {
|
|
await repo.delete({ token: normalized });
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
token: record.token,
|
|
user,
|
|
issuedAt: record.issuedAt,
|
|
expiresAt: record.expiresAt
|
|
};
|
|
}
|
|
|
|
export async function revokeSessionToken(token: string): Promise<void> {
|
|
const normalized = token.trim();
|
|
|
|
if (!normalized) {
|
|
return;
|
|
}
|
|
|
|
await getTokenRepository().delete({ token: normalized });
|
|
}
|
|
|
|
export async function revokeAllSessionTokensForUser(userId: string): Promise<void> {
|
|
await getTokenRepository().delete({ userId });
|
|
}
|
|
|
|
export async function pruneExpiredSessionTokens(): Promise<void> {
|
|
const repo = getTokenRepository();
|
|
const now = Date.now();
|
|
const expired = await repo.createQueryBuilder('token')
|
|
.where('token.expiresAt < :now', { now })
|
|
.getMany();
|
|
|
|
if (expired.length === 0) {
|
|
return;
|
|
}
|
|
|
|
await repo.remove(expired);
|
|
}
|