Files
IdP/apps/sso-core/src/domain/session.service.ts
2026-06-25 23:48:57 +03:00

174 lines
5.0 KiB
TypeScript

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<SessionState | null> {
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
}
});
}
}