fix: correct malformed @Value annotations in DataInitializer
Missing closing braces caused Spring to inject the literal placeholder string instead of resolving the property, silently ignoring any app.admin.username / app.admin.password env-var overrides. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
141
docs/TODO-backend.md
Normal file
141
docs/TODO-backend.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Backend TODO
|
||||
|
||||
Findings from architectural review. Ordered roughly by severity.
|
||||
|
||||
---
|
||||
|
||||
## Bugs
|
||||
|
||||
### `@Value` annotation missing closing brace
|
||||
**File:** `config/DataInitializer.java:33–37`
|
||||
```java
|
||||
// Current (broken — Spring may silently fail to resolve the property)
|
||||
@Value("${app.admin.username:admin")
|
||||
@Value("${app.admin.password:admin123")
|
||||
|
||||
// Fix
|
||||
@Value("${app.admin.username:admin}")
|
||||
@Value("${app.admin.password:admin123}")
|
||||
```
|
||||
Spring's `@Value` SpEL parser requires the closing `}`. Without it, the literal string `"${app.admin.username:admin"` is injected instead of the resolved value. The default credentials will appear to work but env-var overrides will silently be ignored.
|
||||
|
||||
---
|
||||
|
||||
### Default credentials logged in plaintext
|
||||
**File:** `config/DataInitializer.java:64`
|
||||
```java
|
||||
log.info("Default Admin erstellt: User='admin', Pass='admin123'");
|
||||
```
|
||||
The password appears in application logs. Remove the password from the log statement entirely. Log only the username.
|
||||
|
||||
---
|
||||
|
||||
## Design Issues
|
||||
|
||||
### Test data runs in every environment
|
||||
**File:** `config/DataInitializer.java:69–143`
|
||||
|
||||
`initData` seeds 500 fake documents and 4 random persons whenever the database is empty. This runs unconditionally — including in production — and the log message at line 141 claims "50 Personen" when only 4 are created (copy/paste error).
|
||||
|
||||
**Fix:** Guard the bean with a Spring profile so it only runs locally:
|
||||
```java
|
||||
@Bean
|
||||
@Profile("dev")
|
||||
public CommandLineRunner initData(...) { ... }
|
||||
```
|
||||
Set `spring.profiles.active=dev` in `application.properties` for local use, and ensure the production environment does not set that profile.
|
||||
|
||||
---
|
||||
|
||||
### Two redundant permission groups created on startup
|
||||
**File:** `config/DataInitializer.java:49–53` and `101–105`
|
||||
|
||||
`initAdminUser` creates group `"Administrators"` with `{ADMIN, READ_ALL, WRITE_ALL}`.
|
||||
`initData` creates group `"Admins"` with `{READ_ALL, WRITE_ALL, ADMIN}` — identical permissions, different name, different bean.
|
||||
|
||||
Both beans run when the DB is empty, resulting in two duplicate admin groups and the admin user only belonging to the first one. `initData` is also guarded by `personRepo.count() > 0` — so if persons already exist it won't create the second group, making startup behaviour inconsistent.
|
||||
|
||||
**Fix:** Consolidate into a single `CommandLineRunner` bean or extract group creation into a shared `@PostConstruct` method. Remove `initData`'s group creation entirely (it belongs in `initAdminUser`).
|
||||
|
||||
---
|
||||
|
||||
### CSRF protection disabled with no plan to re-enable it
|
||||
**File:** `config/SecurityConfig.java:39`
|
||||
```java
|
||||
.csrf(csrf -> csrf.disable())
|
||||
```
|
||||
The comment says "for development". HTTP Basic Auth over a cookie-authenticated SvelteKit frontend is still vulnerable to CSRF for mutating endpoints (PUT, DELETE, POST).
|
||||
|
||||
**Fix:** Either:
|
||||
- Re-enable CSRF with `CookieCsrfTokenRepository` and pass the token to the frontend, or
|
||||
- Adopt a stateless JWT/session approach and verify `Origin`/`Referer` headers
|
||||
|
||||
At minimum, document this as a known gap that must be addressed before any external deployment.
|
||||
|
||||
---
|
||||
|
||||
### Spring Session tables are created but never used
|
||||
**File:** `db/migration/V1__initial_schema.sql` (Spring Session tables), `config/SecurityConfig.java`
|
||||
|
||||
The schema includes `spring_session` and `spring_session_attributes` tables, but the auth model is stateless HTTP Basic — credentials are re-validated from the database on every request. Spring Session JDBC is wiring itself up but managing nothing meaningful.
|
||||
|
||||
**Fix:** Decide on one model:
|
||||
- **Keep Basic Auth (stateless):** Remove the Spring Session JDBC dependency and tables. Each request re-authenticates — which is fine for a small internal app.
|
||||
- **Switch to session-based auth:** Replace Basic Auth with form login, issue a server-side session ID, and let Spring Session manage it. This reduces per-request DB hits.
|
||||
|
||||
---
|
||||
|
||||
### `Permission` enum not used consistently — permissions are plain strings
|
||||
**File:** `security/Permission.java`, `security/RequirePermission.java`, `security/PermissionAspect.java`
|
||||
|
||||
`@RequirePermission` takes a `Permission` enum value, but user group permissions are stored and compared as raw `String` values (e.g., `"ADMIN"`, `"READ_ALL"`). The comparison in `PermissionAspect` calls `permission.name()` to convert the enum back to a string for matching — so type-safety is only at the annotation call site, not end-to-end.
|
||||
|
||||
**Fix:** Make the entire stack type-safe: store permissions as the enum's name (which is already happening) but also parse them back into `Permission` enum values when loading `UserDetails` authorities. This way a typo in the DB is caught at load time rather than silently failing permission checks.
|
||||
|
||||
---
|
||||
|
||||
### `MassImportService` provides no status or error feedback
|
||||
**File:** `service/MassImportService.java`, `controller/AdminController.java`
|
||||
|
||||
`/api/admin/trigger-import` returns immediately (async), but there is no way for the admin to know whether the import succeeded, failed, or is still running. Errors during async execution are silently swallowed.
|
||||
|
||||
**Fix options:**
|
||||
- Store import job status in a DB table (`import_jobs`) with state (`RUNNING`, `DONE`, `FAILED`) and expose a `GET /api/admin/import-status` endpoint
|
||||
- Alternatively, make the endpoint synchronous since it already blocks on file I/O — only use async if you need true non-blocking behaviour
|
||||
|
||||
---
|
||||
|
||||
## Missing Capabilities
|
||||
|
||||
### No test coverage
|
||||
**File:** `src/test/java/org/raddatz/familienarchiv/FamilienarchivApplicationTests.java`
|
||||
|
||||
The only test is a Spring context load test. No unit or integration tests exist for any service, repository, or controller logic.
|
||||
|
||||
**Suggested starting points (highest value for effort):**
|
||||
1. `DocumentSpecifications` — pure logic, easy to unit test with an in-memory H2 or Testcontainers PostgreSQL
|
||||
2. `ExcelService` — parsing logic, test with fixture `.xlsx` files (one exists in `api_tests/`)
|
||||
3. `PermissionAspect` — security logic should be tested; use `@WithMockUser` from Spring Security Test
|
||||
|
||||
---
|
||||
|
||||
### No API documentation
|
||||
There is no OpenAPI/Swagger endpoint. The only documentation is the `.http` files in `backend/api_tests/`.
|
||||
|
||||
**Fix:** Add `springdoc-openapi-starter-webmvc-ui` to `pom.xml`. It generates `/swagger-ui.html` and `/v3/api-docs` automatically from existing controller annotations with zero additional code.
|
||||
|
||||
---
|
||||
|
||||
### `FileService` content-type detection is fragile
|
||||
File extension is inferred from the S3 key string. Many document types (TIFF scans, HEIC photos, Word documents) are unhandled and fall back to `application/octet-stream`, forcing a download instead of inline display.
|
||||
|
||||
**Fix:** Use `java.nio.file.Files.probeContentType()` or Apache Tika for robust MIME detection. Alternatively, store the content-type at upload time in the `Document` entity and retrieve it on download.
|
||||
|
||||
---
|
||||
|
||||
### Generic error response is hardcoded in German
|
||||
**File:** `controller/GlobalExceptionHandler.java:36`
|
||||
```java
|
||||
.body(new ErrorResponse("Ein Fehler ist aufgetreten"))
|
||||
```
|
||||
The fallback error message is hardcoded in German. It should use a locale-aware message source or at minimum be in English as the lingua franca of APIs.
|
||||
148
docs/TODO-frontend.md
Normal file
148
docs/TODO-frontend.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Frontend TODO
|
||||
|
||||
Findings from architectural review. Ordered roughly by severity.
|
||||
|
||||
---
|
||||
|
||||
## Bugs
|
||||
|
||||
### Backend URL hardcoded as `localhost` in the session hook
|
||||
**File:** `src/hooks.server.ts:20`
|
||||
```ts
|
||||
const response = await fetch('http://localhost:8080/api/users/me', {
|
||||
```
|
||||
The `userGroup` handle (which runs on every request) calls the backend via a hardcoded `localhost:8080` URL. The `API_INTERNAL_URL` env var used in `handleFetch` is not applied here. Inside Docker, `localhost` from the frontend container does not resolve to the backend container — requests will fail silently (the catch swallows the error) and `event.locals.user` will be `undefined` for every request.
|
||||
|
||||
**Fix:** Centralise the base URL:
|
||||
```ts
|
||||
const API_BASE = env.API_INTERNAL_URL ?? 'http://localhost:8080';
|
||||
// then use it in both userGroup and handleFetch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `import { env } from 'process'` bypasses SvelteKit's env system
|
||||
**File:** `src/hooks.server.ts:4`
|
||||
```ts
|
||||
import { env } from 'process';
|
||||
```
|
||||
This is a raw Node.js import that bypasses SvelteKit's `$env/dynamic/private` and `$env/static/private` modules. It won't work if the adapter is ever changed (e.g., to Deno or a serverless edge runtime), and it skips SvelteKit's build-time validation that env vars are actually set.
|
||||
|
||||
**Fix:**
|
||||
```ts
|
||||
import { env } from '$env/dynamic/private';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design Issues
|
||||
|
||||
### Every page load hits the backend twice minimum
|
||||
**File:** `src/hooks.server.ts:15–34`
|
||||
|
||||
The `userGroup` hook calls `GET /api/users/me` on every single server-side request to check the session. Then each page's `+page.server.ts` load function makes its own API calls. For a search page that's three sequential backend round-trips before the user sees anything.
|
||||
|
||||
**Fix options:**
|
||||
- Cache the user in the session (if Spring Session is adopted on the backend) — validate the session cookie once, not per request
|
||||
- If sticking with Basic Auth: trust the cookie value directly and only call `/api/users/me` when the user object is actually needed (e.g., in `+layout.server.ts`), not unconditionally in the hook
|
||||
|
||||
---
|
||||
|
||||
### Two conflicting package managers
|
||||
**Files:** `package-lock.json` (npm), `yarn.lock` (Yarn)
|
||||
|
||||
Both lock files exist, meaning the project has been installed with both npm and Yarn at different times. This leads to divergent dependency trees and confusing contributor setup.
|
||||
|
||||
**Fix:** Pick one (npm is already the default in the devcontainer), delete the other lock file, and add an `.npmrc` or `package.json` `engines` field to enforce it:
|
||||
```json
|
||||
"engines": { "npm": ">=10" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### i18n is a stub — only German is implemented
|
||||
**Files:** `messages/en.json`, `messages/es.json`
|
||||
|
||||
English and Spanish message files contain only the scaffolding example key `hello_world`. All actual UI strings are rendered directly in German inside Svelte components and are not extracted into the message catalogue.
|
||||
|
||||
**Fix options:**
|
||||
- If multi-language support is a real requirement: extract all German strings from `.svelte` files into `messages/de.json` and provide translations in `en.json` / `es.json`
|
||||
- If it's not a requirement: remove Paraglide, the `messages/` directory, and the i18n hook to reduce complexity
|
||||
|
||||
---
|
||||
|
||||
### No `Dockerfile` — frontend cannot run in Docker
|
||||
**File:** `docker-compose.yml` (frontend service commented out)
|
||||
|
||||
The frontend service in `docker-compose.yml` is commented out because no `Dockerfile` exists. The SvelteKit Node adapter is already configured, so producing a Dockerfile is straightforward.
|
||||
|
||||
**Fix:** Add `frontend/Dockerfile`:
|
||||
```dockerfile
|
||||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/build ./build
|
||||
COPY --from=builder /app/package.json .
|
||||
EXPOSE 3000
|
||||
CMD ["node", "build"]
|
||||
```
|
||||
Then uncomment and complete the frontend service in `docker-compose.yml`, adding `API_INTERNAL_URL=http://backend:8080`.
|
||||
|
||||
---
|
||||
|
||||
### Demo routes are leftover scaffolding
|
||||
**Files:** `src/routes/demo/+page.svelte`, `src/routes/demo/paraglide/+page.svelte`
|
||||
|
||||
These routes were generated by the SvelteKit + Paraglide scaffolding tool. They serve no function in the application and are publicly accessible.
|
||||
|
||||
**Fix:** Delete `src/routes/demo/` entirely.
|
||||
|
||||
---
|
||||
|
||||
## Missing Capabilities
|
||||
|
||||
### No shared TypeScript types with the backend
|
||||
The frontend manually constructs objects and parses JSON responses without any type definitions that match the backend's DTOs (`DocumentUpdateDTO`, `GroupDTO`, etc.). A breaking change in the backend API — renaming a field, changing a type — won't be caught until runtime.
|
||||
|
||||
**Fix options (pick one):**
|
||||
- **Simple:** Define matching TypeScript interfaces manually in `src/lib/types.ts` for all backend response shapes and use them in load functions
|
||||
- **Robust:** Add `springdoc-openapi` to the backend and use `openapi-typescript` to generate types from the OpenAPI spec as part of the build pipeline
|
||||
|
||||
---
|
||||
|
||||
### No global error page
|
||||
SvelteKit renders a default unstyled error page for unhandled errors and 404s. There is no `src/routes/+error.svelte` in the project.
|
||||
|
||||
**Fix:** Create `src/routes/+error.svelte` with the application's layout and a user-friendly message. Use SvelteKit's `$page.status` and `$page.error` stores to display appropriate messages for 404 vs 500.
|
||||
|
||||
---
|
||||
|
||||
### No loading state during navigation
|
||||
Long-running searches or slow backend responses give the user no visual feedback. SvelteKit's `$navigating` store is available but appears unused.
|
||||
|
||||
**Fix:** Add a global navigation progress indicator in `+layout.svelte`:
|
||||
```svelte
|
||||
<script>
|
||||
import { navigating } from '$app/stores';
|
||||
</script>
|
||||
{#if $navigating}
|
||||
<div class="progress-bar" />
|
||||
{/if}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `handleFetch` redirect inside a hook can cause silent failures
|
||||
**File:** `src/hooks.server.ts:46`
|
||||
```ts
|
||||
throw redirect(302, '/login');
|
||||
```
|
||||
Throwing a redirect inside `handleFetch` during a server-side load that makes multiple API calls (e.g., the search page calling both `/api/documents/search` and `/api/persons`) means only the first missing-token fetch triggers the redirect — subsequent ones may behave unpredictably.
|
||||
|
||||
**Fix:** Move the auth guard to `+layout.server.ts` as the single authoritative redirect point, and let `handleFetch` simply pass through without the token (letting the backend return 401). Handle 401 responses from the backend in load functions with an explicit `redirect(302, '/login')`.
|
||||
266
docs/architecture/c4-diagrams.md
Normal file
266
docs/architecture/c4-diagrams.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# Familienarchiv — C4 Architecture Diagrams
|
||||
|
||||
## Level 1 — System Context
|
||||
|
||||
Who uses the system and what external systems does it interact with.
|
||||
|
||||
```mermaid
|
||||
C4Context
|
||||
title System Context: Familienarchiv
|
||||
|
||||
Person(admin, "Administrator", "Manages users, triggers bulk imports, reviews documents")
|
||||
Person(member, "Family Member", "Searches, browses, and reads archived documents")
|
||||
|
||||
System(familienarchiv, "Familienarchiv", "Web application for digitising, organising, and searching family documents")
|
||||
|
||||
Rel(admin, familienarchiv, "Manages via browser", "HTTPS")
|
||||
Rel(member, familienarchiv, "Searches and views via browser", "HTTPS")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Level 2 — Containers
|
||||
|
||||
The deployable units that make up the system and how they communicate.
|
||||
|
||||
```mermaid
|
||||
C4Container
|
||||
title Container Diagram: Familienarchiv
|
||||
|
||||
Person(user, "User", "Admin or family member")
|
||||
|
||||
System_Boundary(archiv, "Familienarchiv (Docker Compose)") {
|
||||
Container(frontend, "Web Frontend", "SvelteKit / Node.js", "Server-side rendered UI. Handles session cookies, search UI, document viewer, and admin panel.")
|
||||
|
||||
Container(backend, "API Backend", "Spring Boot 4 / Java 21 / Jetty", "REST API. Implements document management, search, user auth, file upload/download, and Excel import.")
|
||||
|
||||
ContainerDb(db, "Relational Database", "PostgreSQL 16", "Stores document metadata, persons, users, permission groups, tags, and Spring Session data.")
|
||||
|
||||
ContainerDb(storage, "Object Storage", "MinIO (S3-compatible)", "Stores the actual document files (PDFs, scans). Objects keyed as documents/{UUID}_{filename}.")
|
||||
|
||||
Container(mc, "Bucket Init Helper", "MinIO Client (mc)", "One-shot container on startup. Creates the archive bucket with private access policy.")
|
||||
}
|
||||
|
||||
Rel(user, frontend, "Uses", "HTTPS / Browser")
|
||||
Rel(frontend, backend, "API requests with Basic Auth token", "HTTP / REST / JSON")
|
||||
Rel(backend, db, "Reads and writes metadata and sessions", "JDBC / SQL")
|
||||
Rel(backend, storage, "Uploads and streams document files", "HTTP / S3 API (AWS SDK v2)")
|
||||
Rel(mc, storage, "Creates bucket on startup", "MinIO Client CLI")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Level 3 — Components: API Backend
|
||||
|
||||
The internal structure of the Spring Boot backend.
|
||||
|
||||
```mermaid
|
||||
C4Component
|
||||
title Component Diagram: API Backend
|
||||
|
||||
Container(frontend, "Web Frontend", "SvelteKit")
|
||||
ContainerDb(db, "PostgreSQL")
|
||||
ContainerDb(minio, "MinIO")
|
||||
|
||||
System_Boundary(backend, "API Backend (Spring Boot)") {
|
||||
|
||||
Component(secFilter, "Security Filter Chain", "Spring Security", "Enforces authentication on all requests. Parses Basic Auth header and validates credentials via BCrypt.")
|
||||
Component(permAspect, "PermissionAspect", "Spring AOP", "Intercepts methods annotated with @RequirePermission. Checks user's granted authorities against the required permission. Throws 401/403 if denied.")
|
||||
|
||||
Component(docCtrl, "DocumentController", "Spring MVC — /api/documents", "CRUD for documents. Endpoints: search, get by ID, update metadata, upload file, download file, get conversation thread.")
|
||||
Component(personCtrl, "PersonController", "Spring MVC — /api/persons", "Lists and searches family members. Also returns all documents sent by a person.")
|
||||
Component(userCtrl, "UserController", "Spring MVC — /api/users", "Returns current user (/me). Creates and deletes users (requires ADMIN_USER permission).")
|
||||
Component(adminCtrl, "AdminController", "Spring MVC — /api/admin", "Triggers asynchronous Excel mass import (requires ADMIN permission).")
|
||||
Component(groupCtrl, "GroupController", "Spring MVC — /api/groups", "Lists and manages permission groups.")
|
||||
Component(tagCtrl, "TagController", "Spring MVC — /api/tags", "Lists tags for typeahead.")
|
||||
|
||||
Component(docSvc, "DocumentService", "Spring Service", "Core business logic: store, update, search documents. Resolves persons and tags. Delegates file I/O to FileService. Builds JPA Specifications for dynamic search queries.")
|
||||
Component(fileSvc, "FileService", "Spring Service", "Wraps AWS SDK v2 S3Client. Uploads files with UUID-keyed paths. Downloads with content-type detection (PDF, JPEG, PNG, octet-stream).")
|
||||
Component(excelSvc, "ExcelService", "Spring Service", "Parses Excel workbooks (Apache POI). Column indices are configurable via application.properties. Creates/updates document records per row.")
|
||||
Component(massImport, "MassImportService", "Spring Service — @Async", "Reads Excel files from /import mount. Delegates to ExcelService. Runs asynchronously so the HTTP response returns immediately.")
|
||||
Component(userSvc, "UserService", "Spring Service", "User CRUD. Encodes passwords with BCrypt. Assigns users to permission groups.")
|
||||
Component(dataInit, "DataInitializer", "CommandLineRunner", "On startup: creates default admin user and groups if none exist. Seeds test data (persons, documents) if DB is empty.")
|
||||
|
||||
Component(docRepo, "DocumentRepository", "Spring Data JPA", "Queries documents. Supports Specification-based dynamic search, conversation thread queries (bidirectional sender/receiver), and filename lookups.")
|
||||
Component(docSpec, "DocumentSpecifications", "JPA Criteria API", "Factory for composable query predicates: hasText (full-text across title/filename/transcription/location), hasSender, hasReceiver (join), isBetween (date range), hasTags (subquery AND logic).")
|
||||
Component(personRepo, "PersonRepository", "Spring Data JPA", "Lists all persons sorted by last name. Supports name search for typeahead.")
|
||||
Component(userRepo, "AppUserRepository", "Spring Data JPA", "Finds users by username. Used by Spring Security and UserService.")
|
||||
Component(tagRepo, "TagRepository", "Spring Data JPA", "Finds or creates tags by name (case-insensitive).")
|
||||
Component(groupRepo, "UserGroupRepository", "Spring Data JPA", "Manages permission groups.")
|
||||
|
||||
Component(minioConf, "MinioConfig", "Spring @Configuration", "Creates the S3Client bean with path-style access for MinIO. Validates MinIO connectivity on startup.")
|
||||
Component(secConf, "SecurityConfig", "Spring @Configuration", "Configures filter chain: all routes require authentication, CSRF disabled, BCrypt password encoder, DaoAuthenticationProvider with CustomUserDetailsService.")
|
||||
Component(userDetails, "CustomUserDetailsService", "Spring Security UserDetailsService", "Loads AppUser by username from DB. Converts group permissions to Spring GrantedAuthority objects.")
|
||||
}
|
||||
|
||||
Rel(frontend, secFilter, "All requests", "HTTP / Basic Auth header")
|
||||
Rel(secFilter, permAspect, "Authenticated requests proceed", "")
|
||||
|
||||
Rel(secFilter, docCtrl, "Routes to", "")
|
||||
Rel(secFilter, personCtrl, "Routes to", "")
|
||||
Rel(secFilter, userCtrl, "Routes to", "")
|
||||
Rel(secFilter, adminCtrl, "Routes to", "")
|
||||
|
||||
Rel(permAspect, docCtrl, "Guards", "AOP @Around")
|
||||
Rel(permAspect, userCtrl, "Guards", "AOP @Around")
|
||||
Rel(permAspect, adminCtrl, "Guards", "AOP @Around")
|
||||
|
||||
Rel(docCtrl, docSvc, "Delegates to", "")
|
||||
Rel(adminCtrl, massImport, "Triggers", "")
|
||||
Rel(userCtrl, userSvc, "Delegates to", "")
|
||||
|
||||
Rel(docSvc, fileSvc, "Upload / download files", "")
|
||||
Rel(docSvc, docRepo, "Reads / writes documents", "")
|
||||
Rel(docSvc, docSpec, "Builds search predicates", "")
|
||||
Rel(docSvc, personRepo, "Resolves sender / receivers", "")
|
||||
Rel(docSvc, tagRepo, "Finds or creates tags", "")
|
||||
|
||||
Rel(massImport, excelSvc, "Parses Excel file", "")
|
||||
Rel(excelSvc, docSvc, "Creates / updates documents", "")
|
||||
|
||||
Rel(userSvc, userRepo, "Reads / writes users", "")
|
||||
Rel(userSvc, groupRepo, "Assigns groups", "")
|
||||
Rel(userDetails, userRepo, "Loads user by username", "")
|
||||
|
||||
Rel(fileSvc, minio, "PUT / GET objects", "S3 API / HTTP")
|
||||
Rel(docRepo, db, "SQL queries", "JDBC")
|
||||
Rel(personRepo, db, "SQL queries", "JDBC")
|
||||
Rel(userRepo, db, "SQL queries", "JDBC")
|
||||
Rel(tagRepo, db, "SQL queries", "JDBC")
|
||||
Rel(groupRepo, db, "SQL queries", "JDBC")
|
||||
Rel(dataInit, db, "Seeds initial data", "JDBC")
|
||||
Rel(secConf, userDetails, "Wires", "")
|
||||
Rel(minioConf, fileSvc, "Provides S3Client bean", "")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Level 3 — Components: Web Frontend
|
||||
|
||||
The internal structure of the SvelteKit frontend.
|
||||
|
||||
```mermaid
|
||||
C4Component
|
||||
title Component Diagram: Web Frontend
|
||||
|
||||
Person(user, "User")
|
||||
Container(backend, "API Backend", "Spring Boot")
|
||||
|
||||
System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") {
|
||||
|
||||
Component(hooks, "hooks.server.ts", "SvelteKit Server Hook", "Two responsibilities: (1) userGroup handle — reads auth_token cookie, fetches /api/users/me, stores user in event.locals. (2) handleFetch — intercepts all outgoing fetch() calls, injects Authorization header from cookie. Redirects to /login if token absent.")
|
||||
Component(i18n, "hooks.ts (Paraglide)", "SvelteKit Client Hook", "Client-side i18n middleware. Detects language from URL and sets the active locale for Paraglide.js translation functions.")
|
||||
|
||||
Component(layout, "+layout.server.ts", "SvelteKit Layout Loader", "Passes event.locals.user down to all child pages so every route has access to the authenticated user.")
|
||||
|
||||
Component(homePage, "/ (Home / Search)", "SvelteKit Route", "Loader: parses URL search params (q, from, to, senderId, receiverId, tags), fetches /api/documents/search and /api/persons, returns results. Page: renders search form with full-text, date range, sender/receiver typeahead, tag filters. Displays paginated document list.")
|
||||
Component(docDetail, "/documents/[id]", "SvelteKit Route", "Loader: fetches /api/documents/{id}. Handles 401 redirect to login, 404 error. Page: shows document metadata, file viewer (PDF/image inline), transcription, tags.")
|
||||
Component(docEdit, "/documents/[id]/edit", "SvelteKit Route", "Form with PersonTypeahead for sender/receiver, TagInput for tags, date/location fields. Submits PUT to /api/documents/{id}.")
|
||||
Component(persons, "/persons and /persons/[id]", "SvelteKit Routes", "Lists all persons. Detail page shows person metadata and all documents they sent.")
|
||||
Component(conversations, "/conversations", "SvelteKit Route", "Selects two persons via PersonTypeahead, fetches /api/documents/conversation, displays chronological exchange.")
|
||||
Component(loginPage, "/login", "SvelteKit Route", "Form action: encodes username:password as Base64 Basic Auth token, POSTs to /api/users/me to validate, sets auth_token httpOnly cookie (SameSite=strict, maxAge=86400), redirects to /.")
|
||||
Component(logoutPage, "/logout", "SvelteKit Route (server-only)", "Clears the auth_token cookie and redirects to /login.")
|
||||
Component(adminPage, "/admin", "SvelteKit Route", "User management UI (create/delete users). Excel import trigger button (calls /api/admin/trigger-import).")
|
||||
|
||||
Component(apiPersons, "/api/persons (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/persons?q=... to backend. Used by PersonTypeahead for typeahead suggestions.")
|
||||
Component(apiTags, "/api/tags (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/tags to backend. Used by TagInput for autocomplete.")
|
||||
|
||||
Component(typeahead, "PersonTypeahead.svelte", "Svelte Component", "Async autocomplete for selecting a person. Debounces input, calls /api/persons?q=.")
|
||||
Component(tagInput, "TagInput.svelte", "Svelte Component", "Multi-tag input. Supports free-text entry and selecting existing tags from /api/tags.")
|
||||
}
|
||||
|
||||
Rel(user, hooks, "Every browser request", "HTTPS")
|
||||
Rel(hooks, backend, "GET /api/users/me (session check)", "HTTP / Basic Auth")
|
||||
Rel(hooks, loginPage, "Redirect if no token", "")
|
||||
|
||||
Rel(layout, homePage, "Provides user context", "")
|
||||
Rel(layout, docDetail, "Provides user context", "")
|
||||
Rel(layout, adminPage, "Provides user context", "")
|
||||
|
||||
Rel(homePage, backend, "GET /api/documents/search", "HTTP / JSON")
|
||||
Rel(homePage, backend, "GET /api/persons", "HTTP / JSON")
|
||||
Rel(docDetail, backend, "GET /api/documents/{id}", "HTTP / JSON")
|
||||
Rel(docDetail, backend, "GET /api/documents/{id}/file", "HTTP / Binary stream")
|
||||
Rel(docEdit, backend, "PUT /api/documents/{id}", "HTTP / Multipart")
|
||||
Rel(conversations, backend, "GET /api/documents/conversation", "HTTP / JSON")
|
||||
Rel(loginPage, backend, "POST /api/users/me (auth check)", "HTTP / Basic Auth")
|
||||
Rel(adminPage, backend, "GET/POST/DELETE /api/users", "HTTP / JSON")
|
||||
Rel(adminPage, backend, "POST /api/admin/trigger-import", "HTTP / JSON")
|
||||
|
||||
Rel(apiPersons, backend, "GET /api/persons", "HTTP / JSON")
|
||||
Rel(apiTags, backend, "GET /api/tags", "HTTP / JSON")
|
||||
|
||||
Rel(homePage, typeahead, "Uses for sender/receiver filter", "")
|
||||
Rel(docEdit, typeahead, "Uses for sender/receiver selection", "")
|
||||
Rel(docEdit, tagInput, "Uses for tag management", "")
|
||||
Rel(typeahead, apiPersons, "Fetches suggestions", "HTTP")
|
||||
Rel(tagInput, apiTags, "Fetches existing tags", "HTTP")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
How a user session is established and maintained across requests.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant Browser
|
||||
participant Frontend as Frontend (SvelteKit)
|
||||
participant Backend as Backend (Spring Boot)
|
||||
participant DB as PostgreSQL
|
||||
|
||||
User->>Browser: Enter username + password
|
||||
Browser->>Frontend: POST /login (form action)
|
||||
Frontend->>Frontend: Base64 encode "user:password"
|
||||
Frontend->>Backend: GET /api/users/me<br/>Authorization: Basic <token>
|
||||
Backend->>Backend: Spring Security parses Basic Auth
|
||||
Backend->>DB: SELECT user WHERE username=?
|
||||
DB-->>Backend: AppUser + groups + permissions
|
||||
Backend->>Backend: BCrypt.matches(password, hash)
|
||||
Backend-->>Frontend: 200 OK — UserDTO
|
||||
Frontend->>Browser: Set-Cookie: auth_token=<base64><br/>(httpOnly, SameSite=strict, maxAge=86400)
|
||||
Browser->>Frontend: GET / (next request)
|
||||
Frontend->>Frontend: hooks.server.ts reads auth_token cookie
|
||||
Frontend->>Backend: GET /api/users/me<br/>Authorization: Basic <token>
|
||||
Backend-->>Frontend: 200 OK — user in event.locals
|
||||
Frontend-->>Browser: Render page with user context
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Document Upload Flow
|
||||
|
||||
How a document file moves from a user's browser to MinIO.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant Frontend as Frontend (SvelteKit)
|
||||
participant Backend as Backend (Spring Boot)
|
||||
participant Aspect as PermissionAspect (AOP)
|
||||
participant DocSvc as DocumentService
|
||||
participant FileSvc as FileService
|
||||
participant MinIO
|
||||
participant DB as PostgreSQL
|
||||
|
||||
User->>Frontend: Submit edit form (file + metadata)
|
||||
Frontend->>Backend: PUT /api/documents/{id}<br/>multipart/form-data + Authorization header
|
||||
Backend->>Aspect: @RequirePermission(WRITE_ALL) check
|
||||
Aspect->>Aspect: Verify user has WRITE_ALL authority
|
||||
Aspect-->>Backend: Proceed
|
||||
Backend->>DocSvc: updateDocument(id, dto, file)
|
||||
DocSvc->>DocSvc: Resolve sender Person by ID
|
||||
DocSvc->>DocSvc: Resolve/create Tags
|
||||
DocSvc->>FileSvc: uploadFile(file, filename)
|
||||
FileSvc->>FileSvc: Generate key: documents/{UUID}_{filename}
|
||||
FileSvc->>MinIO: PutObject(bucket, key, stream)
|
||||
MinIO-->>FileSvc: Success
|
||||
FileSvc-->>DocSvc: S3 key
|
||||
DocSvc->>DB: UPDATE documents SET file_path=?, status='UPLOADED', ...
|
||||
DB-->>DocSvc: OK
|
||||
DocSvc-->>Backend: Updated Document entity
|
||||
Backend-->>Frontend: 200 OK — Document JSON
|
||||
Frontend-->>User: Refreshed document view
|
||||
```
|
||||
Reference in New Issue
Block a user