Files
ApiCore/ApiCore-Backend/src/auth/reauth/reauth.service.ts
T
Nick SzaunigandClaude Opus 5.5 7a1480138d fix: T4 browser-check findings, interval on season state, T6 plan refresh
- Honey batch create omits an empty batch number (was 400); product form
  labels the legacy total weight, detail shows jar weight and shelf life;
  bottling hints next to their fields, values cleared on product change,
  post-save water-content wording; withdraw reason placeholder not
  selectable; honey batch actions offline-disabled with reason, 44 px.
- Offline signal: one slow request no longer flips the app offline; two
  consecutive timeouts do, any HTTP answer clears it, aborts never count.
- Phenology sheet as a side card on desktop with focus return; confirm
  dialogs as alertdialog; step-up texts in du-form ("Das Passwort stimmt
  nicht."); backup import gives its disabled reason; missing event labels
  plus a guard test; profile buttons 44 px.
- GET /phenology/state returns intervalDays via the shared
  inspectionIntervalOf (autopilot, digest, season state use one source).
- T6 plan aligned with the code after T4, plus owner decisions of
  2026-10-15 (Berlin day for restrictions, COMPLIANCE category, override
  without step-up).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-15 12:47:08 +02:00

62 lines
2.0 KiB
TypeScript

import { Injectable, UnauthorizedException } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { PrismaService } from '../../prisma/prisma.service';
import { AccountGuardService } from '../mfa/account-guard.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 accountGuard: AccountGuardService,
) {}
async verify(userId: string, proof: ReauthProof): Promise<void> {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
passwordHash: true,
mfaEnabled: true,
mfaSecret: true,
mfaLastTimeStep: true,
authLockedUntil: true,
isActive: true,
},
});
if (!user || !user.isActive || this.accountGuard.isLocked(user)) {
throw new UnauthorizedException(
'Zu viele Fehlversuche. Bestätigen ist für 15 Minuten gesperrt.',
);
}
const passwordMatches =
typeof proof.password === 'string' &&
(await bcrypt.compare(proof.password, user.passwordHash));
if (!passwordMatches) {
await this.accountGuard.recordFailure(user.id);
throw new UnauthorizedException('Das Passwort stimmt nicht.');
}
if (user.mfaEnabled) {
const codeMatches =
typeof proof.mfaCode === 'string' &&
(await this.accountGuard.consumeTotp(user, proof.mfaCode));
if (!codeMatches) {
await this.accountGuard.recordFailure(user.id);
throw new UnauthorizedException('Der Code stimmt nicht.');
}
}
await this.accountGuard.recordSuccess(user.id);
}
}