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:
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({
|
||||
|
||||
@@ -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).`,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user