feat(devices): pairing codes replace the registration broadcast
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).
This commit is contained in:
1 parent
ab5c52c709
commit
8fbacd9f46
47 files changed
+935
-273
No files matched your search
@@ -4,21 +4,19 @@ path: /api/devices/pending
|
||||
auth: jwt
|
||||
area: devices
|
||||
---
|
||||
<!-- source: internal/handlers/devices.go:703 DeviceHandler.ListPendingRegistrations -->
|
||||
<!-- source: internal/handlers/devices.go DeviceHandler.ListPendingRegistrations -->
|
||||
|
||||
# List Pending Device Registrations:<|end|><|start|><|start|>assistant to=edit<|channel|>analysis <|constrain|>xml<|message|><rfl:parameter name="path">
|
||||
/home/nymusicman/Code/bookhoard/docs/developer/api/devices/reject_device_registration.md
|
||||
# List Pending Device Registrations
|
||||
|
||||
List all pending device registration requests.
|
||||
Always empty by design. Unclaimed device registrations are invisible to every
|
||||
account since pairing codes took over the approval flow: first-approve-wins on
|
||||
a broadcast list let any account claim (or reject) a device it could not even
|
||||
name. Instead, the registering device shows a 6-digit pairing code, and the
|
||||
person holding the device types it into their own account's Devices page —
|
||||
`POST /api/devices/claim`.
|
||||
|
||||
**Endpoint**: `GET /api/devices/pending`
|
||||
**Auth**: Required (Admin only)
|
||||
|
||||
## Request Headers
|
||||
|
||||
| Header | Type | Required | Description |
|
||||
| ------------- | ------ | -------- | ----------------------------------- |
|
||||
| Authorization | string | Yes | Bearer token (must have admin role) |
|
||||
**Auth**: Required (JWT)
|
||||
|
||||
### Example Request
|
||||
|
||||
@@ -31,23 +29,16 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
|
||||
```json example-response
|
||||
{
|
||||
"registrations": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"device_name": "My Kobo",
|
||||
"device_type": "kobo",
|
||||
"user_email": "user@example.com",
|
||||
"created_at": "2026-02-08T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"total": 3,
|
||||
"pending_registrations": null
|
||||
"registrations": [],
|
||||
"pending_registrations": [],
|
||||
"total": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
Both `registrations` and `pending_registrations` key spellings are emitted for
|
||||
API compatibility.
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------------------------------- |
|
||||
| 401 | Invalid or expired token |
|
||||
| 403 | User does not have admin privileges |
|
||||
## Related
|
||||
|
||||
- [Register Device](register_device.md) — mints the pairing code
|
||||
- [Claim Device](claim_device.md) — the code-driven replacement for list-and-approve
|
||||
Reference in new issue
Block a user