refactor: migrate all page.server.ts files to typed API client
All server-side fetch calls now go through createApiClient() from $lib/api.server.ts, which wraps openapi-fetch with the generated OpenAPI types. This means backend changes are reflected in the frontend after running npm run generate:api. - Add stub src/lib/generated/api.ts (replaced by generate:api output) - Fix GroupController: missing /api prefix and ResponseStatusException - Root, conversations, persons, documents pages all use typed client - Error handling uses apiError.code directly (no parseBackendError needed) - Edit page load uses typed client; PUT action keeps raw fetch (multipart) - Login keeps raw fetch (explicit Authorization header, not cookie auth) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,8 @@ import org.raddatz.familienarchiv.repository.UserGroupRepository;
|
|||||||
import org.raddatz.familienarchiv.security.Permission;
|
import org.raddatz.familienarchiv.security.Permission;
|
||||||
import org.raddatz.familienarchiv.security.RequirePermission;
|
import org.raddatz.familienarchiv.security.RequirePermission;
|
||||||
import org.raddatz.familienarchiv.service.UserService;
|
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.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/groups")
|
@RequestMapping("/api/groups")
|
||||||
@RequirePermission(Permission.ADMIN_PERMISSION)
|
@RequirePermission(Permission.ADMIN_PERMISSION)
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GroupController {
|
public class GroupController {
|
||||||
@@ -42,7 +42,7 @@ public class GroupController {
|
|||||||
@PatchMapping("/{id}")
|
@PatchMapping("/{id}")
|
||||||
public ResponseEntity<UserGroup> updateGroup(@PathVariable UUID id, @RequestBody GroupDTO dto) {
|
public ResponseEntity<UserGroup> updateGroup(@PathVariable UUID id, @RequestBody GroupDTO dto) {
|
||||||
UserGroup group = groupRepository.findById(id)
|
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)
|
if (dto.getName() != null)
|
||||||
group.setName(dto.getName());
|
group.setName(dto.getName());
|
||||||
@@ -53,14 +53,9 @@ public class GroupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ResponseEntity<?> deleteGroup(@PathVariable UUID id) {
|
public ResponseEntity<Void> deleteGroup(@PathVariable UUID id) {
|
||||||
try {
|
groupRepository.deleteById(id);
|
||||||
// Optional: Check if users are assigned before deleting
|
return ResponseEntity.ok().build();
|
||||||
groupRepository.deleteById(id);
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ResponseEntity.badRequest().body("Gruppe konnte nicht gelöscht werden.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("")
|
@GetMapping("")
|
||||||
|
|||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -26,4 +26,5 @@ vite.config.ts.timestamp-*
|
|||||||
src/lib/paraglide
|
src/lib/paraglide
|
||||||
|
|
||||||
# Generated OpenAPI types — regenerate with: npm run generate:api
|
# 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
|
||||||
|
|||||||
15
frontend/src/lib/generated/api.ts
Normal file
15
frontend/src/lib/generated/api.ts
Normal file
@@ -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<string, any>;
|
||||||
|
export type components = Record<string, never>;
|
||||||
|
export type operations = Record<string, never>;
|
||||||
@@ -1,72 +1,58 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ url, fetch }) => {
|
export async function load({ url, fetch }) {
|
||||||
|
|
||||||
|
|
||||||
// 1. Extract params
|
|
||||||
const q = url.searchParams.get('q') || '';
|
const q = url.searchParams.get('q') || '';
|
||||||
const from = url.searchParams.get('from') || '';
|
const from = url.searchParams.get('from') || '';
|
||||||
const to = url.searchParams.get('to') || '';
|
const to = url.searchParams.get('to') || '';
|
||||||
const senderId = url.searchParams.get('senderId') || '';
|
const senderId = url.searchParams.get('senderId') || '';
|
||||||
const receiverId = url.searchParams.get('receiverId') || '';
|
const receiverId = url.searchParams.get('receiverId') || '';
|
||||||
const tags = url.searchParams.getAll('tag') || '';
|
const tags = url.searchParams.getAll('tag');
|
||||||
|
|
||||||
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
// 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';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [docsRes, personsRes] = await Promise.all([
|
const [docsResult, personsResult] = await Promise.all([
|
||||||
fetch(searchUrl.toString()),
|
api.GET('/api/documents/search', {
|
||||||
fetch(personsUrl)
|
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');
|
throw redirect(302, '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
const documents = await docsRes.json();
|
const documents = docsResult.data ?? [];
|
||||||
const allPersons = await personsRes.json();
|
const allPersons: { id: string; firstName: string; lastName: string }[] = personsResult.data ?? [];
|
||||||
|
|
||||||
// Resolve Names for the Typeahead Inputs
|
const senderObj = allPersons.find(p => p.id === senderId);
|
||||||
const senderObj = allPersons.find((p: any) => p.id === senderId);
|
const receiverObj = allPersons.find(p => p.id === receiverId);
|
||||||
const receiverObj = allPersons.find((p: any) => p.id === receiverId);
|
|
||||||
|
|
||||||
const senderName = senderObj ? `${senderObj.firstName} ${senderObj.lastName}` : '';
|
|
||||||
const receiverName = receiverObj ? `${receiverObj.firstName} ${receiverObj.lastName}` : '';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
documents,
|
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: {
|
initialValues: {
|
||||||
senderName,
|
senderName: senderObj ? `${senderObj.firstName} ${senderObj.lastName}` : '',
|
||||||
receiverName
|
receiverName: receiverObj ? `${receiverObj.firstName} ${receiverObj.lastName}` : ''
|
||||||
},
|
},
|
||||||
filters: { q, from, to, senderId, receiverId, tags }
|
filters: { q, from, to, senderId, receiverId, tags }
|
||||||
};
|
};
|
||||||
|
} catch (e) {
|
||||||
} catch (error) {
|
if ((e as { status?: number }).status) throw e;
|
||||||
console.error("Error loading data:", error);
|
console.error('Error loading data:', e);
|
||||||
return {
|
return {
|
||||||
documents: [],
|
documents: [],
|
||||||
initialValues: { senderName: '', receiverName: '' },
|
initialValues: { senderName: '', receiverName: '' },
|
||||||
filters: { q, from, to, senderId, receiverId },
|
filters: { q, from, to, senderId, receiverId, tags }
|
||||||
error: "Could not load data."
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,159 +1,146 @@
|
|||||||
import { error, fail } from '@sveltejs/kit';
|
import { error, fail } from '@sveltejs/kit';
|
||||||
import { env } from '$env/dynamic/private';
|
import { createApiClient } from '$lib/api.server';
|
||||||
import { parseBackendError, getErrorMessage } from '$lib/errors';
|
import { getErrorMessage } from '$lib/errors';
|
||||||
|
|
||||||
export async function load({ fetch, locals }) {
|
export async function load({ fetch, locals }) {
|
||||||
// 1. Check Permissions (Adapt logic to your user object)
|
|
||||||
const user = locals.user;
|
const user = locals.user;
|
||||||
|
const hasAdmin = user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'));
|
||||||
|
|
||||||
// Assuming user.group.permissions is an array of strings
|
|
||||||
const hasAdmin = user?.groups.some(g => g.permissions.includes("ADMIN"));
|
|
||||||
if (!hasAdmin) throw error(403, getErrorMessage('FORBIDDEN'));
|
if (!hasAdmin) throw error(403, getErrorMessage('FORBIDDEN'));
|
||||||
|
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
// 2. Load Data
|
const [usersResult, groupsResult, tagsResult] = await Promise.all([
|
||||||
const [usersRes, groupsRes, tagsRes] = await Promise.all([
|
api.GET('/api/users'),
|
||||||
fetch(baseUrl + '/api/users'),
|
api.GET('/api/groups'),
|
||||||
fetch(baseUrl + '/api/groups'),
|
api.GET('/api/tags')
|
||||||
fetch(baseUrl + '/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 = {
|
export const actions = {
|
||||||
|
|
||||||
createUser: async ({ request, fetch }) => {
|
createUser: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
// Extract array of group IDs
|
const { error: apiError, response } = await api.POST('/api/users', {
|
||||||
// "groupIds" matches the name attribute in the <select>
|
body: {
|
||||||
const groupIds = data.getAll('groupIds');
|
username: data.get('username'),
|
||||||
|
initialPassword: data.get('password'),
|
||||||
const payload = {
|
groupIds: data.getAll('groupIds')
|
||||||
username: data.get('username'),
|
}
|
||||||
initialPassword: data.get('password'),
|
|
||||||
groupIds: groupIds // Send array to backend
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Payload", JSON.stringify(payload))
|
|
||||||
|
|
||||||
const res = await fetch(baseUrl + '/api/users', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (apiError) {
|
||||||
const backendError = await parseBackendError(res);
|
const code = (apiError as { code?: string })?.code;
|
||||||
return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) });
|
return fail(response.status, { success: false, message: getErrorMessage(code) });
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteUser: async ({ request, fetch }) => {
|
deleteUser: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const id = data.get('id');
|
const id = data.get('id') as string;
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
const res = await fetch(baseUrl + `/api/users/${id}`, {
|
const { error: apiError, response } = await api.DELETE('/api/users/{id}', {
|
||||||
method: 'DELETE'
|
params: { path: { id } }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (apiError) {
|
||||||
const backendError = await parseBackendError(res);
|
const code = (apiError as { code?: string })?.code;
|
||||||
return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) });
|
return fail(response.status, { success: false, message: getErrorMessage(code) });
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTag: async ({ request, fetch }) => {
|
updateTag: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const id = data.get('id');
|
const id = data.get('id') as string;
|
||||||
const name = data.get('name');
|
const api = createApiClient(fetch);
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
await fetch(baseUrl + `/api/tags/${id}`, {
|
const { error: apiError, response } = await api.PUT('/api/tags/{id}', {
|
||||||
method: 'PUT',
|
params: { path: { id } },
|
||||||
headers: { 'Content-Type': 'application/json' },
|
body: { name: data.get('name') }
|
||||||
body: JSON.stringify({ name })
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (apiError) {
|
||||||
|
const code = (apiError as { code?: string })?.code;
|
||||||
|
return fail(response.status, { success: false, message: getErrorMessage(code) });
|
||||||
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTag: async ({ request, fetch }) => {
|
deleteTag: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const id = data.get('id');
|
const id = data.get('id') as string;
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
const res = await fetch(baseUrl + `/api/tags/${id}`, { method: 'DELETE' });
|
const { error: apiError, response } = await api.DELETE('/api/tags/{id}', {
|
||||||
if (!res.ok) {
|
params: { path: { id } }
|
||||||
const backendError = await parseBackendError(res);
|
});
|
||||||
return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) });
|
|
||||||
|
if (apiError) {
|
||||||
|
const code = (apiError as { code?: string })?.code;
|
||||||
|
return fail(response.status, { success: false, message: getErrorMessage(code) });
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
createGroup: async ({ request, fetch }) => {
|
createGroup: async ({ request, fetch }) => {
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const payload = {
|
const api = createApiClient(fetch);
|
||||||
name: data.get('name'),
|
|
||||||
permissions: data.getAll('permissions') // Gets all checked checkboxes
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await fetch(baseUrl + '/api/groups', {
|
const { error: apiError, response } = await api.POST('/api/groups', {
|
||||||
method: 'POST',
|
body: {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
name: data.get('name'),
|
||||||
body: JSON.stringify(payload)
|
permissions: data.getAll('permissions')
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (apiError) {
|
||||||
const backendError = await parseBackendError(res);
|
const code = (apiError as { code?: string })?.code;
|
||||||
return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) });
|
return fail(response.status, { success: false, message: getErrorMessage(code) });
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
updateGroup: async ({ request, fetch }) => {
|
updateGroup: async ({ request, fetch }) => {
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const id = data.get('id');
|
const id = data.get('id') as string;
|
||||||
const payload = {
|
const api = createApiClient(fetch);
|
||||||
name: data.get('name'),
|
|
||||||
permissions: data.getAll('permissions')
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await fetch(baseUrl + `/api/groups/${id}`, {
|
const { error: apiError, response } = await api.PATCH('/api/groups/{id}', {
|
||||||
method: 'PATCH',
|
params: { path: { id } },
|
||||||
headers: { 'Content-Type': 'application/json' },
|
body: {
|
||||||
body: JSON.stringify(payload)
|
name: data.get('name'),
|
||||||
|
permissions: data.getAll('permissions')
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (apiError) {
|
||||||
const backendError = await parseBackendError(res);
|
const code = (apiError as { code?: string })?.code;
|
||||||
return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) });
|
return fail(response.status, { success: false, message: getErrorMessage(code) });
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteGroup: async ({ request, fetch }) => {
|
deleteGroup: async ({ request, fetch }) => {
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const id = data.get('id');
|
const id = data.get('id') as string;
|
||||||
const res = await fetch(baseUrl + `/api/groups/${id}`, { method: 'DELETE' });
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
if (!res.ok) {
|
const { error: apiError, response } = await api.DELETE('/api/groups/{id}', {
|
||||||
const backendError = await parseBackendError(res);
|
params: { path: { id } }
|
||||||
return fail(res.status, { success: false, message: getErrorMessage(backendError?.code) });
|
});
|
||||||
|
|
||||||
|
if (apiError) {
|
||||||
|
const code = (apiError as { code?: string })?.code;
|
||||||
|
return fail(response.status, { success: false, message: getErrorMessage(code) });
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,61 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
|
||||||
export async function load({ url, fetch }) {
|
export async function load({ url, fetch }) {
|
||||||
|
|
||||||
|
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
// 1. Parameter auslesen
|
|
||||||
const senderId = url.searchParams.get('senderId') || '';
|
const senderId = url.searchParams.get('senderId') || '';
|
||||||
const receiverId = url.searchParams.get('receiverId') || '';
|
const receiverId = url.searchParams.get('receiverId') || '';
|
||||||
const from = url.searchParams.get('from') || '';
|
const from = url.searchParams.get('from') || '';
|
||||||
const to = url.searchParams.get('to') || '';
|
const to = url.searchParams.get('to') || '';
|
||||||
const dir = url.searchParams.get('dir') || 'DESC';
|
const dir = url.searchParams.get('dir') || 'DESC';
|
||||||
|
|
||||||
let documents = [];
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
|
let documents: unknown[] = [];
|
||||||
let senderName = '';
|
let senderName = '';
|
||||||
let receiverName = '';
|
let receiverName = '';
|
||||||
|
|
||||||
// 2. Fetch-Requests vorbereiten
|
const requests: Promise<void>[] = [];
|
||||||
const requests = [];
|
|
||||||
|
|
||||||
// Dokumente laden (nur wenn beide IDs da sind)
|
|
||||||
if (senderId && receiverId) {
|
if (senderId && receiverId) {
|
||||||
const query = new URLSearchParams({ senderId, receiverId, dir });
|
|
||||||
if (from) query.set('from', from);
|
|
||||||
if (to) query.set('to', to);
|
|
||||||
requests.push(
|
requests.push(
|
||||||
fetch(`${baseUrl}/api/documents/conversation?${query}`)
|
api.GET('/api/documents/conversation', {
|
||||||
.then(r => r.ok ? r.json() : [])
|
params: {
|
||||||
.then(data => documents = data)
|
query: {
|
||||||
|
senderId,
|
||||||
|
receiverId,
|
||||||
|
dir,
|
||||||
|
from: from || undefined,
|
||||||
|
to: to || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then(({ data }) => { documents = data ?? []; })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Namen auflösen für Typeahead Initial Value
|
|
||||||
if (senderId) {
|
if (senderId) {
|
||||||
requests.push(
|
requests.push(
|
||||||
fetch(`${baseUrl}/api/persons/${senderId}`)
|
api.GET('/api/persons/{id}', { params: { path: { id: senderId } } })
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(({ data }) => {
|
||||||
.then(p => senderName = p ? `${p.firstName} ${p.lastName}` : '')
|
const p = data as { firstName: string; lastName: string } | undefined;
|
||||||
|
if (p) senderName = `${p.firstName} ${p.lastName}`;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receiverId) {
|
if (receiverId) {
|
||||||
requests.push(
|
requests.push(
|
||||||
fetch(`${baseUrl}/api/persons/${receiverId}`)
|
api.GET('/api/persons/{id}', { params: { path: { id: receiverId } } })
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(({ data }) => {
|
||||||
.then(p => receiverName = p ? `${p.firstName} ${p.lastName}` : '')
|
const p = data as { firstName: string; lastName: string } | undefined;
|
||||||
|
if (p) receiverName = `${p.firstName} ${p.lastName}`;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alles parallel abfeuern
|
|
||||||
await Promise.all(requests);
|
await Promise.all(requests);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
documents,
|
documents,
|
||||||
initialValues: {
|
initialValues: { senderName, receiverName },
|
||||||
senderName,
|
|
||||||
receiverName
|
|
||||||
},
|
|
||||||
filters: { senderId, receiverId, from, to, dir }
|
filters: { senderId, receiverId, from, to, dir }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { error, redirect } from '@sveltejs/kit';
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
import { env } from '$env/dynamic/private';
|
import { createApiClient } from '$lib/api.server';
|
||||||
import { parseBackendError, getErrorMessage } from '$lib/errors';
|
import { getErrorMessage } from '$lib/errors';
|
||||||
|
|
||||||
export async function load({ params, fetch }) {
|
export async function load({ params, fetch }) {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
try {
|
const { data, error: apiError, response } = await api.GET('/api/documents/{id}', {
|
||||||
const res = await fetch(`${baseUrl}/api/documents/${id}`);
|
params: { path: { id } }
|
||||||
|
});
|
||||||
|
|
||||||
if (res.status === 401) throw redirect(302, '/login');
|
if (response.status === 401) throw redirect(302, '/login');
|
||||||
|
|
||||||
if (!res.ok) {
|
if (apiError) {
|
||||||
const backendError = await parseBackendError(res);
|
const code = (apiError as { code?: string })?.code;
|
||||||
throw error(res.status, getErrorMessage(backendError?.code));
|
throw error(response.status, getErrorMessage(code));
|
||||||
}
|
|
||||||
|
|
||||||
return { document: await res.json() };
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status) throw e;
|
|
||||||
throw error(500, getErrorMessage('INTERNAL_ERROR'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { document: data };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,35 @@
|
|||||||
import { error, fail, redirect } from '@sveltejs/kit';
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { createApiClient } from '$lib/api.server';
|
||||||
import { parseBackendError, getErrorMessage } from '$lib/errors';
|
import { parseBackendError, getErrorMessage } from '$lib/errors';
|
||||||
|
|
||||||
export async function load({ params, fetch }) {
|
export async function load({ params, fetch }) {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
try {
|
const [docResult, personsResult] = await Promise.all([
|
||||||
const [docRes, personsRes] = await Promise.all([
|
api.GET('/api/documents/{id}', { params: { path: { id } } }),
|
||||||
fetch(`${baseUrl}/api/documents/${id}`),
|
api.GET('/api/persons')
|
||||||
fetch(`${baseUrl}/api/persons`)
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
if (!docRes.ok) {
|
if (docResult.error) {
|
||||||
const backendError = await parseBackendError(docRes);
|
const code = (docResult.error as { code?: string })?.code;
|
||||||
throw error(docRes.status, getErrorMessage(backendError?.code));
|
throw error(docResult.response.status, getErrorMessage(code));
|
||||||
}
|
|
||||||
if (!personsRes.ok) {
|
|
||||||
throw error(personsRes.status, getErrorMessage('INTERNAL_ERROR'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
document: await docRes.json(),
|
|
||||||
persons: await personsRes.json()
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status) throw e;
|
|
||||||
throw error(500, getErrorMessage('INTERNAL_ERROR'));
|
|
||||||
}
|
}
|
||||||
|
if (personsResult.error) {
|
||||||
|
throw error(personsResult.response.status, getErrorMessage('INTERNAL_ERROR'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
document: docResult.data,
|
||||||
|
persons: personsResult.data
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ request, params, fetch }) => {
|
default: async ({ request, params, fetch }) => {
|
||||||
|
// Raw fetch is used here because FormData multipart bodies are passed through
|
||||||
|
// directly from the browser without transformation.
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
|||||||
@@ -12,19 +12,16 @@ export const actions = {
|
|||||||
return fail(400, { error: 'Bitte Benutzername und Passwort eingeben.' });
|
return fail(400, { error: 'Bitte Benutzername und Passwort eingeben.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wir bauen den Basic Auth Header
|
|
||||||
const credentials = btoa(`${username}:${password}`);
|
const credentials = btoa(`${username}:${password}`);
|
||||||
const authHeader = `Basic ${credentials}`;
|
const authHeader = `Basic ${credentials}`;
|
||||||
|
|
||||||
|
// Raw fetch is intentional here: we need to pass an explicit Authorization
|
||||||
|
// header built from the form data, not the cookie-based auth used elsewhere.
|
||||||
try {
|
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 baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||||
const response = await fetch(`${baseUrl}/api/users/me`, {
|
const response = await fetch(`${baseUrl}/api/users/me`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: { Authorization: authHeader }
|
||||||
'Authorization': authHeader
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
@@ -35,22 +32,18 @@ export const actions = {
|
|||||||
return fail(500, { error: getErrorMessage('INTERNAL_ERROR') });
|
return fail(500, { error: getErrorMessage('INTERNAL_ERROR') });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login erfolgreich! Wir speichern den Header in einem Cookie.
|
|
||||||
// (In Produktion würde man hier ein Session-Token nutzen, aber für Basic Auth müssen wir es mitschleifen)
|
|
||||||
cookies.set('auth_token', authHeader, {
|
cookies.set('auth_token', authHeader, {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true, // JavaScript kann das Cookie nicht lesen (Schutz vor XSS)
|
httpOnly: true,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
secure: false, // Auf true setzen, wenn wir HTTPS haben
|
secure: false, // set to true when HTTPS is available
|
||||||
maxAge: 60 * 60 * 24 // 1 Tag
|
maxAge: 60 * 60 * 24
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return fail(500, { error: getErrorMessage('INTERNAL_ERROR') });
|
return fail(500, { error: getErrorMessage('INTERNAL_ERROR') });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weiterleitung zur Startseite
|
|
||||||
return redirect(303, '/');
|
return redirect(303, '/');
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { createApiClient } from '$lib/api.server';
|
||||||
import { env } from '$env/dynamic/private';
|
|
||||||
|
|
||||||
export async function load({ url, fetch }) {
|
export async function load({ url, fetch }) {
|
||||||
const q = url.searchParams.get('q') || '';
|
const q = url.searchParams.get('q') || '';
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
// Query Parameter an Backend durchreichen
|
const { data } = await api.GET('/api/persons', {
|
||||||
const apiUrl = new URL(`${baseUrl}/api/persons`);
|
params: { query: { q: q || undefined } }
|
||||||
if (q) apiUrl.searchParams.set('q', q);
|
});
|
||||||
|
|
||||||
const res = await fetch(apiUrl.toString());
|
return { persons: data ?? [], q };
|
||||||
|
|
||||||
if (!res.ok) return { persons: [] };
|
|
||||||
|
|
||||||
const persons = await res.json();
|
|
||||||
return { persons, q };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import { error, } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { env } from '$env/dynamic/private';
|
import { createApiClient } from '$lib/api.server';
|
||||||
|
import { getErrorMessage } from '$lib/errors';
|
||||||
|
|
||||||
export async function load({ params, fetch }) {
|
export async function load({ params, fetch }) {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
const [personResult, docsResult] = await Promise.all([
|
||||||
|
api.GET('/api/persons/{id}', { params: { path: { id } } }),
|
||||||
|
api.GET('/api/persons/{id}/documents', { params: { path: { id } } })
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
if (personResult.error) {
|
||||||
// Parallel Fetching: Person Infos + Ihre Dokumente
|
const code = (personResult.error as { code?: string })?.code;
|
||||||
const [personRes, docsRes] = await Promise.all([
|
throw error(personResult.response.status, getErrorMessage(code));
|
||||||
fetch(`${baseUrl}/api/persons/${id}`),
|
|
||||||
fetch(`${baseUrl}/api/persons/${id}/documents`)
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (personRes.status === 404) throw error(404, 'Person nicht gefunden');
|
|
||||||
|
|
||||||
return {
|
|
||||||
person: await personRes.json(),
|
|
||||||
documents: await docsRes.json()
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status) throw e;
|
|
||||||
throw error(500, 'Ladefehler');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
person: personResult.data,
|
||||||
|
documents: docsResult.data ?? []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user