From 4cc86de1435f5a5b84e0b0be9a305a3f28be3335 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 15 Mar 2026 13:15:28 +0100 Subject: [PATCH] feat: replace raw error messages with structured error codes Backend now returns { code: ErrorCode, message: string } for all errors, making it language-agnostic. Frontend maps codes to localised strings via Paraglide (en/de/es), so translations live in messages/*.json. - Add ErrorCode enum and DomainException with static factory methods - Update GlobalExceptionHandler to return ErrorResponse(code, message) - Replace ResponseStatusException throughout controllers/services/aspects - Add frontend errors.ts with parseBackendError() and getErrorMessage() - getErrorMessage() delegates to Paraglide m.error_*() functions - Add error_* keys to messages/en.json, de.json, es.json - Update all page.server.ts files to use the new error utilities - Fix hardcoded localhost URLs in admin and login pages - Fix missing baseUrl in deleteTag action Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 14 ++--- .../controller/GlobalExceptionHandler.java | 17 +++--- .../controller/UserController.java | 19 ++----- .../exception/DomainException.java | 49 +++++++++++++++++ .../familienarchiv/exception/ErrorCode.java | 40 ++++++++++++++ .../security/PermissionAspect.java | 7 ++- .../service/MassImportService.java | 5 ++ .../familienarchiv/service/UserService.java | 6 ++- frontend/messages/de.json | 12 ++++- frontend/messages/en.json | 12 ++++- frontend/messages/es.json | 12 ++++- frontend/src/lib/errors.ts | 54 +++++++++++++++++++ frontend/src/routes/admin/+page.server.ts | 39 ++++++++++---- .../src/routes/documents/[id]/+page.server.ts | 27 +++------- .../documents/[id]/edit/+page.server.ts | 29 +++++----- frontend/src/routes/login/+page.server.ts | 15 +++--- 16 files changed, 269 insertions(+), 88 deletions(-) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/exception/DomainException.java create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java create mode 100644 frontend/src/lib/errors.ts diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index fe9f2879..3da987d8 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.UUID; import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.repository.DocumentRepository; import org.raddatz.familienarchiv.security.Permission; @@ -15,7 +17,6 @@ import org.raddatz.familienarchiv.service.FileService; import org.springframework.core.io.InputStreamResource; import org.springframework.data.domain.Sort; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -27,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.server.ResponseStatusException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -47,10 +47,10 @@ public class DocumentController { public ResponseEntity getDocumentFile(@PathVariable UUID id) { // 1. Look up path in DB Document doc = documentRepository.findById(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + .orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id)); if (doc.getFilePath() == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No file attached"); + throw DomainException.notFound(ErrorCode.DOCUMENT_NO_FILE, "Document has no file attached: " + id); } // 2. Delegate Retrieval to FileService @@ -62,7 +62,7 @@ public class DocumentController { .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + doc.getOriginalFilename() + "\"") .body(download.resource()); } catch (FileService.StorageFileNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "File missing in storage"); + throw DomainException.notFound(ErrorCode.FILE_NOT_FOUND, "File missing in storage: " + doc.getFilePath()); } } @@ -70,7 +70,7 @@ public class DocumentController { @GetMapping("/{id}") public Document getDocument(@PathVariable UUID id) { return documentRepository.findById(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + .orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id)); } @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -82,7 +82,7 @@ public class DocumentController { try { return documentService.updateDocument(id, dto, file); } catch (IOException e) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Fehler beim Upload"); + throw DomainException.internal(ErrorCode.FILE_UPLOAD_FAILED, "Failed to upload file: " + e.getMessage()); } } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/GlobalExceptionHandler.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/GlobalExceptionHandler.java index 3f438b05..ece8292a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/GlobalExceptionHandler.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/GlobalExceptionHandler.java @@ -2,11 +2,12 @@ package org.raddatz.familienarchiv.controller; import java.util.stream.Collectors; +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.exception.ErrorCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.server.ResponseStatusException; import lombok.extern.slf4j.Slf4j; @@ -14,11 +15,11 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class GlobalExceptionHandler { - @ExceptionHandler(ResponseStatusException.class) - public ResponseEntity handleStatus(ResponseStatusException ex) { + @ExceptionHandler(DomainException.class) + public ResponseEntity handleDomain(DomainException ex) { return ResponseEntity - .status(ex.getStatusCode()) - .body(new ErrorResponse(ex.getReason())); + .status(ex.getStatus()) + .body(new ErrorResponse(ex.getCode(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) @@ -26,15 +27,15 @@ public class GlobalExceptionHandler { String message = ex.getBindingResult().getFieldErrors().stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()) .collect(Collectors.joining(", ")); - return ResponseEntity.badRequest().body(new ErrorResponse(message)); + return ResponseEntity.badRequest().body(new ErrorResponse(ErrorCode.VALIDATION_ERROR, message)); } @ExceptionHandler(Exception.class) public ResponseEntity handleGeneric(Exception ex) { log.error("Unhandled exception", ex); return ResponseEntity.internalServerError() - .body(new ErrorResponse("Ein Fehler ist aufgetreten")); + .body(new ErrorResponse(ErrorCode.INTERNAL_ERROR, "An unexpected error occurred")); } - public record ErrorResponse(String message) {} + public record ErrorResponse(ErrorCode code, String message) {} } \ No newline at end of file diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/UserController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/UserController.java index 99503150..4ac4fa47 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/UserController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/UserController.java @@ -52,24 +52,15 @@ public class UserController { @PostMapping("/users") @RequirePermission(Permission.ADMIN_USER) - public ResponseEntity createUser(@RequestBody CreateUserRequest request) { - try { - AppUser createdUser = userService.createUserOrUpdate(request); - return ResponseEntity.ok(createdUser); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body(e.getMessage()); - } + public ResponseEntity createUser(@RequestBody CreateUserRequest request) { + return ResponseEntity.ok(userService.createUserOrUpdate(request)); } @DeleteMapping("/users/{id}") @RequirePermission(Permission.ADMIN_USER) - public ResponseEntity deleteUser(@PathVariable UUID id) { - try { - userService.deleteUser(id); - return ResponseEntity.ok().build(); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body(e.getMessage()); - } + public ResponseEntity deleteUser(@PathVariable UUID id) { + userService.deleteUser(id); + return ResponseEntity.ok().build(); } } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/exception/DomainException.java b/backend/src/main/java/org/raddatz/familienarchiv/exception/DomainException.java new file mode 100644 index 00000000..a34332ce --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/exception/DomainException.java @@ -0,0 +1,49 @@ +package org.raddatz.familienarchiv.exception; + +import org.springframework.http.HttpStatus; + +/** + * Exception for domain-level errors that should be surfaced to the API caller + * with a machine-readable ErrorCode and an English developer message. + */ +public class DomainException extends RuntimeException { + + private final ErrorCode code; + private final HttpStatus status; + + public DomainException(ErrorCode code, HttpStatus status, String developerMessage) { + super(developerMessage); + this.code = code; + this.status = status; + } + + public ErrorCode getCode() { + return code; + } + + public HttpStatus getStatus() { + return status; + } + + // --- Static factories for common cases --- + + public static DomainException notFound(ErrorCode code, String message) { + return new DomainException(code, HttpStatus.NOT_FOUND, message); + } + + public static DomainException forbidden(String message) { + return new DomainException(ErrorCode.FORBIDDEN, HttpStatus.FORBIDDEN, message); + } + + public static DomainException unauthorized(String message) { + return new DomainException(ErrorCode.UNAUTHORIZED, HttpStatus.UNAUTHORIZED, message); + } + + public static DomainException conflict(ErrorCode code, String message) { + return new DomainException(code, HttpStatus.CONFLICT, message); + } + + public static DomainException internal(ErrorCode code, String message) { + return new DomainException(code, HttpStatus.INTERNAL_SERVER_ERROR, message); + } +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java b/backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java new file mode 100644 index 00000000..f9f251d5 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java @@ -0,0 +1,40 @@ +package org.raddatz.familienarchiv.exception; + +/** + * Machine-readable error codes returned in API responses. + * The frontend uses these to display localised messages to the user. + * Every code must be documented here with its meaning and the HTTP status + * it is paired with in DomainException. + */ +public enum ErrorCode { + + // --- Documents --- + /** A document with the given ID does not exist. 404 */ + DOCUMENT_NOT_FOUND, + /** The document exists but has no file attached yet. 404 */ + DOCUMENT_NO_FILE, + /** The file referenced by the document is missing in object storage. 404 */ + FILE_NOT_FOUND, + /** An error occurred while uploading a file to object storage. 500 */ + FILE_UPLOAD_FAILED, + + // --- Users --- + /** A user with the given ID or username does not exist. 404 */ + USER_NOT_FOUND, + + // --- Import --- + /** A mass import is already in progress; only one can run at a time. 409 */ + IMPORT_ALREADY_RUNNING, + + // --- Auth --- + /** The request is not authenticated. 401 */ + UNAUTHORIZED, + /** The authenticated user lacks the required permission. 403 */ + FORBIDDEN, + + // --- Generic --- + /** Request validation failed (missing or malformed fields). 400 */ + VALIDATION_ERROR, + /** An unexpected server-side error occurred. 500 */ + INTERNAL_ERROR, +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/security/PermissionAspect.java b/backend/src/main/java/org/raddatz/familienarchiv/security/PermissionAspect.java index a2d7ab5e..4a8c17f5 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/security/PermissionAspect.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/security/PermissionAspect.java @@ -4,11 +4,10 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.http.HttpStatus; +import org.raddatz.familienarchiv.exception.DomainException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import org.springframework.web.server.ResponseStatusException; import java.lang.reflect.Method; @@ -48,14 +47,14 @@ public class PermissionAspect { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Nicht authentifiziert"); + throw DomainException.unauthorized("Not authenticated"); } boolean hasPermission = auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals(requiredPerm.name())); if (!hasPermission) { - throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Fehlende Berechtigung: " + requiredPerm); + throw DomainException.forbidden("Missing required permission: " + requiredPerm.name()); } } } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/MassImportService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/MassImportService.java index 35cdae7d..fb39d0bd 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/MassImportService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/MassImportService.java @@ -4,6 +4,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentStatus; import org.raddatz.familienarchiv.repository.DocumentRepository; @@ -63,6 +65,9 @@ public class MassImportService { @Async public void runImportAsync() { + if (currentStatus.state() == State.RUNNING) { + throw DomainException.conflict(ErrorCode.IMPORT_ALREADY_RUNNING, "A mass import is already in progress"); + } currentStatus = new ImportStatus(State.RUNNING, "Import läuft...", 0, LocalDateTime.now()); try { File excelFile = findExcelFile(); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/UserService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/UserService.java index c3fca747..4e6b1326 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/UserService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/UserService.java @@ -4,6 +4,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.raddatz.familienarchiv.dto.CreateUserRequest; +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.UserGroup; import org.raddatz.familienarchiv.repository.AppUserRepository; @@ -63,13 +65,13 @@ public AppUser createUserOrUpdate(CreateUserRequest request) { log.info("Delete user {}", userId); AppUser user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException(String.format("No User found for id %", userId))); + .orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, String.format("No user found for id %s", userId))); userRepository.delete(user); } public AppUser findByUsername(String username) { return userRepository.findByUsername(username).orElseThrow( - () -> new IllegalArgumentException(String.format("No User found for userrname %", username))); + () -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, String.format("No user found for username %s", username))); } public List getAllUsers() { diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 81075467..d184227c 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -1,4 +1,14 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "hello_world": "Hello, {name} from de!" + "hello_world": "Hello, {name} from de!", + "error_document_not_found": "Das Dokument wurde nicht gefunden.", + "error_document_no_file": "Diesem Dokument ist noch keine Datei zugeordnet.", + "error_file_not_found": "Die Datei konnte im Speicher nicht gefunden werden.", + "error_file_upload_failed": "Die Datei konnte nicht hochgeladen werden.", + "error_user_not_found": "Der Benutzer wurde nicht gefunden.", + "error_import_already_running": "Ein Import läuft bereits. Bitte warten Sie, bis dieser abgeschlossen ist.", + "error_unauthorized": "Sie sind nicht angemeldet.", + "error_forbidden": "Sie haben keine Berechtigung für diese Aktion.", + "error_validation_error": "Die Eingabe ist ungültig.", + "error_internal_error": "Ein unerwarteter Fehler ist aufgetreten." } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 37a98944..d233a202 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,4 +1,14 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "hello_world": "Hello, {name} from en!" + "hello_world": "Hello, {name} from en!", + "error_document_not_found": "Document not found.", + "error_document_no_file": "No file is associated with this document.", + "error_file_not_found": "The file could not be found in storage.", + "error_file_upload_failed": "The file could not be uploaded.", + "error_user_not_found": "User not found.", + "error_import_already_running": "An import is already running. Please wait for it to finish.", + "error_unauthorized": "You are not logged in.", + "error_forbidden": "You do not have permission for this action.", + "error_validation_error": "The input is invalid.", + "error_internal_error": "An unexpected error occurred." } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 176345c1..aa3841d9 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -1,4 +1,14 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "hello_world": "Hello, {name} from es!" + "hello_world": "Hello, {name} from es!", + "error_document_not_found": "Documento no encontrado.", + "error_document_no_file": "No hay ningún archivo asociado a este documento.", + "error_file_not_found": "El archivo no pudo encontrarse en el almacenamiento.", + "error_file_upload_failed": "No se pudo subir el archivo.", + "error_user_not_found": "Usuario no encontrado.", + "error_import_already_running": "Ya hay una importación en curso. Por favor, espere a que finalice.", + "error_unauthorized": "No ha iniciado sesión.", + "error_forbidden": "No tiene permiso para realizar esta acción.", + "error_validation_error": "La entrada no es válida.", + "error_internal_error": "Se ha producido un error inesperado." } diff --git a/frontend/src/lib/errors.ts b/frontend/src/lib/errors.ts new file mode 100644 index 00000000..de577eb5 --- /dev/null +++ b/frontend/src/lib/errors.ts @@ -0,0 +1,54 @@ +import * as m from '$lib/paraglide/messages.js'; + +/** + * Mirror of the backend ErrorCode enum. + * Keep in sync with backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java + */ +export type ErrorCode = + | 'DOCUMENT_NOT_FOUND' + | 'DOCUMENT_NO_FILE' + | 'FILE_NOT_FOUND' + | 'FILE_UPLOAD_FAILED' + | 'USER_NOT_FOUND' + | 'IMPORT_ALREADY_RUNNING' + | 'UNAUTHORIZED' + | 'FORBIDDEN' + | 'VALIDATION_ERROR' + | 'INTERNAL_ERROR'; + +export interface BackendError { + code: ErrorCode; + message: string; // English developer message — not shown to users +} + +/** + * Attempts to parse a backend ErrorResponse from a failed fetch response. + * Returns null if the body is not valid JSON or does not contain a code field. + */ +export async function parseBackendError(res: Response): Promise { + try { + const body = await res.json(); + if (body && typeof body.code === 'string') { + return body as BackendError; + } + } catch { + // Body was not JSON + } + return null; +} + +/** Returns a localised message for the given error code via Paraglide. Falls back to INTERNAL_ERROR. */ +export function getErrorMessage(code: ErrorCode | string | undefined): string { + switch (code) { + case 'DOCUMENT_NOT_FOUND': return m.error_document_not_found(); + case 'DOCUMENT_NO_FILE': return m.error_document_no_file(); + case 'FILE_NOT_FOUND': return m.error_file_not_found(); + case 'FILE_UPLOAD_FAILED': return m.error_file_upload_failed(); + case 'USER_NOT_FOUND': return m.error_user_not_found(); + case 'IMPORT_ALREADY_RUNNING':return m.error_import_already_running(); + case 'UNAUTHORIZED': return m.error_unauthorized(); + case 'FORBIDDEN': return m.error_forbidden(); + case 'VALIDATION_ERROR': return m.error_validation_error(); + default: return m.error_internal_error(); + } +} diff --git a/frontend/src/routes/admin/+page.server.ts b/frontend/src/routes/admin/+page.server.ts index 8bb060b2..8f152be8 100644 --- a/frontend/src/routes/admin/+page.server.ts +++ b/frontend/src/routes/admin/+page.server.ts @@ -1,5 +1,6 @@ -import { error } from '@sveltejs/kit'; +import { error, fail } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; +import { parseBackendError, getErrorMessage } from '$lib/errors'; export async function load({ fetch, locals }) { // 1. Check Permissions (Adapt logic to your user object) @@ -8,7 +9,7 @@ export async function load({ fetch, locals }) { // Assuming user.group.permissions is an array of strings const hasAdmin = user?.groups.some(g => g.permissions.includes("ADMIN")); - if (!hasAdmin) throw error(403, 'Zugriff verweigert'); + if (!hasAdmin) throw error(403, getErrorMessage('FORBIDDEN')); const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; @@ -50,8 +51,11 @@ export const actions = { body: JSON.stringify(payload) }); - if (!res.ok) return { success: false, message: 'Fehler beim Erstellen' }; - return { success: true, message: 'User angelegt' }; + if (!res.ok) { + const backendError = await parseBackendError(res); + return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) }); + } + return { success: true }; }, deleteUser: async ({ request, fetch }) => { const data = await request.formData(); @@ -63,9 +67,10 @@ export const actions = { }); if (!res.ok) { - return { success: false, message: 'Fehler beim Löschen des Benutzers' }; + const backendError = await parseBackendError(res); + return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) }); } - return { success: true, message: 'Benutzer erfolgreich gelöscht' }; + return { success: true }; }, updateTag: async ({ request, fetch }) => { const data = await request.formData(); @@ -84,8 +89,13 @@ export const actions = { deleteTag: async ({ request, fetch }) => { const data = await request.formData(); const id = data.get('id'); + const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; - await fetch(`/api/tags/${id}`, { method: 'DELETE' }); + const res = await fetch(baseUrl + `/api/tags/${id}`, { method: 'DELETE' }); + if (!res.ok) { + const backendError = await parseBackendError(res); + return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) }); + } return { success: true }; }, @@ -104,7 +114,10 @@ export const actions = { body: JSON.stringify(payload) }); - if (!res.ok) return { success: false, message: 'Fehler beim Erstellen der Gruppe' }; + if (!res.ok) { + const backendError = await parseBackendError(res); + return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) }); + } return { success: true }; }, @@ -124,7 +137,10 @@ export const actions = { body: JSON.stringify(payload) }); - if (!res.ok) return { success: false, message: 'Fehler beim Aktualisieren' }; + if (!res.ok) { + const backendError = await parseBackendError(res); + return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) }); + } return { success: true }; }, @@ -135,7 +151,10 @@ export const actions = { const id = data.get('id'); const res = await fetch(baseUrl + `/api/groups/${id}`, { method: 'DELETE' }); - if (!res.ok) return { success: false, message: 'Gruppe kann nicht gelöscht werden (evtl. noch Benutzer zugeordnet?)' }; + if (!res.ok) { + const backendError = await parseBackendError(res); + return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) }); + } return { success: true }; } }; diff --git a/frontend/src/routes/documents/[id]/+page.server.ts b/frontend/src/routes/documents/[id]/+page.server.ts index 34e6e4da..fcc325aa 100644 --- a/frontend/src/routes/documents/[id]/+page.server.ts +++ b/frontend/src/routes/documents/[id]/+page.server.ts @@ -1,37 +1,24 @@ import { error, redirect } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; +import { parseBackendError, getErrorMessage } from '$lib/errors'; export async function load({ params, fetch }) { const { id } = params; - - const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; try { const res = await fetch(`${baseUrl}/api/documents/${id}`); - if (res.status === 404) { - throw error(404, 'Dokument nicht gefunden'); - } - - if (res.status === 401) { - throw redirect(302, '/login'); - } + if (res.status === 401) throw redirect(302, '/login'); if (!res.ok) { - console.error(`Backend Fehler (${res.status}):`, res.statusText); - throw error(500, 'Fehler beim Laden des Dokuments'); + const backendError = await parseBackendError(res); + throw error(res.status, getErrorMessage(backendError?.code)); } - const document = await res.json(); - - return { - document - }; + return { document: await res.json() }; } catch (e) { - // Fehlerbehandlung - if (e.status) throw e; // Redirects und HttpErrors durchlassen - console.error("Ladefehler:", e); - throw error(500, 'Verbindung zum Server fehlgeschlagen'); + if (e.status) throw e; + throw error(500, getErrorMessage('INTERNAL_ERROR')); } } diff --git a/frontend/src/routes/documents/[id]/edit/+page.server.ts b/frontend/src/routes/documents/[id]/edit/+page.server.ts index 8bb81008..32fbbe4c 100644 --- a/frontend/src/routes/documents/[id]/edit/+page.server.ts +++ b/frontend/src/routes/documents/[id]/edit/+page.server.ts @@ -1,47 +1,48 @@ -import { error, redirect } from '@sveltejs/kit'; +import { error, fail, redirect } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; +import { parseBackendError, getErrorMessage } from '$lib/errors'; export async function load({ params, fetch }) { const { id } = params; - - - const baseUrl = 'http://localhost:8080'; + const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; try { - // Parallel Dokument und Personen laden const [docRes, personsRes] = await Promise.all([ fetch(`${baseUrl}/api/documents/${id}`), fetch(`${baseUrl}/api/persons`) ]); - if (!docRes.ok) throw error(docRes.status, 'Dokument nicht gefunden'); - if (!personsRes.ok) throw error(personsRes.status, 'Personen konnten nicht geladen werden'); + if (!docRes.ok) { + const backendError = await parseBackendError(docRes); + throw error(docRes.status, getErrorMessage(backendError?.code)); + } + if (!personsRes.ok) { + throw error(personsRes.status, getErrorMessage('INTERNAL_ERROR')); + } return { document: await docRes.json(), persons: await personsRes.json() }; } catch (e) { - console.error(e); - throw error(500, 'Ladefehler'); + if (e.status) throw e; + throw error(500, getErrorMessage('INTERNAL_ERROR')); } } export const actions = { default: async ({ request, params, fetch }) => { const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; - const formData = await request.formData(); - // Sende den FormData Request direkt an das Spring Backend weiter - // (Spring kann Multipart verarbeiten) const res = await fetch(`${baseUrl}/api/documents/${params.id}`, { - method: "PUT", + method: 'PUT', body: formData }); if (!res.ok) { - return { success: false, message: 'Speichern fehlgeschlagen' }; + const backendError = await parseBackendError(res); + return fail(res.status, { error: getErrorMessage(backendError?.code) }); } throw redirect(303, `/documents/${params.id}`); diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 9354a1c3..01030ab4 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -1,4 +1,6 @@ import { fail, redirect, type Actions } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import { getErrorMessage } from '$lib/errors'; export const actions = { login: async ({ request, cookies, fetch }) => { @@ -17,7 +19,8 @@ export const actions = { try { // Test-Request an das Backend (z.B. an den Upload-Endpunkt oder einen speziellen /me Endpunkt) // Wir nutzen hier http://localhost:8080, da beide Container im selben Netz sind (oder localhost im DevContainer) - const response = await fetch('http://localhost:8080/api/users/me', { + const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; + const response = await fetch(`${baseUrl}/api/users/me`, { method: 'GET', headers: { 'Authorization': authHeader @@ -25,11 +28,11 @@ export const actions = { }); if (response.status === 401 || response.status === 403) { - return fail(401, { error: 'Ungültige Zugangsdaten.' }); + return fail(401, { error: getErrorMessage('UNAUTHORIZED') }); } if (!response.ok) { - return fail(500, { error: 'Serverfehler beim Login.' }); + return fail(500, { error: getErrorMessage('INTERNAL_ERROR') }); } // Login erfolgreich! Wir speichern den Header in einem Cookie. @@ -42,9 +45,9 @@ export const actions = { maxAge: 60 * 60 * 24 // 1 Tag }); - } catch (error) { - console.error(error); - return fail(500, { error: 'Verbindung zum Backend fehlgeschlagen.' }); + } catch (e) { + console.error(e); + return fail(500, { error: getErrorMessage('INTERNAL_ERROR') }); } // Weiterleitung zur Startseite