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]>
This commit is contained in:
1 parent
9760ce154f
commit
7a1480138d
63 files changed
+1076
-251
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { ReauthService } from './reauth.service';
|
||||
|
||||
describe('ReauthService', () => {
|
||||
const baseUser = {
|
||||
id: 'u1',
|
||||
email: '[email protected]',
|
||||
passwordHash: bcrypt.hashSync('richtig', 4),
|
||||
mfaEnabled: false,
|
||||
mfaSecret: null,
|
||||
mfaLastTimeStep: null,
|
||||
authLockedUntil: null,
|
||||
isActive: true,
|
||||
};
|
||||
const accountGuard = {
|
||||
isLocked: jest.fn(),
|
||||
recordFailure: jest.fn(),
|
||||
recordSuccess: jest.fn(),
|
||||
consumeTotp: jest.fn(),
|
||||
};
|
||||
const prisma = { user: { findUnique: jest.fn() } };
|
||||
const service = new ReauthService(prisma as never, accountGuard as never);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
prisma.user.findUnique.mockResolvedValue(baseUser);
|
||||
accountGuard.isLocked.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('sagt bei falschem Passwort, dass es nicht stimmt', async () => {
|
||||
await expect(service.verify('u1', { password: 'falsch' })).rejects.toThrow(
|
||||
new UnauthorizedException('Das Passwort stimmt nicht.'),
|
||||
);
|
||||
expect(accountGuard.recordFailure).toHaveBeenCalledWith('u1');
|
||||
});
|
||||
|
||||
it('sagt bei falschem Code, dass er nicht stimmt', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue({ ...baseUser, mfaEnabled: true });
|
||||
accountGuard.consumeTotp.mockResolvedValue(false);
|
||||
await expect(
|
||||
service.verify('u1', { password: 'richtig', mfaCode: '000000' }),
|
||||
).rejects.toThrow(new UnauthorizedException('Der Code stimmt nicht.'));
|
||||
});
|
||||
|
||||
it('nennt bei gesperrtem Konto die Sperre', async () => {
|
||||
accountGuard.isLocked.mockReturnValue(true);
|
||||
await expect(service.verify('u1', { password: 'richtig' })).rejects.toThrow(
|
||||
new UnauthorizedException(
|
||||
'Zu viele Fehlversuche. Bestätigen ist für 15 Minuten gesperrt.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('lässt richtige Angaben durch', async () => {
|
||||
await expect(
|
||||
service.verify('u1', { password: 'richtig' }),
|
||||
).resolves.toBeUndefined();
|
||||
expect(accountGuard.recordSuccess).toHaveBeenCalledWith('u1');
|
||||
});
|
||||
});
|
||||
@@ -37,7 +37,7 @@ export class ReauthService {
|
||||
});
|
||||
if (!user || !user.isActive || this.accountGuard.isLocked(user)) {
|
||||
throw new UnauthorizedException(
|
||||
'Bestätigung derzeit nicht möglich. Nach mehreren Fehlversuchen ist sie für 15 Minuten gesperrt.',
|
||||
'Zu viele Fehlversuche. Bestätigen ist für 15 Minuten gesperrt.',
|
||||
);
|
||||
}
|
||||
const passwordMatches =
|
||||
@@ -45,9 +45,7 @@ export class ReauthService {
|
||||
(await bcrypt.compare(proof.password, user.passwordHash));
|
||||
if (!passwordMatches) {
|
||||
await this.accountGuard.recordFailure(user.id);
|
||||
throw new UnauthorizedException(
|
||||
'Bitte zur Bestätigung Ihr aktuelles Passwort eingeben',
|
||||
);
|
||||
throw new UnauthorizedException('Das Passwort stimmt nicht.');
|
||||
}
|
||||
if (user.mfaEnabled) {
|
||||
const codeMatches =
|
||||
@@ -55,9 +53,7 @@ export class ReauthService {
|
||||
(await this.accountGuard.consumeTotp(user, proof.mfaCode));
|
||||
if (!codeMatches) {
|
||||
await this.accountGuard.recordFailure(user.id);
|
||||
throw new UnauthorizedException(
|
||||
'Bitte zur Bestätigung den aktuellen Code aus Ihrer Authenticator-App eingeben',
|
||||
);
|
||||
throw new UnauthorizedException('Der Code stimmt nicht.');
|
||||
}
|
||||
}
|
||||
await this.accountGuard.recordSuccess(user.id);
|
||||
|
||||
Reference in new issue
Block a user