feat(todo-api): add full-stack POC demonstrating micro-lib v0.9.0

Runnable REST API exercising every micro-lib tier in a containerless setup: N-layer architecture, SQLite persistence, header-based auth simulating Firebase output, and bit-mask RBAC enforcement.

What's included:
- cmd/todo-api: minimal main delegating to application.Run
- internal/application: full object graph wiring — launcher, sqlite, httpserver, httpmw stack, routes in BeforeStart
- internal/domain: User entity, ResourceTodos constant, PermReadTodo/PermWriteTodo bit positions
- internal/repository: TodoRepository, UserRepository, DBPermissionProvider (SQLite via modernc)
- internal/service: TodoService, UserService with interface-based dependencies
- internal/handler: TodoHandler, UserHandler using httputil adapters and valid for input validation
- internal/middleware: Auth (X-User-ID → rbac.Identity) and Require (bit-mask permission gate)
- logAdapter: bridges logz.Logger.With return type to httpmw.Logger interface
- SQLite schema: users, user_role (bitmask), todos; migrations run in BeforeStart
- Routes: POST /users (open), GET+POST /todos (RBAC), GET /users (RBAC)

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
This commit is contained in:
2026-03-19 13:55:08 +00:00
commit 3fcba82448
23 changed files with 1143 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
# ADR-002: Header-Based Auth for POC
**Status:** Accepted
**Date:** 2026-03-18
## Context
The todo-api needs authentication so it can demonstrate the RBAC permission system. In production this role would be played by Firebase Auth: the client presents a Firebase ID token, the `httpauth-firebase` middleware (from `httpmw`) verifies the JWT, and an `rbac.Identity` is placed in the request context.
For this POC, introducing Firebase Auth would require:
- A Firebase project or emulator
- Token issuance in tests and curl examples
- JWT verification dependencies in the application
This overhead is not necessary to demonstrate the RBAC flow. The goal is to show the contract between auth middleware and the rest of the stack, not to validate the auth mechanism itself.
## Decision
Authentication is performed by reading the `X-User-ID` request header. The `middleware.Auth` middleware:
1. Reads `r.Header.Get("X-User-ID")`.
2. Returns 401 if the header is absent.
3. Calls `userRepo.FindByID(ctx, uid)` to verify the user exists in the database.
4. Returns 401 if the user is not found (the two failure cases are intentionally indistinguishable to callers — no enumeration of which check failed).
5. Constructs an `rbac.Identity` via `rbac.NewIdentity(user.ID, user.Name, user.Email)` and stores it in the context with `rbac.SetInContext`.
The output contract — an `rbac.Identity` in the request context — is identical to what `httpauth-firebase` would produce. The downstream `middleware.Require` RBAC guard consumes `rbac.Identity` from context and has no knowledge of how the identity was established.
The `POST /users` route is deliberately unguarded. It allows bootstrapping: a new deployment can create its first users (with permission bits set) without prior credentials.
## Consequences
- Replacing header auth with Firebase auth in a production version requires only swapping the `Auth` middleware. All downstream code (`Require`, handlers, services, repositories) is unchanged.
- The header-based approach is intentionally insecure and must not be used in production. This is a POC.
- The 401 response does not distinguish "missing header" from "user not found" — this is intentional to avoid user enumeration.
- `POST /users` is an open endpoint. In a production system this would be protected by Firebase auth or an admin credential. Here it serves as the bootstrap mechanism to populate the `users` table and assign permission bits.
- The `logAdapter` in `application` bridges `logz.Logger.With(...)` (which returns `logz.Logger`) to `httpmw.Logger.With(...)` (which must return `httpmw.Logger`). This mismatch arises because `httpmw.Logger` is a duck-typed interface with a `With` method that must return `httpmw.Logger`, not a concrete type. The adapter is defined locally in `application` — no extra package needed.