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 Users
List all users in the system (admin only). List all users in the system (admin only).
@@ -28,29 +36,34 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK) ## Response (200 OK)
```json ```json example-response
{ [
"users": [ {
{ "id": "550e8400-e29b-41d4-a716-446655440000",
"id": "uuid", "email": "user@example.com",
"email": "user@example.com", "username": "johndoe",
"username": "johndoe", "password_hash": "$2a$04$REDACTED",
"first_name": "John", "first_name": "John",
"last_name": "Doe", "last_name": "Doe",
"role": "user", "role": "user",
"theme": "tokyo-night", "theme": "tokyo-night",
"max_devices": 10, "max_devices": 10,
"device_count": 3, "created_at": "2026-02-08T10:00:00Z",
"created_at": "2026-02-08T10:00:00Z", "timezone": "America/New_York",
"updated_at": "2026-02-08T10:00:00Z" "updated_at": "2026-02-08T10:00:00Z",
} "default_audio_speed": 1
], }
"total": 25, ]
"limit": 10,
"offset": 0
}
``` ```
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 ### Response Fields
| Field | Type | Description | | 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 User Max Devices
Update the maximum number of devices a user can register (admin only). 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 ### Example Request
```json ```json example-request
{ {
"max_devices": 15 "max_devices": 15
} }
@@ -28,14 +36,9 @@ Update the maximum number of devices a user can register (admin only).
## Response (200 OK) ## Response (200 OK)
```json ```json example-response
{ {
"id": "uuid", "message": null
"email": "user@example.com",
"username": "johndoe",
"max_devices": 15,
"active_devices": 3,
"updated_at": "2026-02-08T11:00:00Z"
} }
``` ```
@@ -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 Book Matches
Get potential book matches for a given query. Get potential book matches for a given query.
@@ -27,20 +35,22 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK) ## Response (200 OK)
```json ```json example-response
{ {
"matches": [ "matches": [
{ {
"id": "uuid", "id": "uuid",
"title": "Foundation", "title": "Foundation",
"authors": ["Isaac Asimov"], "authors": [
"Isaac Asimov"
],
"isbn": "978-0553293357", "isbn": "978-0553293357",
"published_year": 1951, "published_year": 1951,
"publisher": "Bantam Books", "publisher": "Bantam Books",
"cover_url": "https://example.com/cover.jpg" "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 Device Registration
Approve a pending device registration request. Approve a pending device registration request.
@@ -26,7 +34,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK) ## Response (200 OK)
```json ```json example-response
{ {
"message": "device approved successfully", "message": "device approved successfully",
"device_name": "string", "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. List all pending device registration requests.
@@ -20,7 +29,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK) ## Response (200 OK)
```json ```json example-response
{ {
"registrations": [ "registrations": [
{ {
@@ -31,7 +40,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
"created_at": "2026-02-08T10:00:00Z" "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 Device Registration
Reject a pending device registration request. Reject a pending device registration request.
@@ -26,7 +34,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK) ## Response (200 OK)
```json ```json example-response
{ {
"message": "device registration rejected" "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 Progress History
Get historical reading progress data for a media item. Get historical reading progress data for a media item.
@@ -33,31 +41,11 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
## Response (200 OK) ## Response (200 OK)
```json ```json example-response
{ {
"history": [ "count": null,
{ "media_item_id": null,
"media_item_id": "uuid", "sessions": null
"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
} }
``` ```
+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 Password
Change user password. Supports both self-service and admin modes. 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 ### Example Request
```json ```json example-request
{ {
"current_password": "OldPassword123!", "current_password": "OldPassword123!",
"new_password": "NewSecureP@ss123!", "new_password": "NewSecureP@ss123!",
@@ -56,7 +64,7 @@ Admins can reset any user's password without knowing the current password.
## Response (200 OK) ## Response (200 OK)
```json ```json example-response
{ {
"message": "password updated" "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 User
Delete a user account. Supports both self-deletion and admin deletion. 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) ## Response (200 OK)
```json ```json example-response
{ {
"message": "account deleted" "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 Profile
Update user profile information. Supports both self-edit and admin modes. 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 ### Example Request
```json ```json example-request
{ {
"username": "newusername", "username": "newusername",
"email": "newemail@example.com", "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) ## Response (200 OK)
```json ```json example-response
{ {
"id": "uuid", "message": null
"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"
} }
``` ```
+4
View File
@@ -140,6 +140,10 @@ func authLabel(chain []string) string {
return "device-token" return "device-token"
case has("createOptionalAuthMiddleware"): case has("createOptionalAuthMiddleware"):
return "optional-jwt" 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"): case has("jwt"):
return "jwt" return "jwt"
default: default:
+67
View File
@@ -659,4 +659,71 @@ var Schemas = map[string]Schema{
"watching_libraries": [""], "total_watching": 0 "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
}`),
},
} }