docs: restructure server and web documentation into ADR files and a
routing table
This commit is contained in:
+81
-101
@@ -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.
|
||||
|
||||
+11
-146
@@ -1,148 +1,13 @@
|
||||
# Technical Decisions
|
||||
# Server Decisions
|
||||
|
||||
## 2026-04-25: v0 Tech Stack & Architecture
|
||||
Read the ADRs relevant to the current change. Accepted records define active decisions. Superseded records preserve history and link to current guidance.
|
||||
|
||||
**Context**: Project skeleton was created with only cobra CLI. We needed a concrete tech stack and package layout to begin implementation.
|
||||
|
||||
**Decisions**:
|
||||
|
||||
| Area | Choice | Rationale |
|
||||
|------|--------|-----------|
|
||||
| HTTP framework | Gin | Most widely adopted Go web framework, mature middleware ecosystem |
|
||||
| ORM | GORM | SQLite-first dev, PostgreSQL option later; GORM abstracts dialect differences |
|
||||
| Config management | Viper | YAML + env vars + CLI flags three-way merge, built for cobra integration |
|
||||
| Database | SQLite (v0) → PostgreSQL (future) | SQLite zero setup for dev; repo interface isolates the switch |
|
||||
| File storage | Local disk (v0) → S3 (future) | Backend interface (`internal/storage`) hides implementation |
|
||||
| File identity | UUID | Distributed-friendly, no coordination needed; cost is negligible for file metadata |
|
||||
| Token strategy | JWT, refresh token stored in DB | Enables server-side revocation (admin kick, logout-all-devices) |
|
||||
| Pagination | OFFSET/LIMIT | Simple, sufficient for v0; migrate to cursor-based if needed |
|
||||
| API response format | Direct JSON success bodies + unified error body | HTTP status codes carry request outcome; error body carries human-readable details |
|
||||
|
||||
**Architecture**: Four-layer model — Handler (Gin) → Service (business logic) → Repository (GORM data access) + Storage (file I/O). Each layer depends only on interfaces of the layer below.
|
||||
|
||||
**Consequences**:
|
||||
- Handler layer has no business logic; Service layer is reusable across REST API, WebDAV, and future Nextcloud API.
|
||||
- Repository interfaces keep DB swappable; future PostgreSQL implementation only needs a new package.
|
||||
- Refresh token in DB adds a `sessions` table and a `repository.SessionRepository` interface.
|
||||
- UUID dependency: `github.com/google/uuid` to be added.
|
||||
- Gin middleware chain: default logger/recovery → cors → auth (route-group-scoped).
|
||||
|
||||
## 2026-04-27: Web API Foundation
|
||||
|
||||
**Context**: The project needed the first HTTP slice that can validate Gin wiring and provide a stable shape for future auth, file, and admin APIs.
|
||||
|
||||
**Decisions**:
|
||||
|
||||
| Area | Choice | Guidance |
|
||||
|------|--------|----------|
|
||||
| API versioning | All REST routes under `/api/v1` | Keep future REST handlers under the versioned group. |
|
||||
| Initial public endpoint | `GET /api/v1/version` | Returns build metadata only; health/readiness endpoints need a separate security review. |
|
||||
| Success responses | Direct JSON resource bodies | Use HTTP status codes as the request outcome signal. |
|
||||
| Error responses | `{"error":{"message":"..."}}` | Add machine-readable error codes only when clients need stable branching behavior. |
|
||||
| App composition | `internal/app.WebApp` | `cmd/serve.go` creates the app from config and build metadata, then passes it to router setup. |
|
||||
| Router setup | `internal/server.NewRouter(*app.WebApp)` | Public routes (`routes_public.go`) and protected routes (`routes_protected.go`) split by auth boundary; `WebApp` serves as the unified dependency container. |
|
||||
| Server lifecycle | `RunWithGracefulShutdown` | Preserve graceful shutdown while keeping command startup linear. |
|
||||
| Default middleware | `gin.Default()` | Use default logger/recovery for the skeleton; add CORS/auth explicitly when their policies exist. |
|
||||
|
||||
**Consequences**:
|
||||
- Version is build metadata from `internal/app/version.go`, not a config-file field.
|
||||
- `app.WebApp` is the place to add future runtime services, storage, and app metadata incrementally; repository implementations stay inside composition setup.
|
||||
- Request ID middleware is not part of the current foundation; add it only with a logging/tracing/error-correlation design.
|
||||
|
||||
## 2026-04-29: Auth Refinements
|
||||
|
||||
**Context**: Auth layer had three structural weaknesses — handler duplication, indistinguishable token types, and fragile config duration parsing.
|
||||
|
||||
**Decisions**:
|
||||
|
||||
| Decision | Guidance |
|
||||
|----------|----------|
|
||||
| One handler per route group | `AuthHandler` owns `/auth/*` (public); `AccountHandler` owns `/account/*` (protected). A route group maps 1:1 to a handler type. |
|
||||
| JWT `type` claim | `Claims.Type` distinguishes access from refresh tokens. Middleware and service enforce the correct type at their respective boundaries. `ParseToken` does no type check — it verifies cryptographic validity only. |
|
||||
| Default JWT secret hardening | The development placeholder `jwt.secret` is replaced with an ephemeral runtime secret during config loading. Production and multi-instance deployments must set a stable secret. |
|
||||
| `time.Duration` in config structs | Config fields representing durations use `time.Duration` directly. Viper's built-in `StringToTimeDurationHookFunc` handles string→Duration conversion at unmarshal time. No accessor methods, no runtime parsing. Invalid values fail at startup via `Load()`. |
|
||||
|
||||
**Consequences**:
|
||||
- Handlers are independently extensible (caching, rate limiting scoped per handler).
|
||||
- Refresh tokens cannot authenticate API requests; access tokens cannot be used to issue new token pairs.
|
||||
- The placeholder JWT secret is safe for local startup, but tokens signed with it are invalidated on restart.
|
||||
- New duration config fields require zero boilerplate — declare as `time.Duration` in the struct.
|
||||
|
||||
## 2026-07-05: Staged File Uploads
|
||||
|
||||
**Context**: Multipart uploads previously used `ParseMultipartForm`, which parses the request before service-level size checks and may spill oversized requests to temporary disk. The file service also wrote directly to the long-term storage path and then attempted compensating deletes on upload failure.
|
||||
|
||||
**Decisions**:
|
||||
|
||||
| Decision | Guidance |
|
||||
|----------|----------|
|
||||
| Stream multipart requests | Handlers use `Request.MultipartReader()` instead of `ParseMultipartForm`/`FormFile`, so uploads are streamed into the service. |
|
||||
| Optional upload limits | `storage.max_upload_size = 0` means unlimited. Positive values enable both HTTP body limiting and service-level file content limiting. |
|
||||
| Staging before promotion | Storage backends write upload bytes to a staging path first, then promote the object to the long-term data path only after validation succeeds. |
|
||||
| Promote before DB create | The service promotes the object before creating the active file record, preventing visible DB rows from pointing at missing objects. If DB creation fails after promotion, the service best-effort deletes the promoted object. |
|
||||
| Upload parent location | Multipart upload `parent_id` is passed as a query parameter, keeping the multipart body focused on the file stream. |
|
||||
| Service-layer upload errors | HTTP transport errors are converted at the handler boundary; the file service only handles domain errors such as oversized uploads. |
|
||||
|
||||
**Consequences**:
|
||||
- Interrupted, malformed, and oversized uploads leave only staging objects, which are safe to clean by path prefix and age.
|
||||
- Local storage promotes with `os.Rename` and falls back to copy/delete on cross-device `EXDEV`; future S3 storage can implement promotion with copy/delete while keeping business visibility controlled by the DB row.
|
||||
- A DB failure after promotion can still leave a long-term orphan object, but it is not visible through the file API and can be cleaned independently.
|
||||
|
||||
## 2026-07-05: Protocol-Neutral Service Boundary
|
||||
|
||||
**Context**: The service and model layers carried HTTP status codes and JSON response tags, and some middleware and handlers accessed repositories directly. That made the business layer harder to reuse for future WebDAV and Nextcloud-compatible APIs.
|
||||
|
||||
**Decisions**:
|
||||
|
||||
| Decision | Guidance |
|
||||
|----------|----------|
|
||||
| Protocol-neutral errors | `model.AppError` uses an error `Kind`, safe message, and optional cause. REST status codes are mapped in `internal/api` only. |
|
||||
| API log references | REST error responses may include `error.log_id` for server-recorded errors: all 5xx responses and 4xx responses with internal causes. |
|
||||
| Service boundaries | Handlers and middleware depend on services, not repositories. `AuthService` authenticates access tokens into a principal; `AdminService` owns admin user operations. |
|
||||
| DTO ownership | Service and model structs are internal/domain data. HTTP handlers assemble response DTOs with JSON tags. |
|
||||
| Architecture enforcement | Package-level tests reject HTTP imports in model/service and repository imports in handlers/middleware. Targeted model tests verify defensive JSON redaction for sensitive fields. |
|
||||
|
||||
**Consequences**:
|
||||
- REST remains a handler/API concern, and future protocols can reuse services without HTTP leakage.
|
||||
- Error responses keep the same top-level shape, with optional `log_id` instead of embedding log references in the message.
|
||||
- Admin and auth middleware behavior is testable through service contracts rather than database access.
|
||||
|
||||
## 2026-07-15: Conceal File Resource Existence
|
||||
|
||||
**Context**: Returning permission-denied errors for cross-user file access allowed authenticated callers to distinguish another user's resource from a missing resource. Idempotent success for deleting missing files also conflicted with a consistent not-found concealment policy.
|
||||
|
||||
**Decisions**:
|
||||
|
||||
| Decision | Guidance |
|
||||
|----------|----------|
|
||||
| Conceal ownership | File and parent-directory ownership failures return the same not-found kind and safe message as missing or deleted resources. |
|
||||
| Strict delete result | Deleting an active owned file returns success once; missing, deleted, and cross-user targets return not found. |
|
||||
| Atomic soft delete | Repository deletes update only active rows and return `model.ErrNotFound` when no row changes. |
|
||||
|
||||
**Consequences**:
|
||||
- Authenticated file operations cannot use status codes or messages to determine whether another user's resource exists.
|
||||
- A successful first delete returns `204 No Content`; repeated deletes return `404 Not Found` while remaining idempotent in effect.
|
||||
- Unauthenticated requests remain `401 Unauthorized`, non-empty directory deletion remains `409 Conflict`, and permission-denied behavior outside the file boundary remains unchanged.
|
||||
|
||||
## 2026-07-16: Command-Scoped Persistence Mutations
|
||||
|
||||
**Context**: Generic `Repository.Update(*Entity)`, full-entity GORM `Save`, and broad delete methods exposed fields and lifecycle transitions that individual services were not authorized to perform. They also allowed a stale file entity to revive a soft-deleted row, left file parent checks vulnerable to concurrent deletion, and allowed two refresh requests to consume the same session.
|
||||
|
||||
**Decisions**:
|
||||
|
||||
| Decision | Guidance |
|
||||
|----------|----------|
|
||||
| Command-scoped writes | Every mutation has a use-case-specific method and parameter type. Mutable domain entities do not cross a service-to-repository write boundary. |
|
||||
| Capability interfaces | Auth and admin services accept only the query and command capabilities they require. Wider user and session interfaces exist only for construction and repository-focused consumers. |
|
||||
| Explicit mutation predicates | Updates name concrete columns and include ownership, credential type, and/or active-state predicates appropriate to the command. A zero-row guarded mutation maps to a domain not-found result. |
|
||||
| Transactional file hierarchy | Child creation, movement, and directory deletion revalidate hierarchy state in the same transaction. PostgreSQL locks relevant rows in sorted ID order; SQLite DSNs force `_txlock=immediate`. |
|
||||
| Atomic refresh consumption | Refresh rotates through one transaction that locks, returns, and deletes the session. A concurrent replay cannot issue a second token pair. Logout remains an idempotent revoke-by-hash command. |
|
||||
| Test-only privilege setup | Tests elevate users through `internal/testutil`; no production role mutation is added merely to prepare fixtures. |
|
||||
|
||||
**Consequences**:
|
||||
|
||||
- Protected user, file, session, and credential fields are absent from ordinary mutation inputs, reducing illegal write paths at compile time.
|
||||
- Deleted file rows cannot be revived by stale metadata updates, and successful hierarchy races cannot leave active orphan children.
|
||||
- PostgreSQL relies on row-level locking while SQLite serializes these hierarchy and session write transactions from their first database operation. This favors correctness over concurrent SQLite writers.
|
||||
- Adding a future profile, password, role, restore, or other mutation requires a named command and an explicit capability rather than extending a universal update method.
|
||||
- REST routes, payloads, status codes, database schema, and dependency set remain unchanged.
|
||||
| ADR | Date | Status | Decision |
|
||||
|-----|------|--------|----------|
|
||||
| [ADR-0001](decisions/0001-server-foundation.md) | 2026-04-25 | Accepted | Select the server foundation |
|
||||
| [ADR-0002](decisions/0002-rest-api-foundation.md) | 2026-04-27 | Accepted | Establish the REST API foundation |
|
||||
| [ADR-0003](decisions/0003-auth-contracts.md) | 2026-04-29 | Accepted | Define authentication contracts |
|
||||
| [ADR-0004](decisions/0004-staged-file-uploads.md) | 2026-07-05 | Accepted | Stage uploads before publication |
|
||||
| [ADR-0005](decisions/0005-protocol-neutral-services.md) | 2026-07-05 | Accepted | Keep service contracts protocol-neutral |
|
||||
| [ADR-0006](decisions/0006-conceal-file-resources.md) | 2026-07-15 | Accepted | Conceal file ownership boundaries |
|
||||
| [ADR-0007](decisions/0007-command-scoped-persistence.md) | 2026-07-16 | Accepted | Use command-scoped persistence mutations |
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-0001: Select the Server Foundation
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-04-25
|
||||
|
||||
## Context
|
||||
|
||||
MyGO needed a server stack that supports local development, relational metadata, configurable deployment, and multiple file storage backends.
|
||||
|
||||
## Decision
|
||||
|
||||
- Use Gin for HTTP routing and middleware.
|
||||
- Use GORM with SQLite and PostgreSQL drivers.
|
||||
- Use Viper for YAML and environment configuration, with a Cobra flag for the config file path.
|
||||
- Use local file storage first and expose storage through `internal/storage`.
|
||||
- Use UUIDs for file and domain identities.
|
||||
- Use JWT access tokens and database-backed refresh sessions.
|
||||
- Use offset and limit pagination for the initial API.
|
||||
- Return direct JSON success bodies and a shared JSON error shape.
|
||||
- Organize the server as Handler -> Service -> Repository and Storage.
|
||||
|
||||
## Consequences
|
||||
|
||||
- SQLite supports a zero-service local setup; PostgreSQL supports multi-instance deployments.
|
||||
- Repository and storage interfaces isolate infrastructure choices from services.
|
||||
- Database-backed refresh sessions support rotation and revocation.
|
||||
- Offset pagination remains suitable for the current dataset size and API scope.
|
||||
@@ -0,0 +1,24 @@
|
||||
# ADR-0002: Establish the REST API Foundation
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-04-27
|
||||
|
||||
## Context
|
||||
|
||||
The first HTTP slice needed stable versioning, response conventions, dependency composition, and server lifecycle behavior.
|
||||
|
||||
## Decision
|
||||
|
||||
- Place REST routes under `/api/v1`.
|
||||
- Expose build metadata through `GET /api/v1/version`.
|
||||
- Return resource bodies directly for successful requests.
|
||||
- Return errors as `{"error":{"message":"..."}}` with optional diagnostic fields.
|
||||
- Use `internal/app.Bootstrap` to construct runtime dependencies and return `app.WebApp`.
|
||||
- Register public and protected routes in separate server functions.
|
||||
- Stop accepting new requests during shutdown and allow in-flight requests to finish.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Future REST endpoints share one version boundary and response convention.
|
||||
- `app.WebApp` provides the services and metadata required by route setup.
|
||||
- HTTP lifecycle behavior stays in `internal/server`.
|
||||
@@ -0,0 +1,24 @@
|
||||
# ADR-0003: Define Authentication Contracts
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-04-29
|
||||
|
||||
## Context
|
||||
|
||||
Authentication requires distinct access and refresh behavior, stable duration parsing, and safe development defaults.
|
||||
|
||||
## Decision
|
||||
|
||||
- Use `AuthHandler` for public authentication routes and `AccountHandler` for authenticated account routes.
|
||||
- Add a JWT `type` claim that distinguishes access and refresh tokens.
|
||||
- Verify token type at the boundary that consumes the token.
|
||||
- Store configuration durations as `time.Duration` values.
|
||||
- Replace known development JWT placeholders with a random runtime secret during configuration loading.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Access tokens authenticate protected requests.
|
||||
- Refresh tokens rotate token pairs through database sessions.
|
||||
- Invalid duration values fail during startup.
|
||||
- Tokens signed with the generated development secret expire when the process restarts.
|
||||
- Multi-instance deployments require a stable configured secret.
|
||||
@@ -0,0 +1,24 @@
|
||||
# ADR-0004: Stage Uploads Before Publication
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-07-05
|
||||
|
||||
## Context
|
||||
|
||||
File uploads need streaming size enforcement and a clear publication boundary between stored content and visible metadata.
|
||||
|
||||
## Decision
|
||||
|
||||
- Stream multipart bodies with `Request.MultipartReader`.
|
||||
- Interpret `storage.max_upload_size = 0` as unlimited and positive values as a byte limit.
|
||||
- Write upload content to a staging path.
|
||||
- Validate and promote staged content before creating the active database record.
|
||||
- Pass multipart `parent_id` as a query parameter.
|
||||
- Convert HTTP body errors in the handler and domain upload errors in the service.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Upload size checks apply while the request streams.
|
||||
- Interrupted and rejected uploads remain in the staging namespace.
|
||||
- Local storage promotes with rename and uses copy and delete across filesystems.
|
||||
- A failed database create can leave a promoted object; the service attempts cleanup and the object remains invisible to the file API.
|
||||
@@ -0,0 +1,23 @@
|
||||
# ADR-0005: Keep Service Contracts Protocol-Neutral
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-07-05
|
||||
|
||||
## Context
|
||||
|
||||
MyGO exposes REST today and is designed to support additional protocols. Business services need contracts that transport adapters can reuse.
|
||||
|
||||
## Decision
|
||||
|
||||
- Services return domain results and `model.AppError` values.
|
||||
- HTTP handlers own REST response DTOs.
|
||||
- `internal/api` maps domain error kinds to REST responses.
|
||||
- HTTP packages obtain application behavior through service contracts and authenticated principals.
|
||||
- Architecture tests enforce the package dependency boundaries.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Additional protocol adapters can reuse the existing services.
|
||||
- Each protocol adapter defines its own response mapping.
|
||||
- Service tests exercise business behavior without constructing HTTP requests.
|
||||
- Error responses can include `error.log_id` for failures recorded by the server.
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR-0006: Conceal File Ownership Boundaries
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-07-15
|
||||
|
||||
## Context
|
||||
|
||||
Authenticated callers must not infer the existence of files owned by another user from status codes or response messages.
|
||||
|
||||
## Decision
|
||||
|
||||
- Map missing, deleted, and cross-user file targets to the same not-found result.
|
||||
- Apply the same rule to parent-directory lookups.
|
||||
- Return success for one deletion of an active owned file.
|
||||
- Return not found for repeated deletion and cross-user deletion.
|
||||
- Guard soft deletion with ownership and active-state predicates.
|
||||
|
||||
## Consequences
|
||||
|
||||
- File responses conceal resource ownership from other authenticated users.
|
||||
- The first successful deletion returns `204 No Content`; later attempts return `404 Not Found`.
|
||||
- Non-empty directory deletion continues to return a conflict.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-0007: Use Command-Scoped Persistence Mutations
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-07-16
|
||||
|
||||
## Context
|
||||
|
||||
Each business operation needs a persistence contract that exposes its writable fields, authorization predicates, and transaction boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
- Give each mutation a use-case-specific method and parameter type.
|
||||
- Inject the query and mutation capabilities required by each service.
|
||||
- Select mutation columns explicitly.
|
||||
- Include ownership, credential type, and active-state predicates where the operation requires them.
|
||||
- Revalidate file hierarchy state inside the mutation transaction.
|
||||
- Lock PostgreSQL hierarchy rows in sorted order.
|
||||
- Start SQLite write transactions with `_txlock=immediate`.
|
||||
- Consume a refresh session in one lock, read, and delete transaction.
|
||||
- Use `internal/testutil` for privileged fixture setup.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Mutation inputs expose the fields writable by the use case.
|
||||
- Guarded zero-row updates map to domain not-found results.
|
||||
- File hierarchy mutations preserve active parent-child relationships under concurrency.
|
||||
- One refresh session issues one replacement token pair.
|
||||
- SQLite serializes these write transactions from their first database operation.
|
||||
- Future mutations require a named command and an explicit capability.
|
||||
+55
-45
@@ -1,9 +1,13 @@
|
||||
# Development
|
||||
# Server Development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.26.2 (pinned in `mise.toml`)
|
||||
- `mise` (https://mise.jdx.dev) — run `mise install` to install toolchain
|
||||
- Go 1.26.2, pinned in `mise.toml`
|
||||
- `mise` for installing the pinned toolchain
|
||||
|
||||
```bash
|
||||
mise install
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
@@ -20,69 +24,75 @@ go test -v -run TestName ./internal/...
|
||||
go test -race ./internal/repository ./internal/service ./internal/server
|
||||
```
|
||||
|
||||
## Lint & Format
|
||||
## Format and Static Analysis
|
||||
|
||||
```bash
|
||||
go vet ./...
|
||||
go fmt ./...
|
||||
```
|
||||
|
||||
## Repository Review
|
||||
|
||||
Use the repository-scoped `$mygo-api-repo-review` skill for evidence-based architecture, design, security, resilience, data-consistency, performance, and implementation audits. Reviews are read-only unless fixes are requested separately.
|
||||
|
||||
Select one review mode:
|
||||
|
||||
- `diff` compares the current workspace, including relevant untracked files, with `HEAD`.
|
||||
- `main` compares the current workspace with the merge base of `HEAD` and the local `main` branch without fetching remote refs.
|
||||
- `full` reviews the current repository as a whole.
|
||||
|
||||
Optionally limit any mode to a target path, Go package, or logical component. For example:
|
||||
|
||||
```text
|
||||
Use $mygo-api-repo-review in main mode with target internal/storage.
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Run module cleanup after adding or removing imports:
|
||||
|
||||
```bash
|
||||
go mod tidy # after adding/removing imports
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
## Config
|
||||
## Repository Review
|
||||
|
||||
Server config is loaded via viper from `config.yaml` (defaults in `internal/config/load.go`).
|
||||
Use `$mygo-api-repo-review` for evidence-based architecture, design, security, resilience, data-consistency, performance, and implementation audits.
|
||||
|
||||
For SQLite, the repository preserves configured DSN parameters and adds
|
||||
`_txlock=immediate`. Write transactions therefore reserve the database before
|
||||
performing hierarchy or refresh-session reads, matching the locking assumptions
|
||||
used by the repository commands. Keep this behavior when supplying a SQLite URI
|
||||
such as `file:mygo.db?cache=shared`.
|
||||
| Mode | Scope |
|
||||
|------|-------|
|
||||
| `diff` | Workspace, including relevant untracked files, compared with `HEAD` |
|
||||
| `main` | Workspace compared with the merge base of `HEAD` and local `main` |
|
||||
| `full` | Complete repository |
|
||||
|
||||
Reviews are read-only unless the user also requests fixes. A mode can target a path, Go package, or logical component.
|
||||
|
||||
## Configuration
|
||||
|
||||
The server reads `config.yaml` from the working directory when present. Environment variables use the `MYGO_` prefix and underscores, for example `MYGO_SERVER_PORT` and `MYGO_JWT_SECRET`.
|
||||
|
||||
```yaml
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: 10086
|
||||
host: 0.0.0.0
|
||||
port: 10086
|
||||
|
||||
database:
|
||||
driver: sqlite3
|
||||
sqlite:
|
||||
path: data/mygo.db
|
||||
driver: sqlite3
|
||||
sqlite:
|
||||
path: data/mygo.db
|
||||
|
||||
storage:
|
||||
driver: local
|
||||
local:
|
||||
path: data/files
|
||||
driver: local
|
||||
max_upload_size: 0
|
||||
local:
|
||||
path: data/files
|
||||
|
||||
jwt:
|
||||
secret: dev-secret-do-not-use-in-production
|
||||
access_ttl: 15m
|
||||
refresh_ttl: 168h
|
||||
secret: dev-secret-do-not-use-in-production
|
||||
access_ttl: 15m
|
||||
refresh_ttl: 168h
|
||||
|
||||
log:
|
||||
level: info
|
||||
file_path: ""
|
||||
file_level: debug
|
||||
```
|
||||
|
||||
Environment variables use `MYGO_` prefix with underscore separators: `MYGO_SERVER_PORT=8080`, `MYGO_JWT_SECRET=...`
|
||||
| Setting | Behavior |
|
||||
|---------|----------|
|
||||
| `database.driver` | Selects `sqlite3` or `postgres` |
|
||||
| `storage.max_upload_size` | `0` allows any upload size; a positive byte count enables request and service limits |
|
||||
| `jwt.secret` | Signs access and refresh tokens |
|
||||
| `log.level` | Sets stderr logging to `debug`, `info`, `warn`, or `error` |
|
||||
| `log.file_path` | Enables an additional text log file when set |
|
||||
| `log.file_level` | Sets the file log level independently |
|
||||
|
||||
The default JWT secret is a development placeholder. At startup, MyGO replaces
|
||||
it with an ephemeral runtime secret; set a stable `jwt.secret` or
|
||||
`MYGO_JWT_SECRET` when tokens must survive restarts or when running multiple
|
||||
instances.
|
||||
PostgreSQL uses the `database.postgres` host, port, user, password, database name, and SSL mode fields defined in `internal/config`.
|
||||
|
||||
The SQLite connector preserves configured DSN parameters and sets `_txlock=immediate`. This setting establishes the transaction behavior required by hierarchy and refresh-session commands.
|
||||
|
||||
The default JWT secret is a development placeholder. Configuration loading replaces it with a random in-memory secret for the current process. Set a stable secret for multi-instance deployments and tokens that must survive a restart.
|
||||
|
||||
+22
-34
@@ -1,40 +1,28 @@
|
||||
# Roadmap
|
||||
# Server Roadmap
|
||||
|
||||
## v0
|
||||
## Available Capabilities
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| CLI config management | ✅ | Viper YAML + env + flags, typed Duration config |
|
||||
| JWT authentication | ✅ | access + refresh tokens, atomic single-use refresh sessions, app passkey support |
|
||||
| Web API foundation | ✅ | WebApp composition, Gin router, graceful shutdown, `GET /api/v1/version` |
|
||||
| File upload/download/manage APIs | 🛠 WIP | REST API via Gin |
|
||||
| Admin endpoints | 🛠 WIP | user service boundary in place for superusers |
|
||||
| WebDAV | 🛠 WIP | future v0 or v1 |
|
||||
- YAML and environment configuration loading
|
||||
- SQLite and PostgreSQL metadata persistence
|
||||
- JWT access and single-use refresh tokens
|
||||
- Account and application-passkey APIs
|
||||
- File listing, upload, download, directory creation, metadata update, and soft deletion
|
||||
- Ownership concealment for file resources
|
||||
- Administrator user listing, lookup, and deletion
|
||||
- Local storage with staged upload promotion
|
||||
- Request IDs, structured logging, and graceful shutdown
|
||||
- Architecture, repository, service, handler, and route integration tests
|
||||
|
||||
## Implementation Tasks
|
||||
## Planned v0 Work
|
||||
|
||||
Package-level implementation order (each task includes unit tests):
|
||||
- Add `mygo config` for instance configuration management.
|
||||
- Add `mygo status` for server status inspection.
|
||||
- Complete the remaining administrator workflows.
|
||||
- Define the WebDAV delivery milestone and acceptance criteria.
|
||||
|
||||
1. `internal/config` — Viper loader, config struct ✅
|
||||
2. `internal/app` — runtime dependency container ✅
|
||||
3. `internal/model` — domain types, error codes ✅
|
||||
4. `internal/api` — protocol-neutral error kind to REST response mapping ✅
|
||||
5. `internal/auth` — JWT utils ✅
|
||||
6. `internal/storage` — backend interface + local fs with staged upload promotion
|
||||
7. `internal/repository` — command-scoped mutation capabilities + GORM/SQLite transaction protocol ✅
|
||||
8. `internal/service` — auth, file, admin services ✅
|
||||
9. `internal/middleware` — logger, cors, auth ✅ (auth done; principal boundary enforced)
|
||||
10. `internal/handler` — auth, account, file, admin handlers 🛠 (HTTP DTO mapping in place)
|
||||
11. `internal/server` — Gin router, route registration, graceful shutdown ✅
|
||||
12. `cmd/serve.go`, `cmd/config.go`, `cmd/status.go` ✅ (serve done)
|
||||
13. Integration tests 🛠 (authenticated register → login → upload → list → download route flow covered with a small file; authentication bypass, invalid token, concealed cross-user resources, strict delete, atomic refresh consumption, and file hierarchy race boundaries covered)
|
||||
14. Architecture boundary tests ✅
|
||||
## Future Candidates
|
||||
|
||||
## Future
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Image server | ⬜ plan | thumbnail generation |
|
||||
| Pastebin & code snippets | ⬜ plan | in sharing context |
|
||||
| S3 storage backend | ⬜ plan | new storage impl |
|
||||
| Nextcloud-compatible API | ⬜ plan | new handler layer on existing services |
|
||||
- S3 storage
|
||||
- Image thumbnails
|
||||
- Paste and code-snippet sharing
|
||||
- Nextcloud-compatible protocol adapters
|
||||
|
||||
Reference in New Issue
Block a user