From 857be30e82ecdb41aac6ce36f481f4fad39e05e7 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 8 Jan 2026 08:43:23 -0800 Subject: [PATCH] Update TODO.md: mark auth/encrypted URLs feature as complete --- TODO.md | 154 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/TODO.md b/TODO.md index 93a5c33..95414b3 100644 --- a/TODO.md +++ b/TODO.md @@ -5,37 +5,37 @@ A single linear checklist of tasks to implement the complete pixa caching image ## 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 +- [x] Create `internal/seal/crypto.go` with: + - [x] `DeriveKey(masterKey []byte, salt string) ([32]byte, error)` - HKDF-SHA256 key derivation + - [x] `Encrypt(key [32]byte, plaintext []byte) (string, error)` - NaCl secretbox encrypt, returns base64url + - [x] `Decrypt(key [32]byte, ciphertext string) ([]byte, error)` - NaCl secretbox decrypt from base64url +- [x] Create `internal/seal/crypto_test.go` with tests for: + - [x] Key derivation produces consistent keys for same input + - [x] Encrypt/decrypt round-trip + - [x] Decryption fails with wrong key + - [x] 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 +- [x] Add `github.com/gorilla/securecookie` dependency +- [x] Create `internal/session/session.go` with: + - [x] `Data` struct: `Authenticated bool`, `CreatedAt time.Time`, `ExpiresAt time.Time` + - [x] `Manager` struct using gorilla/securecookie with keys derived via HKDF + - [x] `NewManager(signingKey string, secure bool) (*Manager, error)` + - [x] `CreateSession(w http.ResponseWriter) error` - creates 30-day encrypted cookie + - [x] `ValidateSession(r *http.Request) (*Data, error)` - decrypts and validates cookie + - [x] `ClearSession(w http.ResponseWriter)` - clears cookie (logout) + - [x] `IsAuthenticated(r *http.Request) bool` - convenience wrapper + - [x] Cookie settings: `HttpOnly`, `Secure` (prod), `SameSite=Strict`, name `pixa_session` +- [x] Create `internal/session/session_test.go` with tests for: + - [x] Session creation and validation round-trip + - [x] Expired session rejection + - [x] Tampered cookie rejection + - [x] 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): +- [x] Add `github.com/fxamacker/cbor/v2` dependency for compact binary encoding +- [x] Create `internal/encurl/encurl.go` with: + - [x] `Payload` 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 @@ -45,69 +45,67 @@ A single linear checklist of tasks to implement the complete pixa caching image - `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 + - [x] `Generator` struct with URL key derived via HKDF (salt: `"pixa-urlenc-v1"`) + - [x] `NewGenerator(signingKey string) (*Generator, error)` + - [x] `Generate(p *Payload) (string, error)` - CBOR encode, encrypt, base64url + - [x] `Parse(token string) (*Payload, error)` - base64url decode, decrypt, CBOR decode, validate expiration + - [x] `(p *Payload) ToImageRequest() *imgcache.ImageRequest` +- [x] Create `internal/encurl/encurl_test.go` with tests for: + - [x] Generate/parse round-trip preserves all fields + - [x] Expired URL returns `ErrExpired` + - [x] Malformed token returns error + - [x] 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 +- [x] Create `internal/templates/templates.go` with: + - [x] `//go:embed *.html` for embedded templates + - [x] `Get() *template.Template` function +- [x] Create `internal/templates/login.html`: + - [x] Simple form with password input for signing key + - [x] POST to `/` + - [x] Error message display area + - [x] Tailwind CSS styling +- [x] Create `internal/templates/generator.html`: + - [x] Logout link at top + - [x] Form with fields: Source URL, Width, Height, Format (dropdown), Quality, Fit Mode (dropdown), Expiration TTL (dropdown) + - [x] POST to `/generate` + - [x] Result display area showing generated URL and expiration + - [x] Click-to-copy functionality 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 +- [x] Create `internal/handlers/auth.go` with: + - [x] `HandleRoot() http.HandlerFunc` - serves login form (GET) or authenticates (POST) if not logged in; serves generator form if logged in + - [x] `handleLoginPost(w, r)` - validates key with constant-time comparison, creates session on success + - [x] `HandleLogout() http.HandlerFunc` - clears session, redirects to `/` + - [x] `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 +- [x] Create `internal/handlers/imageenc.go` with: + - [x] `HandleImageEnc() http.HandlerFunc` - handles `/v1/e/{token}` + - [x] Extract token from chi URL param + - [x] Decrypt and validate via `encGen.Parse(token)` + - [x] Convert to `ImageRequest` via `payload.ToImageRequest()` + - [x] Serve via `imgSvc.Get()` (bypass signature validation - encrypted URL is trusted) + - [x] Set same response headers as regular image handler (Cache-Control, Content-Type, etc.) + - [x] 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` +- [x] Modify `internal/handlers/handlers.go`: + - [x] Add `sessMgr *session.Manager` field to `Handlers` struct + - [x] Add `encGen *encurl.Generator` field to `Handlers` struct + - [x] Initialize session manager and URL generator in `initImageService()` ### 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 +- [x] Modify `internal/server/routes.go`: + - [x] Add `s.router.Get("/", s.h.HandleRoot())` - login or generator page + - [x] Add `s.router.Post("/", s.h.HandleRoot())` - login form submission + - [x] Add `s.router.Get("/logout", s.h.HandleLogout())` - logout + - [x] Add `s.router.Post("/generate", s.h.HandleGenerateURL())` - URL generation + - [x] Add `s.router.Get("/v1/e/{token}", s.h.HandleImageEnc())` - encrypted image serving + - [x] Add static file serving for Tailwind CSS ### Phase 9: Testing & Verification -- [ ] Run `make check` to verify lint and tests pass +- [x] 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