docs(auth,users,devices,progress,matching): Beam batch 8 — WO5 backfill complete

14 endpoints backfilled: devices pending/approve/reject; users
password/profile/profile-delete self-service (admin variants in prose);
admin list_users + update_user_max_devices; progress history; book
matching; new password-rules doc.

Gate fix: authLabel missed JWT middleware written inline as
createJWTMiddleware(cfg) (uppercase 'JWT') — reported auth 'none' for
/api/auth self-service routes and would have for any wrapper-inlined
group. Labeled 'JWTMiddleware' + fallthrough 'jwt' case.

Claude registry: 12 entries. Drift fixed by gate: list_users on-device —
handler returns raw []database.Users (bare array, full model incl.
password_hash — real leak the API emits; pinned + documented; earlier
draft had invented {users,total,limit,offset} wrapper). max-devices
response is {message} only. books/match is GET /api/books/match
{matches, action}. progress history sessions = ReadingHistory rows
(pgtype fields). make test 9/9.
This commit is contained in:
John O'Keefe committed 2026-10-24 17:33:32 -04:00
1 parent 2c56ae4b9d
commit cc5677eb17
13 files changed
+233 -75

No files matched your search

+34 -21
View File
@@ -1,3 +1,11 @@
---
method: GET
path: /api/auth/users
auth: jwt+admin
area: authentication
---
<!-- source: internal/handlers/auth.go:734 AuthHandler.ListUsers -->
# List Users
List all users in the system (admin only).
@@ -28,29 +36,34 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK)
```json
{
"users": [
{
"id": "uuid",
"email": "user@example.com",
"username": "johndoe",
"first_name": "John",
"last_name": "Doe",
"role": "user",
"theme": "tokyo-night",
"max_devices": 10,
"device_count": 3,
"created_at": "2026-02-08T10:00:00Z",
"updated_at": "2026-02-08T10:00:00Z"
}
],
"total": 25,
"limit": 10,
"offset": 0
}
```json example-response
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "johndoe",
"password_hash": "$2a$04$REDACTED",
"first_name": "John",
"last_name": "Doe",
"role": "user",
"theme": "tokyo-night",
"max_devices": 10,
"created_at": "2026-02-08T10:00:00Z",
"timezone": "America/New_York",
"updated_at": "2026-02-08T10:00:00Z",
"default_audio_speed": 1
}
]
```
Response is a bare JSON array of full user rows — no pagination wrapper
and no `device_count` (those were invented in an earlier draft).
**Leak note:** the user model json-marshals `password_hash` verbatim —
the list-users response exposes password hashes to any admin caller.
Known defect, tracked separately (doc mirrors the real field set; do not
implement against a redacted shape).
### Response Fields
| Field | Type | Description |
@@ -1,3 +1,11 @@
---
method: PUT
path: /api/auth/users/:id/max-devices
auth: jwt+admin
area: authentication
---
<!-- source: internal/handlers/auth.go:1160 AuthHandler.UpdateUserMaxDevices -->
# Update User Max Devices
Update the maximum number of devices a user can register (admin only).
@@ -20,7 +28,7 @@ Update the maximum number of devices a user can register (admin only).
### Example Request
```json
```json example-request
{
"max_devices": 15
}
@@ -28,14 +36,9 @@ Update the maximum number of devices a user can register (admin only).
## Response (200 OK)
```json
```json example-response
{
"id": "uuid",
"email": "user@example.com",
"username": "johndoe",
"max_devices": 15,
"active_devices": 3,
"updated_at": "2026-02-08T11:00:00Z"
"message": null
}
```
@@ -0,0 +1,31 @@
---
method: GET
path: /api/auth/password-rules
auth: none
area: authentication
---
<!-- source: internal/handlers/auth.go:746 AuthHandler.GetPasswordRules -->
# Password Rules
Returns the active password complexity configuration, so UI requirement
checkers mirror the real rules. The server remains the enforcer. This
route is rate-limited.
## Example response
```json example-response
{
"min_length": 8,
"require_upper": true,
"require_lower": true,
"require_number": true,
"require_special": true
}
```
## Errors
| Status | Body | When |
| ------ | ---- | ---- |
| 429 | `{"error": "..."}` | rate limited |
@@ -1,3 +1,11 @@
---
method: GET
path: /api/books/match
auth: jwt
area: book-matching
---
<!-- source: internal/handlers/matching.go:309 MatchingHandler.GetBookMatches -->
# Get Book Matches
Get potential book matches for a given query.
@@ -27,20 +35,22 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK)
```json
```json example-response
{
"matches": [
{
"id": "uuid",
"title": "Foundation",
"authors": ["Isaac Asimov"],
"authors": [
"Isaac Asimov"
],
"isbn": "978-0553293357",
"published_year": 1951,
"publisher": "Bantam Books",
"cover_url": "https://example.com/cover.jpg"
}
],
"total": 5
"action": null
}
```
@@ -1,3 +1,11 @@
---
method: GET
path: /api/devices/approve/:registration_id
auth: jwt
area: devices
---
<!-- source: internal/handlers/devices.go:513 DeviceHandler.ApproveDevice -->
# Approve Device Registration
Approve a pending device registration request.
@@ -26,7 +34,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK)
```json
```json example-response
{
"message": "device approved successfully",
"device_name": "string",
@@ -1,4 +1,13 @@
# List Pending Device Registrations
---
method: GET
path: /api/devices/pending
auth: jwt
area: devices
---
<!-- source: internal/handlers/devices.go:703 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 all pending device registration requests.
@@ -20,7 +29,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK)
```json
```json example-response
{
"registrations": [
{
@@ -31,7 +40,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
"created_at": "2026-02-08T10:00:00Z"
}
],
"total": 3
"total": 3,
"pending_registrations": null
}
```
@@ -1,3 +1,11 @@
---
method: POST
path: /api/devices/reject/:registration_id
auth: jwt
area: devices
---
<!-- source: internal/handlers/devices.go:664 DeviceHandler.RejectDevice -->
# Reject Device Registration
Reject a pending device registration request.
@@ -26,7 +34,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK)
```json
```json example-response
{
"message": "device registration rejected"
}
@@ -1,3 +1,11 @@
---
method: GET
path: /api/progress/:id/history
auth: jwt
area: progress
---
<!-- source: internal/handlers/progress.go:34 Handler.GetProgressHistory -->
# Get Progress History
Get historical reading progress data for a media item.
@@ -33,31 +41,11 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK)
```json
```json example-response
{
"history": [
{
"media_item_id": "uuid",
"percentage": 75.5,
"position": 1234,
"page": 150,
"device_id": "device-uuid",
"device_name": "My Kobo",
"created_at": "2026-02-08T10:00:00Z"
},
{
"media_item_id": "uuid",
"percentage": 50.0,
"position": 800,
"page": 100,
"device_id": "device-uuid",
"device_name": "My Kobo",
"created_at": "2026-02-07T15:00:00Z"
}
],
"total": 45,
"limit": 20,
"offset": 0
"count": null,
"media_item_id": null,
"sessions": null
}
```
+10 -2
View File
@@ -1,3 +1,11 @@
---
method: PUT
path: /api/auth/password
auth: jwt
area: authentication
---
<!-- source: internal/handlers/auth.go:968 AuthHandler.UpdatePassword -->
# Change Password
Change user password. Supports both self-service and admin modes.
@@ -24,7 +32,7 @@ Users can change their own password by providing current password verification.
### Example Request
```json
```json example-request
{
"current_password": "OldPassword123!",
"new_password": "NewSecureP@ss123!",
@@ -56,7 +64,7 @@ Admins can reset any user's password without knowing the current password.
## Response (200 OK)
```json
```json example-response
{
"message": "password updated"
}
+9 -1
View File
@@ -1,3 +1,11 @@
---
method: DELETE
path: /api/auth/profile
auth: jwt
area: authentication
---
<!-- source: internal/handlers/auth.go:1049 AuthHandler.DeleteUser -->
# Delete User
Delete a user account. Supports both self-deletion and admin deletion.
@@ -25,7 +33,7 @@ Admins can delete any user account by providing the user ID in the URL.
## Response (200 OK)
```json
```json example-response
{
"message": "account deleted"
}
+11 -11
View File
@@ -1,3 +1,11 @@
---
method: PUT
path: /api/auth/profile
auth: jwt
area: authentication
---
<!-- source: internal/handlers/auth.go:562 AuthHandler.UpdateProfile -->
# Update Profile
Update user profile information. Supports both self-edit and admin modes.
@@ -26,7 +34,7 @@ Users can update their own profile. All fields are optional.
### Example Request
```json
```json example-request
{
"username": "newusername",
"email": "newemail@example.com",
@@ -60,17 +68,9 @@ Admins can update any user by providing the user ID in the URL. Additionally sup
## Response (200 OK)
```json
```json example-response
{
"id": "uuid",
"email": "user@example.com",
"username": "john",
"first_name": "John",
"last_name": "Smith",
"theme": "dracula",
"role": "user",
"max_devices": 10,
"created_at": "2026-01-31T10:00:00Z"
"message": null
}
```
+4
View File
@@ -140,6 +140,10 @@ func authLabel(chain []string) string {
return "device-token"
case has("createOptionalAuthMiddleware"):
return "optional-jwt"
case has("JWTMiddleware"):
// Wrapper-inlined forms like createJWTMiddleware(cfg) carry uppercase
// JWT; bare jwtMiddleware carries lowercase. Match both.
fallthrough
case has("jwt"):
return "jwt"
default:
+67
View File
@@ -659,4 +659,71 @@ var Schemas = map[string]Schema{
"watching_libraries": [""], "total_watching": 0
}`),
},
// --- Beam batch 8: devices remaining + auth remainder ---
"GET /api/devices/pending": {
// Device registrations live in an in-memory map; both key spellings
// are emitted for compatibility.
Response: json.RawMessage(`{
"registrations": [], "pending_registrations": [], "total": 0
}`),
},
"GET /api/devices/approve/:registration_id": {
// Already-approved variant omits registration_id. Credentials ride
// the registration map polled elsewhere — NOT this JSON body.
Response: json.RawMessage(`{
"message": "", "device_name": "", "device_type": "",
"registration_id": "", "approved": false
}`),
},
"POST /api/devices/reject/:registration_id": {
Response: json.RawMessage(`{"message": ""}`),
},
"POST /api/auth/users": {
Request: handlers.RegisterRequest{},
Response: json.RawMessage(`{
"id": "", "email": "", "username": "", "role": "",
"timezone": "", "created_at": null
}`),
},
"GET /api/auth/users": {
Response: []database.Users{},
},
"PUT /api/auth/users/:id/max-devices": {
Request: handlers.UpdateUserMaxDevicesRequest{},
Response: json.RawMessage(`{"message": ""}`),
},
"PUT /api/auth/password": {
// Self-change (jwt). Admin variant PUT /auth/password/:id documented
// in prose; handler is shared (admin mode skips current_password).
Request: json.RawMessage(`{
"current_password": "", "new_password": "", "confirm_password": ""
}`),
Response: json.RawMessage(`{"message": ""}`),
},
"PUT /api/auth/profile": {
// Shared with admin variant (PUT /auth/profile/:id).
Request: handlers.AdminUpdateUserRequest{},
Response: json.RawMessage(`{"message": ""}`),
},
"DELETE /api/auth/profile": {
// Self-delete; admin variant DELETE /auth/profile/:id (reassignment
// of admin-owned assets to another admin before cascade).
Response: json.RawMessage(`{"message": ""}`),
},
"GET /api/progress/:id/history": {
// id is MEDIA ITEM id; sessions are ReadingHistory rows.
Response: json.RawMessage(`{
"media_item_id": "", "sessions": [], "count": 0
}`),
},
"GET /api/books/match": {
Response: services.BookQueryResponse{},
},
"GET /api/auth/password-rules": {
Response: json.RawMessage(`{
"min_length": 8, "require_upper": true, "require_lower": true,
"require_number": true, "require_special": true
}`),
},
}