A pending device registration used to be announced to EVERY account's
Devices page, and whoever clicked Approve first bound the device — and
its freshly minted sync token — to their account. Worse, the pending
card displayed the registration UUID, which is also the bearer
credential on the unauthenticated /api/devices/register/status poll
channel: any family user who could see the list already held everything
needed to take the device. On a family server that is a
wrong-account-binding bug (accidental or deliberate); this commit
removes the class of problem rather than patching the race.
The new model is the Netflix/YouTube TV device flow: the registering
device displays a 6-digit pairing code, and the only person who can
read it — whoever holds the device — types it into THEIR account on the
Devices page. Possession of the device screen becomes the credential;
nothing is broadcast; the reject-on-someone-else's-behalf hole dies
with the list.
Server changes:
- POST /api/devices/register now mints pairing_code (crypto/rand,
rejection-sampled for uniformity, collision-checked against live
registrations) and retires prior unclaimed registrations with the
same device_identifier, so re-registering leaves exactly one live
attempt.
- POST /api/devices/claim (JWT) binds the registration behind a code to
the caller's account, reusing the rotate-or-create approval logic
factored out of ApproveDevice (shared bindRegistrationToUser helper;
the Android self-approval route keeps working unchanged).
- POST /api/devices/claim/lookup (JWT) returns the confirm-card data
without binding. Lookup and claim share a per-account budget (12
attempts/min, DeviceRateLimiter gained a claim case) — the 1M-code
space over a 5-minute TTL makes guessing infeasible without it.
- POST /api/devices/register/cancel lets the registering device end its
own attempt (registration UUID bearer, idempotent, never touches a
claimed registration).
- ListPendingRegistrations / GetPendingRegistrationsData are
deprecated-by-emptiness: unclaimed registrations return to nobody.
- RejectDevice and its route are deleted — the broadcast list was its
only consumer, and there is nothing left for anyone to reject.
Additive-only for pinned surfaces: the register response gains
pairing_code (Android DTO untouched), the four KOReader sync surfaces
and approve keep byte-identical shapes. contract_pins.md changelog
records the widening and the reject removal. Docs, schema registry, and
bruno mirrors move together per the drift gate; bruno_gen.go now keeps
hand-authored bruno/podcasts/ (API docs never existed for them, so the
next -update run would have swept them — latent landmine, defused).
Unit tests: code shape, live-collision avoidance, typed-code
normalization, and the resolve scopes (live/expired/claimed/miss).
Phase 1: Register routes that were defined but never connected
- Add GET/PUT /api/system/config routes (admin-only) for system
configuration in new internal/router/system.go
- Add GET /api/devices/:id/sidecar routes for device sidecar config
- Add SidecarHandler to router Config struct
- Instantiate SidecarHandler in main.go with config for fallback support
These routes were implemented in handlers/sidecar.go but never registered,
breaking the ability to configure base URLs for device sync.
- Add handler to regenerate device auth tokens
- Add PUT /api/devices/:id/regenerate-token route
- Returns new token and sync URLs for device configuration
Revert unauthorized route changes made during router refactoring:
Device Routes:
- Change :token back to :registration_id in approve/reject routes
- Keep routes in correct location (approve/reject in protected group)
OPDS Routes:
- Restore /opds/devices/:deviceId/* structure (was /opds/:id/*)
- Add back missing :bookId parameter for download/cover/formats
- Change 'navigation' back to 'nav'
Queue Routes:
- Add missing admin-only routes
- Add missing device-specific queue management routes
All routes now match original main.go signatures exactly.
Breaking changes reverted - API contract restored.
Add createJWTMiddleware helper that sets database.Users object in context,
matching the original main.go JWT middleware behavior. This fixes
'authentication context error' panics in handlers that call
MustGetAuthenticatedUser.
Changes:
- Add createJWTMiddleware() in router.go
- Update all route files to use the helper
- Set user claims AND database.Users object in context
Add stub implementations for:
- library.go: Library management routes (admin + user visibility)
- device.go: Device registration and management routes
- router.go: Updated to import jwt package
Router package structure is complete with all route groups defined.
Next step: Incrementally migrate routes from main.go by calling
router.RegisterRoutes() and removing duplicate definitions.
All verification checks pass (26/26).