feat: Security
This commit is contained in:
110
server/src/services/session-auth.service.ts
Normal file
110
server/src/services/session-auth.service.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
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 = 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);
|
||||
}
|
||||
Reference in New Issue
Block a user