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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<InputStreamResource> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ErrorResponse> handleStatus(ResponseStatusException ex) {
|
||||
@ExceptionHandler(DomainException.class)
|
||||
public ResponseEntity<ErrorResponse> 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<ErrorResponse> 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) {}
|
||||
}
|
||||
@@ -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<AppUser> 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<Void> deleteUser(@PathVariable UUID id) {
|
||||
userService.deleteUser(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<AppUser> getAllUsers() {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
54
frontend/src/lib/errors.ts
Normal file
54
frontend/src/lib/errors.ts
Normal file
@@ -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<BackendError | null> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user