274 lines
16 KiB
Markdown
274 lines
16 KiB
Markdown
# Familienarchiv — C4 Architecture Diagrams
|
|
|
|
> For domain terminology used in these diagrams, see [docs/GLOSSARY.md](../GLOSSARY.md).
|
|
|
|
## 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, transcription, OCR orchestration, and SSE notifications.")
|
|
|
|
Container(ocr, "OCR Service", "Python FastAPI / port 8000", "Handwritten text recognition (HTR) and OCR microservice. Single-node by design — see ADR-001. Reachable only on the internal Docker network; no external port exposed.")
|
|
|
|
ContainerDb(db, "Relational Database", "PostgreSQL 16", "Stores document metadata, persons, users, permission groups, tags, transcription blocks, audit log, 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, user, "SSE notifications (server-sent events)", "HTTP / SSE — direct backend-to-browser")
|
|
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(backend, ocr, "OCR job requests with presigned MinIO URL", "HTTP / REST / JSON")
|
|
Rel(ocr, storage, "Fetches PDF via presigned URL", "HTTP / S3 presigned")
|
|
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
|
|
```
|