diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/GroupController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/GroupController.java index ce47d426..926d79e0 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/GroupController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/GroupController.java @@ -9,7 +9,8 @@ import org.raddatz.familienarchiv.repository.UserGroupRepository; import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.RequirePermission; import org.raddatz.familienarchiv.service.UserService; -import org.springframework.http.HttpStatus; +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.exception.ErrorCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -19,12 +20,11 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; import lombok.RequiredArgsConstructor; @RestController -@RequestMapping("/groups") +@RequestMapping("/api/groups") @RequirePermission(Permission.ADMIN_PERMISSION) @RequiredArgsConstructor public class GroupController { @@ -42,7 +42,7 @@ public class GroupController { @PatchMapping("/{id}") public ResponseEntity updateGroup(@PathVariable UUID id, @RequestBody GroupDTO dto) { UserGroup group = groupRepository.findById(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + .orElseThrow(() -> DomainException.notFound(ErrorCode.INTERNAL_ERROR, "Group not found: " + id)); if (dto.getName() != null) group.setName(dto.getName()); @@ -53,14 +53,9 @@ public class GroupController { } @DeleteMapping("/{id}") - public ResponseEntity deleteGroup(@PathVariable UUID id) { - try { - // Optional: Check if users are assigned before deleting - groupRepository.deleteById(id); - return ResponseEntity.ok().build(); - } catch (Exception e) { - return ResponseEntity.badRequest().body("Gruppe konnte nicht gelöscht werden."); - } + public ResponseEntity deleteGroup(@PathVariable UUID id) { + groupRepository.deleteById(id); + return ResponseEntity.ok().build(); } @GetMapping("") diff --git a/frontend/.gitignore b/frontend/.gitignore index a3537d4e..9f121668 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -26,4 +26,5 @@ vite.config.ts.timestamp-* src/lib/paraglide # Generated OpenAPI types — regenerate with: npm run generate:api -src/lib/generated/api.ts +# (committed as a stub; overwritten by the real spec after generation) +# src/lib/generated/api.ts diff --git a/frontend/src/lib/generated/api.ts b/frontend/src/lib/generated/api.ts new file mode 100644 index 00000000..1070325f --- /dev/null +++ b/frontend/src/lib/generated/api.ts @@ -0,0 +1,15 @@ +/** + * STUB — generated by openapi-typescript + * + * Run `npm run generate:api` with the backend running in dev mode to + * replace this file with fully-typed definitions. + * + * While this stub is in place the typed client still works; TypeScript + * will start enforcing path and parameter correctness once the real + * types are generated. + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type paths = Record; +export type components = Record; +export type operations = Record; diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index c9d76588..2281ef02 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -1,72 +1,58 @@ import { redirect } from '@sveltejs/kit'; -import type { PageServerLoad } from './$types'; +import { createApiClient } from '$lib/api.server'; -export const load: PageServerLoad = async ({ url, fetch }) => { - - - // 1. Extract params +export async function load({ url, fetch }) { const q = url.searchParams.get('q') || ''; const from = url.searchParams.get('from') || ''; const to = url.searchParams.get('to') || ''; const senderId = url.searchParams.get('senderId') || ''; const receiverId = url.searchParams.get('receiverId') || ''; - const tags = url.searchParams.getAll('tag') || ''; + const tags = url.searchParams.getAll('tag'); - - - // 2. Build Search URL - const searchUrl = new URL('http://localhost:8080/api/documents/search'); - if (q) searchUrl.searchParams.set('q', q); - if (from) searchUrl.searchParams.set('from', from); - if (to) searchUrl.searchParams.set('to', to); - if (senderId) searchUrl.searchParams.set('senderId', senderId); - if (receiverId) searchUrl.searchParams.set('receiverId', receiverId); - if(tags) tags.forEach(tag => searchUrl.searchParams.append('tag', tag)); - - - // 3. Build Persons URL (to resolve names for the typeahead initial value) - // Ideally, we would have endpoints like /api/persons/{id}, but for now we load the list or search. - // To keep it simple and performant enough for now, we fetch all to find the names. - const personsUrl = 'http://localhost:8080/api/persons'; + const api = createApiClient(fetch); try { - const [docsRes, personsRes] = await Promise.all([ - fetch(searchUrl.toString()), - fetch(personsUrl) + const [docsResult, personsResult] = await Promise.all([ + api.GET('/api/documents/search', { + params: { + query: { + q: q || undefined, + from: from || undefined, + to: to || undefined, + senderId: senderId || undefined, + receiverId: receiverId || undefined, + tag: tags.length ? tags : undefined + } + } + }), + api.GET('/api/persons') ]); - if (docsRes.status === 401 || personsRes.status === 401) { + if (docsResult.response.status === 401 || personsResult.response.status === 401) { throw redirect(302, '/login'); } - const documents = await docsRes.json(); - const allPersons = await personsRes.json(); + const documents = docsResult.data ?? []; + const allPersons: { id: string; firstName: string; lastName: string }[] = personsResult.data ?? []; - // Resolve Names for the Typeahead Inputs - const senderObj = allPersons.find((p: any) => p.id === senderId); - const receiverObj = allPersons.find((p: any) => p.id === receiverId); - - const senderName = senderObj ? `${senderObj.firstName} ${senderObj.lastName}` : ''; - const receiverName = receiverObj ? `${receiverObj.firstName} ${receiverObj.lastName}` : ''; + const senderObj = allPersons.find(p => p.id === senderId); + const receiverObj = allPersons.find(p => p.id === receiverId); return { documents, - // We don't need to pass the full persons list to the frontend anymore, - // as the Typeahead fetches it dynamically. We only pass the resolved names. initialValues: { - senderName, - receiverName + senderName: senderObj ? `${senderObj.firstName} ${senderObj.lastName}` : '', + receiverName: receiverObj ? `${receiverObj.firstName} ${receiverObj.lastName}` : '' }, filters: { q, from, to, senderId, receiverId, tags } }; - - } catch (error) { - console.error("Error loading data:", error); + } catch (e) { + if ((e as { status?: number }).status) throw e; + console.error('Error loading data:', e); return { documents: [], initialValues: { senderName: '', receiverName: '' }, - filters: { q, from, to, senderId, receiverId }, - error: "Could not load data." + filters: { q, from, to, senderId, receiverId, tags } }; } -}; +} diff --git a/frontend/src/routes/admin/+page.server.ts b/frontend/src/routes/admin/+page.server.ts index 8f152be8..b7d0aa60 100644 --- a/frontend/src/routes/admin/+page.server.ts +++ b/frontend/src/routes/admin/+page.server.ts @@ -1,159 +1,146 @@ import { error, fail } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; -import { parseBackendError, getErrorMessage } from '$lib/errors'; +import { createApiClient } from '$lib/api.server'; +import { getErrorMessage } from '$lib/errors'; export async function load({ fetch, locals }) { - // 1. Check Permissions (Adapt logic to your user object) const user = locals.user; - - - // Assuming user.group.permissions is an array of strings - const hasAdmin = user?.groups.some(g => g.permissions.includes("ADMIN")); + const hasAdmin = user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN')); if (!hasAdmin) throw error(403, getErrorMessage('FORBIDDEN')); - const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; + const api = createApiClient(fetch); - // 2. Load Data - const [usersRes, groupsRes, tagsRes] = await Promise.all([ - fetch(baseUrl + '/api/users'), - fetch(baseUrl + '/api/groups'), - fetch(baseUrl + '/api/tags') + const [usersResult, groupsResult, tagsResult] = await Promise.all([ + api.GET('/api/users'), + api.GET('/api/groups'), + api.GET('/api/tags') ]); - return { - users: await usersRes.json(), - groups: await groupsRes.json(), - tags: await tagsRes.json() - }; + return { + users: usersResult.data ?? [], + groups: groupsResult.data ?? [], + tags: tagsResult.data ?? [] + }; } export const actions = { - createUser: async ({ request, fetch }) => { const data = await request.formData(); - const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080'; + const api = createApiClient(fetch); - // Extract array of group IDs - // "groupIds" matches the name attribute in the