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).
41 lines
1.6 KiB
Go
41 lines
1.6 KiB
Go
package router
|
|
|
|
func registerDeviceRoutes(cfg *Config) {
|
|
e := cfg.Echo
|
|
|
|
// JWT middleware
|
|
jwtMiddleware := createJWTMiddleware(cfg)
|
|
|
|
// Protected routes
|
|
protected := e.Group("/api", jwtMiddleware)
|
|
devices := protected.Group("/devices")
|
|
|
|
// Public device registration routes (no auth required)
|
|
e.POST("/api/devices/register", cfg.DeviceHandler.InitiateRegistration)
|
|
e.POST("/api/devices/register/status", cfg.DeviceHandler.CheckRegistrationStatus)
|
|
// The REGISTERING device may end its own pairing attempt (dismissed
|
|
// waiting dialog); bearer is the registration UUID it holds, same
|
|
// trust as the status poll.
|
|
e.POST("/api/devices/register/cancel", cfg.DeviceHandler.CancelRegistration)
|
|
|
|
// Device management routes (protected)
|
|
devices.GET("", cfg.DeviceHandler.ListDevices)
|
|
devices.GET("/:id", cfg.DeviceHandler.GetDevice)
|
|
devices.PUT("/:id", cfg.DeviceHandler.UpdateDevice)
|
|
devices.DELETE("/:id", cfg.DeviceHandler.DeleteDevice)
|
|
|
|
// Token regeneration endpoint (JWT authentication required)
|
|
devices.PUT("/:id/regenerate-token", cfg.DeviceHandler.RegenerateDeviceToken)
|
|
|
|
devices.GET("/pending", cfg.DeviceHandler.ListPendingRegistrations)
|
|
devices.GET("/approve/:registration_id", cfg.DeviceHandler.ApproveDevice)
|
|
// Pairing-code claim flow (JWT): lookup is the confirm-card data,
|
|
// claim binds the device to the caller's account.
|
|
devices.POST("/claim/lookup", cfg.DeviceHandler.LookupPairingCode)
|
|
devices.POST("/claim", cfg.DeviceHandler.ClaimDevice)
|
|
|
|
// Device sidecar configuration routes
|
|
devices.GET("/:id/sidecar", cfg.SidecarHandler.GetSidecarConfig)
|
|
devices.GET("/:id/sidecar/download", cfg.SidecarHandler.DownloadSidecarConfig)
|
|
}
|