fix sso core folder
This commit is contained in:
164
apps/sso-core/src/domain/session.service.ts
Normal file
164
apps/sso-core/src/domain/session.service.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user