fix sso core folder
This commit is contained in:
185
apps/sso-core/src/domain/pin.service.ts
Normal file
185
apps/sso-core/src/domain/pin.service.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { BadRequestException, 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 { SessionService } from './session.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
@Injectable()
|
||||
export class PinService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly settings: SettingsService,
|
||||
private readonly session: SessionService
|
||||
) {}
|
||||
|
||||
private async getPinLengthBounds() {
|
||||
const min = await this.settings.getNumber('PIN_MIN_LENGTH', 4);
|
||||
const max = await this.settings.getNumber('PIN_MAX_LENGTH', 6);
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
private async assertValidPinFormat(pin: string) {
|
||||
const { min, max } = await this.getPinLengthBounds();
|
||||
const pattern = new RegExp(`^\\d{${min},${max}}$`);
|
||||
if (!pattern.test(pin)) {
|
||||
throw new BadRequestException(`PIN-код должен содержать от ${min} до ${max} цифр`);
|
||||
}
|
||||
}
|
||||
|
||||
async enablePin(userId: string, pin: string) {
|
||||
await this.assertValidPinFormat(pin);
|
||||
|
||||
return this.prisma.pinCode.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
hash: await bcrypt.hash(pin, 12),
|
||||
isEnabled: true,
|
||||
deletionRequestedAt: null
|
||||
},
|
||||
update: {
|
||||
hash: await bcrypt.hash(pin, 12),
|
||||
isEnabled: true,
|
||||
deletionRequestedAt: null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setupPin(userId: string, pin: string) {
|
||||
const pinCode = await this.enablePin(userId, pin);
|
||||
return { userId: pinCode.userId, isEnabled: pinCode.isEnabled, deletionRequestedAt: pinCode.deletionRequestedAt?.toISOString() };
|
||||
}
|
||||
|
||||
async updatePin(userId: string, pin: string) {
|
||||
const pinCode = await this.enablePin(userId, pin);
|
||||
await this.prisma.session.updateMany({
|
||||
where: { userId, status: SessionStatus.ACTIVE },
|
||||
data: { pinVerified: false, status: SessionStatus.LOCKED }
|
||||
});
|
||||
return { userId: pinCode.userId, isEnabled: pinCode.isEnabled, deletionRequestedAt: pinCode.deletionRequestedAt?.toISOString() };
|
||||
}
|
||||
|
||||
async requestPinDeletion(userId: string, pin?: string) {
|
||||
const pinCode = await this.prisma.pinCode.findUnique({ where: { userId } });
|
||||
if (!pinCode?.isEnabled) {
|
||||
throw new BadRequestException('PIN-код не включен');
|
||||
}
|
||||
|
||||
const requirePin = await this.settings.getBoolean('PIN_REQUIRE_ON_DELETE', true);
|
||||
if (requirePin) {
|
||||
if (!pin) {
|
||||
throw new BadRequestException('Для удаления PIN-кода требуется подтверждение текущим PIN');
|
||||
}
|
||||
const matches = await bcrypt.compare(pin, pinCode.hash);
|
||||
if (!matches) {
|
||||
throw new UnauthorizedException('Неверный PIN-код');
|
||||
}
|
||||
}
|
||||
|
||||
const graceMinutes = await this.settings.getNumber('PIN_DELETE_GRACE_MINUTES', 1440);
|
||||
const deletionRequestedAt = new Date();
|
||||
const effectiveAt = new Date(deletionRequestedAt.getTime() + graceMinutes * 60_000);
|
||||
|
||||
await this.prisma.pinCode.update({
|
||||
where: { userId },
|
||||
data: { deletionRequestedAt }
|
||||
});
|
||||
|
||||
return {
|
||||
userId,
|
||||
deletionRequestedAt: deletionRequestedAt.toISOString(),
|
||||
effectiveAt: effectiveAt.toISOString(),
|
||||
graceMinutes
|
||||
};
|
||||
}
|
||||
|
||||
async cancelPinDeletion(userId: string) {
|
||||
const pinCode = await this.prisma.pinCode.findUnique({ where: { userId } });
|
||||
if (!pinCode) {
|
||||
throw new BadRequestException('PIN-код не найден');
|
||||
}
|
||||
await this.prisma.pinCode.update({
|
||||
where: { userId },
|
||||
data: { deletionRequestedAt: null }
|
||||
});
|
||||
return { userId, cancelled: true };
|
||||
}
|
||||
|
||||
async finalizeDuePinDeletions() {
|
||||
const graceMinutes = await this.settings.getNumber('PIN_DELETE_GRACE_MINUTES', 1440);
|
||||
const threshold = new Date(Date.now() - graceMinutes * 60_000);
|
||||
const due = await this.prisma.pinCode.findMany({
|
||||
where: {
|
||||
isEnabled: true,
|
||||
deletionRequestedAt: { lte: threshold }
|
||||
}
|
||||
});
|
||||
|
||||
for (const pinCode of due) {
|
||||
await this.disablePin(pinCode.userId, { skipGraceCheck: true });
|
||||
}
|
||||
|
||||
return { count: due.length };
|
||||
}
|
||||
|
||||
async disablePin(userId: string, options?: { skipGraceCheck?: boolean }) {
|
||||
const pinCode = await this.prisma.pinCode.findUnique({ where: { userId } });
|
||||
if (!pinCode?.isEnabled) {
|
||||
throw new BadRequestException('PIN-код не включен');
|
||||
}
|
||||
|
||||
if (!options?.skipGraceCheck && pinCode.deletionRequestedAt) {
|
||||
const graceMinutes = await this.settings.getNumber('PIN_DELETE_GRACE_MINUTES', 1440);
|
||||
const effectiveAt = pinCode.deletionRequestedAt.getTime() + graceMinutes * 60_000;
|
||||
if (Date.now() < effectiveAt) {
|
||||
throw new BadRequestException('PIN-код будет удалён после истечения периода ожидания');
|
||||
}
|
||||
}
|
||||
|
||||
return this.prisma.pinCode.update({
|
||||
where: { userId },
|
||||
data: { isEnabled: false, deletionRequestedAt: null }
|
||||
});
|
||||
}
|
||||
|
||||
async verifyPin(sessionId: string, pin: string) {
|
||||
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()) {
|
||||
throw new UnauthorizedException('Сессия недействительна');
|
||||
}
|
||||
|
||||
if (!session.user.pinCode?.isEnabled) {
|
||||
throw new BadRequestException('PIN-код не включен');
|
||||
}
|
||||
|
||||
const matches = await bcrypt.compare(pin, session.user.pinCode.hash);
|
||||
if (!matches) {
|
||||
throw new UnauthorizedException('Неверный PIN-код');
|
||||
}
|
||||
|
||||
await this.prisma.session.update({
|
||||
where: { id: sessionId },
|
||||
data: {
|
||||
pinVerified: true,
|
||||
status: SessionStatus.ACTIVE,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: await this.auth.issueAccessToken(session.user, session.id, true),
|
||||
pinVerified: true
|
||||
};
|
||||
}
|
||||
|
||||
async lockExpiredPinSessions() {
|
||||
return this.session.lockExpiredPinSessions();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user