Add implementation plan for auth and encrypted URLs feature

This commit is contained in:
2026-01-08 07:39:31 -08:00
parent 6355886dba
commit f601e17812

113
TODO.md
View File

@@ -2,6 +2,119 @@
A single linear checklist of tasks to implement the complete pixa caching image reverse proxy server.
## Login, Sessions & Encrypted URLs Feature
### Phase 1: Crypto Foundation
- [ ] Create `internal/crypto/crypto.go` with:
- [ ] `DeriveKey(masterKey []byte, salt string) ([32]byte, error)` - HKDF-SHA256 key derivation
- [ ] `Encrypt(key [32]byte, plaintext []byte) (string, error)` - NaCl secretbox encrypt, returns base64url
- [ ] `Decrypt(key [32]byte, ciphertext string) ([]byte, error)` - NaCl secretbox decrypt from base64url
- [ ] Create `internal/crypto/crypto_test.go` with tests for:
- [ ] Key derivation produces consistent keys for same input
- [ ] Encrypt/decrypt round-trip
- [ ] Decryption fails with wrong key
- [ ] Decryption fails with tampered ciphertext
### Phase 2: Session Management
- [ ] Add `github.com/gorilla/securecookie` dependency
- [ ] Create `internal/session/session.go` with:
- [ ] `SessionData` struct: `Authenticated bool`, `CreatedAt time.Time`, `ExpiresAt time.Time`
- [ ] `Manager` struct using gorilla/securecookie with keys derived via HKDF
- [ ] `NewManager(signingKey string, secure bool) (*Manager, error)`
- [ ] `CreateSession(w http.ResponseWriter) error` - creates 30-day encrypted cookie
- [ ] `ValidateSession(r *http.Request) (*SessionData, error)` - decrypts and validates cookie
- [ ] `ClearSession(w http.ResponseWriter)` - clears cookie (logout)
- [ ] `IsAuthenticated(r *http.Request) bool` - convenience wrapper
- [ ] Cookie settings: `HttpOnly`, `Secure` (prod), `SameSite=Strict`, name `pixa_session`
- [ ] Create `internal/session/session_test.go` with tests for:
- [ ] Session creation and validation round-trip
- [ ] Expired session rejection
- [ ] Tampered cookie rejection
- [ ] Missing cookie returns unauthenticated
### Phase 3: Encrypted URL Generation
- [ ] Add `github.com/fxamacker/cbor/v2` dependency for compact binary encoding
- [ ] Create `internal/encurl/encurl.go` with:
- [ ] `EncryptedPayload` struct (short CBOR field names, omitempty for fields with sane defaults):
- `SourceHost string` (`cbor:"h"`) - required
- `SourcePath string` (`cbor:"p"`) - required
- `SourceQuery string` (`cbor:"q,omitempty"`) - optional
- `Width int` (`cbor:"w,omitempty"`) - 0 = original
- `Height int` (`cbor:"ht,omitempty"`) - 0 = original
- `Format ImageFormat` (`cbor:"f,omitempty"`) - default: orig
- `Quality int` (`cbor:"ql,omitempty"`) - default: 85
- `FitMode FitMode` (`cbor:"fm,omitempty"`) - default: cover
- `ExpiresAt int64` (`cbor:"e"`) - required, Unix timestamp
- [ ] `Generator` struct with URL key derived via HKDF (salt: `"pixa-urlenc-v1"`)
- [ ] `NewGenerator(signingKey string) (*Generator, error)`
- [ ] `Generate(p *EncryptedPayload) (string, error)` - CBOR encode, encrypt, base64url
- [ ] `Parse(token string) (*EncryptedPayload, error)` - base64url decode, decrypt, CBOR decode, validate expiration
- [ ] `(p *EncryptedPayload) ToImageRequest() *imgcache.ImageRequest`
- [ ] Create `internal/encurl/encurl_test.go` with tests for:
- [ ] Generate/parse round-trip preserves all fields
- [ ] Expired URL returns `ErrExpired`
- [ ] Malformed token returns error
- [ ] Tampered token fails decryption
### Phase 4: HTML Templates
- [ ] Create `templates/templates.go` with:
- [ ] `//go:embed *.html` for embedded templates
- [ ] `GetParsed() *template.Template` function
- [ ] Create `templates/login.html`:
- [ ] Simple form with password input for signing key
- [ ] POST to `/`
- [ ] Error message display area
- [ ] Minimal inline CSS
- [ ] Create `templates/generator.html`:
- [ ] Logout link at top
- [ ] Form with fields: Source URL, Width, Height, Format (dropdown), Quality, Fit Mode (dropdown), Expiration TTL (dropdown)
- [ ] POST to `/generate`
- [ ] Result display area showing generated URL and expiration
- [ ] Click-to-copy input field for generated URL
### Phase 5: Auth Handlers
- [ ] Create `internal/handlers/auth.go` with:
- [ ] `HandleRoot() http.HandlerFunc` - serves login form (GET) or authenticates (POST) if not logged in; serves generator form if logged in
- [ ] `handleLoginGet(w, r)` - renders login.html template
- [ ] `handleLoginPost(w, r)` - validates key with constant-time comparison, creates session on success
- [ ] `HandleLogout() http.HandlerFunc` - clears session, redirects to `/`
- [ ] `HandleGenerateURL() http.HandlerFunc` - parses form, generates encrypted URL, renders generator.html with result
### Phase 6: Encrypted Image Handler
- [ ] Create `internal/handlers/imageenc.go` with:
- [ ] `HandleImageEnc() http.HandlerFunc` - handles `/v1/e/{token}`
- [ ] Extract token from chi URL param
- [ ] Decrypt and validate via `encGen.Parse(token)`
- [ ] Convert to `ImageRequest` via `payload.ToImageRequest()`
- [ ] Serve via `imgSvc.Get()` (bypass signature validation - encrypted URL is trusted)
- [ ] Set same response headers as regular image handler (Cache-Control, Content-Type, etc.)
- [ ] Handle errors: expired → 410 Gone, decrypt fail → 400 Bad Request
### Phase 7: Handler Integration
- [ ] Modify `internal/handlers/handlers.go`:
- [ ] Add `sessMgr *session.Manager` field to `Handlers` struct
- [ ] Add `encGen *encurl.Generator` field to `Handlers` struct
- [ ] Add `templates *template.Template` field to `Handlers` struct
- [ ] Initialize session manager and URL generator in `initImageService()` or new init method
- [ ] Add import for `internal/session`, `internal/encurl`, `templates`
### Phase 8: Route Registration
- [ ] Modify `internal/server/routes.go`:
- [ ] Add `s.router.Get("/", s.h.HandleRoot())` - login or generator page
- [ ] Add `s.router.Post("/", s.h.HandleRoot())` - login form submission
- [ ] Add `s.router.Get("/logout", s.h.HandleLogout())` - logout
- [ ] Add `s.router.Post("/generate", s.h.HandleGenerateURL())` - URL generation
- [ ] Add `s.router.Get("/v1/e/{token}", s.h.HandleImageEnc())` - encrypted image serving
### Phase 9: Testing & Verification
- [ ] Run `make check` to verify lint and tests pass
- [ ] Manual test: visit `/`, see login form
- [ ] Manual test: enter wrong key, see error
- [ ] Manual test: enter correct signing key, see generator form
- [ ] Manual test: generate encrypted URL, verify it works
- [ ] Manual test: wait for expiration or use short TTL, verify expired URL returns 410
- [ ] Manual test: logout, verify redirected to login
## Project Setup
- [x] Create Makefile with check, lint, test, fmt targets
- [x] Create project structure (cmd/pixad, internal/*)