import { Injectable, UnauthorizedException } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import { SessionStatus } from '../generated/prisma/client'; import { PrismaService } from '../infra/prisma.service'; import { AuthService } from './auth.service'; import { SettingsService } from './settings.service'; import { isProtectedSystemAccount } from './system-account.util'; export interface SessionState { id: string; userId: string; pinVerified: boolean; status: SessionStatus; requiresPin: boolean; } @Injectable() export class SessionService { constructor( private readonly prisma: PrismaService, private readonly settings: SettingsService, private readonly auth: AuthService ) {} async resolveSessionState(sessionId: string): Promise { const session = await this.prisma.session.findUnique({ where: { id: sessionId }, include: { user: { include: { pinCode: true } } } }); if (!session || session.status === SessionStatus.REVOKED || session.expiresAt < new Date()) { return null; } if (isProtectedSystemAccount(session.user)) { await this.prisma.session.update({ where: { id: sessionId }, data: { status: SessionStatus.REVOKED } }); return null; } const pinEnabled = Boolean(session.user.pinCode?.isEnabled); let pinVerified = session.pinVerified; let status = session.status; if (pinEnabled && pinVerified && status === SessionStatus.ACTIVE) { const timeoutMinutes = await this.settings.getNumber('PIN_LOCK_TIMEOUT_MINUTES', 15); const threshold = new Date(Date.now() - timeoutMinutes * 60_000); if (session.updatedAt < threshold) { await this.prisma.session.update({ where: { id: sessionId }, data: { pinVerified: false, status: SessionStatus.LOCKED } }); pinVerified = false; status = SessionStatus.LOCKED; } } const requiresPin = pinEnabled && (!pinVerified || status === SessionStatus.LOCKED); return { id: session.id, userId: session.userId, pinVerified, status, requiresPin }; } async touchSessionActivity(sessionId: string) { const state = await this.resolveSessionState(sessionId); if (!state || state.requiresPin) { return state; } await this.prisma.session.update({ where: { id: sessionId }, data: { updatedAt: new Date() } }); return state; } async validateSession(userId: string, sessionId: string, touchActivity: boolean) { const state = touchActivity ? await this.touchSessionActivity(sessionId) : await this.resolveSessionState(sessionId); if (!state || state.userId !== userId) { throw new UnauthorizedException('Сессия недействительна'); } return { requiresPin: state.requiresPin, sessionId: state.id, pinVerified: state.pinVerified }; } async refreshSession(refreshToken: string, sessionId?: string) { const sessions = await this.prisma.session.findMany({ where: { ...(sessionId ? { id: sessionId } : {}), status: { not: SessionStatus.REVOKED }, expiresAt: { gt: new Date() } }, include: { user: { include: { pinCode: true } } }, orderBy: { updatedAt: 'desc' }, take: sessionId ? 1 : 50 }); let matchedSession: (typeof sessions)[number] | null = null; for (const session of sessions) { const matches = await bcrypt.compare(refreshToken, session.refreshHash); if (matches) { matchedSession = session; break; } } if (!matchedSession) { throw new UnauthorizedException('Refresh token недействителен'); } const state = await this.resolveSessionState(matchedSession.id); if (!state) { throw new UnauthorizedException('Сессия недействительна'); } if (state.requiresPin) { const user = await this.auth.getMe(matchedSession.userId); return { requiresPin: true, sessionId: state.id, pinVerified: false, user }; } await this.prisma.session.update({ where: { id: matchedSession.id }, data: { updatedAt: new Date() } }); const user = await this.auth.getMe(matchedSession.userId); return { requiresPin: false, accessToken: await this.auth.issueAccessToken(matchedSession.user, matchedSession.id, true), sessionId: matchedSession.id, pinVerified: true, user }; } async lockExpiredPinSessions() { const timeoutMinutes = await this.settings.getNumber('PIN_LOCK_TIMEOUT_MINUTES', 15); const threshold = new Date(Date.now() - timeoutMinutes * 60_000); return this.prisma.session.updateMany({ where: { pinVerified: true, updatedAt: { lt: threshold }, status: SessionStatus.ACTIVE, user: { pinCode: { isEnabled: true } } }, data: { pinVerified: false, status: SessionStatus.LOCKED } }); } }