docs: restructure server and web documentation into ADR files and a

routing table
This commit is contained in:
2026-07-17 22:07:55 +08:00
parent f8494a44ca
commit 519aa35f1f
23 changed files with 710 additions and 654 deletions
+81 -101
View File
@@ -1,106 +1,86 @@
# Architecture
# Server Architecture
## Layered Design
## Request Flow
```
Handler (Gin handlers) ← translates HTTP ↔ Service calls
Service (business logic) ← orchestrates, authorizes, validates
↓ ↓
Repository (GORM data access) Storage (file I/O)
↓ ↓
[SQLite / PostgreSQL] [Local FS / S3]
```text
HTTP request
-> middleware
-> handler
-> service
-> repository and storage
```
Rules:
- Handler has no business logic — parse request, call service, write response.
- Handler and middleware never access repositories directly; they depend on services.
- Service has no HTTP awareness — operates on domain models and interfaces.
## Layer Responsibilities
| Layer | Packages | Responsibility |
|-------|----------|----------------|
| CLI | `cmd` | Register Cobra commands and start the application |
| Composition | `internal/app` | Open runtime resources and construct services |
| HTTP | `internal/server`, `internal/handler`, `internal/middleware`, `internal/api` | Route requests, authenticate principals, map DTOs, and write HTTP responses |
| Business | `internal/service`, `internal/model` | Validate operations, enforce authorization, and represent domain results and errors |
| Data | `internal/repository`, `internal/storage` | Persist metadata and file content |
| Infrastructure | `internal/config`, `internal/logging`, `internal/auth` | Load configuration, create loggers, and provide token and password primitives |
| Test support | `internal/testutil` | Prepare privileged fixtures through test-only helpers |
Handlers define REST request and response DTOs. `internal/api` maps domain error kinds to HTTP status codes and error bodies.
Services use protocol-neutral domain types and errors. Each service constructor accepts the repository capabilities required by its use cases.
Repository query capabilities return domain records. Mutation capabilities use operation-specific parameter types, select writable columns explicitly, and apply the ownership and lifecycle predicates of the operation.
Storage backends manage staged and permanent file content. Repository records hold storage paths and file metadata.
## Runtime Composition
`app.Bootstrap` opens the configured database, runs migrations, creates repositories and local storage, constructs the services, and returns `app.WebApp`.
`server.NewRouter` creates a Gin engine and registers the public and protected route groups. `cmd/serve.go` loads configuration, initializes logging, bootstraps the application, and runs graceful shutdown.
`app.WebApp` owns the database lifecycle and exposes the services, storage backend, configuration, and build metadata required by HTTP setup.
## HTTP Boundaries
Global middleware runs in this order:
1. Request ID
2. Gin access logger
3. Gin recovery
Protected routes use `AuthRequired` to authenticate an access token and attach a service principal. Administrator routes add `AdminRequired` to verify the principal's administrator flag.
| Route group | Access | Capability |
|-------------|--------|------------|
| `/api/v1/version` | Public | Build metadata |
| `/api/v1/auth` | Public | Registration, login, refresh, and logout |
| `/api/v1/account` | Authenticated | Account and application passkeys |
| `/api/v1/files` | Authenticated | File and directory operations |
| `/api/v1/admin/users` | Administrator | User listing, lookup, and deletion |
The route declarations in `internal/server/routes_public.go` and `internal/server/routes_protected.go` are the source of truth for individual methods and paths.
## Persistence Commands
| Area | Mutation contract |
|------|-------------------|
| Users | Registration accepts `RegisteredUserParams` and creates an active non-admin user. Administration exposes a separate deletion command. |
| Sessions | Refresh sessions are created, atomically consumed, revoked by token hash, or removed by maintenance operations. |
| Files | Upload, directory creation, metadata update, and soft deletion use distinct parameter types and commands. |
| Credentials | Application passkeys have explicit creation, usage-recording, and revocation operations. |
Metadata updates can write `name`, `parent_id`, and GORM-managed `updated_at`. File ownership and active-state predicates scope user mutations.
## Transaction Boundaries
File child creation, movement, and directory deletion use one hierarchy transaction protocol. PostgreSQL locks active owned source and parent rows in sorted ID order. SQLite adds `_txlock=immediate` to the configured DSN and reserves the write transaction before reading hierarchy state.
Refresh rotation locks, returns, and deletes the session in one transaction. A refresh session can issue one replacement token pair.
Uploads write to a staging path before promotion. The service promotes validated content before creating the active file record and attempts to remove promoted content when the database create fails.
## Enforced Architecture Constraints
`internal/architecture_test.go` enforces these boundaries:
- `internal/model` and `internal/service` do not import `net/http`, Gin, or `internal/api`.
- JSON response DTOs live in HTTP packages. Domain and service result structs are not the public REST schema.
- Domain models may use `json:"-"` for defensive redaction of sensitive fields; this is not a REST response contract.
- Domain errors carry a protocol-neutral kind, a safe message, and an optional cause. HTTP status mapping happens only at the API boundary.
- Repository query ports remain entity-oriented, while mutation ports expose business commands with operation-specific parameter types. Services never pass a mutable domain entity to a generic update or delete method.
- Service constructors using split repository capabilities accept the smallest capability needed by that service. Their composition-time unions do not cross into business services.
- Repository mutations use explicit columns and lifecycle/ownership predicates. Full-entity `Save` is prohibited so a write cannot accidentally change protected fields or revive a soft-deleted record.
- Repository abstracts the database; Storage abstracts where bytes live, including staged upload promotion.
- `internal/server` is the composition root — wires all dependencies together.
## Package Map
| Layer | Package | Purpose | Status |
|-------|---------|---------|--------|
| **CLI** | `cmd` | Cobra root command | 🛠 WIP |
| | `cmd/serve.go` | `mygo serve` — wire deps, start HTTP | ✅ |
| | `cmd/config.go` | `mygo config` — config subcommand | 🛠 WIP |
| | `cmd/status.go` | `mygo status` — health check | 🛠 WIP |
| **Config** | `internal/config` | Viper load (YAML + env + flags), typed Duration config via built-in decode hook | ✅ |
| **App** | `internal/app` | Runtime dependency container and build metadata | 🛠 WIP |
| **HTTP** | `internal/server` | Gin router init, route registration (public/protected split), graceful shutdown | ✅ |
| | `internal/handler` | HTTP handlers (auth, file, admin, webdav...) | 🛠 WIP |
| | `internal/middleware` | Gin middleware (logger, jwt, cors, auth) | 🛠 WIP |
| **Business** | `internal/service` | Business logic: `AuthService`, `FileService`, `AdminService`; protocol-neutral results and errors | ✅ |
| | `internal/model` | Domain types (User, File, Credential, Session), protocol-neutral error kinds | ✅ |
| **Data** | `internal/repository` | Repository interfaces + GORM implementations (User, Session, File, Credential) | ✅ |
| | `internal/storage` | Storage backend interface + local disk impl, with staging and promotion | 🛠 WIP |
| **Util** | `internal/auth` | JWT sign/verify (HS256), token type discrimination (access/refresh), password hashing (bcrypt), app passkey tokens | ✅ |
| | `internal/api` | Unified JSON error response helpers and REST error-kind mapping | ✅ |
| | `internal/testutil` | Test-only fixture helpers for privileged setup that production service ports intentionally do not expose | ✅ |
## Persistence Write Boundaries
- User registration accepts `RegisteredUserParams`; the repository fixes new users to non-admin, active state. Administrative deletion is a separate capability.
- File creation, directory creation, metadata changes, and soft deletion use distinct commands. Metadata updates can only write `name`, `parent_id`, and GORM-maintained `updated_at`.
- Refresh sessions are created, atomically consumed, idempotently revoked, or removed by explicit maintenance operations. Refresh token rotation consumes a session at most once.
- Passkey creation fixes the credential type to the app-passkey type. Usage recording and revocation include credential-type and ownership conditions where applicable.
- `app.WebApp` contains runtime services, storage, database lifecycle, configuration, and metadata. Repository implementations remain local to bootstrap and are injected directly into the services that require them.
## File Hierarchy Transactions
File child creation, movement, and directory deletion share one parent-locking protocol. PostgreSQL locks active owned source and parent rows in sorted ID order with `SELECT ... FOR UPDATE`; directory deletion checks active children while holding the directory lock. SQLite connections add `_txlock=immediate` to the configured DSN so the transaction acquires its write reservation before reading hierarchy state. These rules prevent a concurrent mutation from creating an active child below a deleted directory.
## API Routes (v0)
```
GET /api/v1/version
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
GET /api/v1/account
GET /api/v1/account/passkeys
POST /api/v1/account/passkeys
DELETE /api/v1/account/passkeys/:id
GET /api/v1/files
POST /api/v1/files # JSON creates directories; multipart uploads files. File upload parent_id is a query parameter.
GET /api/v1/files/:id
GET /api/v1/files/:id/content
PUT /api/v1/files/:id
DELETE /api/v1/files/:id
GET /api/v1/admin/users
GET /api/v1/admin/users/:id
PUT /api/v1/admin/users/:id
DELETE /api/v1/admin/users/:id
```
## Middleware Chain
Applied globally by `gin.Default()`: logger → recovery
Planned globally: cors
Applied to protected groups: auth (extract bearer token, delegate access-token authentication to `AuthService`, inject principal into gin.Context)
Applied to admin groups: admin (check authenticated principal from context)
## Server Responsibilities
- `cmd/serve.go` loads config, calls `app.Bootstrap` to initialize DB + services, builds the router, and starts the HTTP server.
- `app.WebApp` carries runtime dependencies and build metadata needed to assemble handlers.
- `internal/server` owns Gin router setup (`router.go`), route registration split into `routes_public.go` (public auth) and `routes_protected.go` (JWT-protected account).
- Each route group creates its own handler instance: `routes_public.go` creates `AuthHandler`, `routes_protected.go` creates `AccountHandler` — no shared handler state between public and protected routes.
- `RunWithGracefulShutdown` stops accepting new requests on termination and gives in-flight requests time to finish.
- `internal/handler` and `internal/middleware` do not import `internal/repository`.
- Repository implementations do not call GORM `Save`; mutation methods select writable columns explicitly.