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:
1 parent
2c56ae4b9d
commit
cc5677eb17
13 files changed
+233
-75
No files matched your search
@@ -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
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
}`),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user