fix: kritische Rechte-Luecke, Admin-Lockout-Schutz und Wetter-Haertung

Dispatchte security-reviewer und code-reviewer Agenten fuer eine
vollstaendige Pruefung des RBAC-Systems (d31aa79..HEAD). Ein
KRITISCHER Fund, zwei HIGH, ein weiteres MEDIUM, alle behoben:

KRITISCH: Deaktivieren eines Benutzers hat dessen Berechtigungen nicht
entzogen. PermissionResolverService.loadPermissions() prüfte nur den
Rollen-Status, nie User.isActive - ein bereits ausgestelltes Access-
Token behielt volle Rechte bis zum Ablauf (bis zu 15 Minuten), nicht
die vorgesehenen 30 Sekunden. UsersService rief zudem nie
invalidateUser() auf. Beides jetzt behoben; neue dedizierte
permission-resolver.service.spec.ts deckt das ab (existierte bisher
gar nicht - ein Grund, warum die Luecke unbemerkt blieb).

HIGH: Kein Schutz gegen den letzten aktiven Systemrollen-Nutzer
(ADMIN). Man konnte den letzten Admin deaktivieren oder umrollen und
sich damit komplett aussperren - ohne Selbsthilfe-Weg, da
prisma/seed.ts das Seeden ueberspringt, sobald die Admin-E-Mail
bereits existiert. UsersService.deactivate()/.update() pruefen jetzt
per assertNotLastSystemRoleUser(), analog zum bestehenden
Lockout-Schutz in RolesService fuer Systemrollen selbst.

HIGH: Der Open-Meteo-Aufruf in WeatherService hatte kein Timeout und
konnte unbegrenzt haengen. Jetzt mit AbortSignal.timeout(8000) plus
sauberem Fehler-Mapping.

MEDIUM: Trachtprognose fiel bei fehlenden Wetterdaten stillschweigend
auf "MAESSIG" zurueck statt einen eigenen Unbekannt-Zustand zu zeigen
- ein Imker haette eine echte Einschaetzung gesehen, wo keine moeglich
war. Neuer Rating-Wert UNBEKANNT in Backend und Frontend.

LOW: Veralteter Kommentar an PermissionsGuard, der noch den in Phase 3
geloeschten RolesGuard erwaehnte, korrigiert.

Verifiziert: tsc, lint, build auf beiden Projekten gruen. Backend-Unit
245/245 (17 neue Tests fuer die Fixes). Frontend-Unit 14/14. Volle
Backend-e2e-Suite konnte weiterhin nicht laufen (WSL/Postgres von
Windows aus nicht erreichbar, auch nach vollem "wsl --shutdown"-
Neustart) - jeder Fix hat aber gezielte, gruene Unit-Abdeckung.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-08-26 14:03:32 +02:00
1 parent f1d6b2956e
commit 5a1015c4a0
9 files changed
+329 -11

No files matched your search

@@ -101,6 +101,20 @@ describe('WeatherService', () => {
weatherService.getWeather('apiary-1'),
).rejects.toBeInstanceOf(BadRequestException);
});
it('throws BadRequestException instead of hanging when the request times out or the network fails', async () => {
prisma.apiary.findUnique.mockResolvedValue({
latitude: 52.5,
longitude: 13.4,
});
fetchMock.mockRejectedValue(
new DOMException('The operation was aborted', 'TimeoutError'),
);
await expect(
weatherService.getWeather('apiary-1'),
).rejects.toBeInstanceOf(BadRequestException);
});
});
describe('getTrachtprognose', () => {
@@ -179,6 +193,22 @@ describe('WeatherService', () => {
expect(result.season).toBe('Frühtracht');
});
it('returns UNBEKANNT during bloom season when there is no usable temperature data', async () => {
mockToday('2026-06-10');
prisma.apiary.findUnique.mockResolvedValue({
latitude: 52.5,
longitude: 13.4,
});
fetchMock.mockResolvedValue(
new Response(JSON.stringify(openMeteoResponse([])), { status: 200 }),
);
const result = await weatherService.getTrachtprognose('apiary-1');
expect(result.rating).toBe('UNBEKANNT');
expect(result.season).toBe('Sommertracht');
});
it('includes the historical average yield when past harvests exist for the same month', async () => {
mockToday('2026-06-10');
prisma.apiary.findUnique.mockResolvedValue({
+12 -3
View File
@@ -9,6 +9,7 @@ const OPEN_METEO_FORECAST_URL = 'https://api.open-meteo.com/v1/forecast';
const PAST_DAYS = 14;
const FORECAST_DAYS = 7;
const RECENT_WINDOW_DAYS = 7;
const FETCH_TIMEOUT_MS = 8_000;
const GOOD_TEMP_THRESHOLD_C = 18;
const GOOD_PRECIPITATION_THRESHOLD_MM = 15;
const POOR_TEMP_THRESHOLD_C = 12;
@@ -26,7 +27,7 @@ export interface ApiaryWeather {
}
export type TrachtprognoseRating =
'GUT' | 'MAESSIG' | 'SCHWACH' | 'KEINE_TRACHT';
'GUT' | 'MAESSIG' | 'SCHWACH' | 'KEINE_TRACHT' | 'UNBEKANNT';
export interface Trachtprognose {
rating: TrachtprognoseRating;
@@ -100,7 +101,14 @@ export class WeatherService {
url.searchParams.set('forecast_days', String(FORECAST_DAYS));
url.searchParams.set('timezone', 'auto');
const response = await fetch(url);
let response: Response;
try {
response = await fetch(url, {
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});
} catch {
throw new BadRequestException('Wetterdaten konnten nicht geladen werden');
}
if (!response.ok) {
throw new BadRequestException('Wetterdaten konnten nicht geladen werden');
}
@@ -178,7 +186,7 @@ export class WeatherService {
0,
);
let rating: TrachtprognoseRating = 'MAESSIG';
let rating: TrachtprognoseRating = 'UNBEKANNT';
if (avgTempMax === null) {
reasoning.push('Keine ausreichenden Wetterdaten für eine Einschätzung.');
} else if (
@@ -198,6 +206,7 @@ export class WeatherService {
`Kühles oder nasses Wetter der letzten 7 Tage (Ø ${avgTempMax.toFixed(1)}°C, ${totalPrecipitation.toFixed(0)} mm Niederschlag) schränkt Bienenflug ein.`,
);
} else {
rating = 'MAESSIG';
reasoning.push(
`Durchwachsenes Wetter der letzten 7 Tage (Ø ${avgTempMax.toFixed(1)}°C, ${totalPrecipitation.toFixed(0)} mm Niederschlag).`,
);