import { Injectable, UnauthorizedException } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import { PrismaService } from '../../prisma/prisma.service'; import { MfaService } from '../mfa/mfa.service'; export interface ReauthProof { password?: string; mfaCode?: string; } /** * Step-up check for actions that are as powerful as the account itself * (restoring a backup overwrites every user): a stolen access token alone * must not be enough, so the caller proves they know the password - and, * with MFA on, holds the second factor - right now. */ @Injectable() export class ReauthService { constructor( private readonly prisma: PrismaService, private readonly mfaService: MfaService, ) {} async verify(userId: string, proof: ReauthProof): Promise { const user = await this.prisma.user.findUnique({ where: { id: userId }, select: { email: true, passwordHash: true, mfaEnabled: true, mfaSecret: true, isActive: true, }, }); const passwordMatches = Boolean(user?.isActive) && typeof proof.password === 'string' && (await bcrypt.compare(proof.password, user!.passwordHash)); if (!user || !passwordMatches) { throw new UnauthorizedException( 'Bitte zur Bestätigung Ihr aktuelles Passwort eingeben', ); } if (user.mfaEnabled) { const codeMatches = typeof proof.mfaCode === 'string' && user.mfaSecret !== null && (await this.mfaService.verifyCode( user.mfaSecret, user.email, proof.mfaCode, )); if (!codeMatches) { throw new UnauthorizedException( 'Bitte zur Bestätigung den aktuellen Code aus Ihrer Authenticator-App eingeben', ); } } } }