Generated by
npm run docs:generatefrom the@openapiJSDoc blocks insrc/routes/*.routes.js— do not hand-edit this file. Change the route annotations and regenerate instead. An interactive version of this same spec is served at/api/docswhenever the server is running.
Base URL: http://localhost:5080/api (configurable via PORT)
Protected endpoints require a JWT in the Authorization header:
Authorization: Bearer <token>
Obtain a token from POST /auth/register, POST /auth/login, or POST /auth/setup-admin, then send it as Authorization: Bearer <token>.
All endpoints respond with JSON in a consistent shape:
{
"success": true,
"message": "Human-readable summary",
"data": {},
"pagination": {}
}Errors:
{
"success": false,
"message": "What went wrong",
"error": "Detail (omitted in production for 500s)"
}| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad request / validation error |
| 401 | Unauthorized (missing/invalid/expired/revoked token) |
| 403 | Forbidden (authenticated, wrong role/owner) |
| 404 | Not found |
| 409 | Conflict (duplicate, business rule violation) |
| 429 | Rate limited |
| 500 | Internal server error |
Applies to every list endpoint. Response includes:
"pagination": {
"current_page": 1,
"total_pages": 5,
"total_items": 47,
"items_per_page": 10,
"has_next": true,
"has_prev": false
}Two layers, both active on every /api/* request:
- A flat, IP-based backstop: 100 requests / 15 min (
express-rate-limit). - A role-aware limit, requests/minute, backed by Redis when
REDIS_URLis set (falls back to in-process otherwise — seedocs/SETUP.md):
| Role | Limit |
|---|---|
| Guest | 20 req / min |
| User | 60 req / min |
| Librarian | 120 req / min |
| Admin | 300 req / min |
Disabled entirely under NODE_ENV=test so the automated test suite isn't
flaky against shared per-minute buckets — see the manual curl procedures in
SECURITY_TESTING.md to exercise this for real.
Liveness/readiness checks
Access: Public
Verifies the process is up and its dependencies (database, and Redis when configured) are reachable. Used by the Docker healthcheck and any future load balancer/uptime monitor.
Responses:
| Status | Description |
|---|---|
| 200 | Healthy — database (and Redis, if configured) reachable. |
| 503 | Degraded — database or Redis unreachable. |
// 200 — Healthy — database (and Redis, if configured) reachable.
{
"status": "ok",
"uptime_seconds": 42,
"timestamp": "2026-08-19T12:00:00.000Z",
"database": "connected",
"redis": "connected"
}// 503 — Degraded — database or Redis unreachable.
{
"status": "degraded",
"uptime_seconds": 42,
"timestamp": "2026-08-19T12:00:00.000Z",
"database": "disconnected",
"redis": "connected"
}Registration, login, and session management
Access: Authenticated (Bearer JWT)
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
current_password |
string | yes | |
new_password |
string | yes | 8+ characters, at least one uppercase, one lowercase, one number. Must differ from the current password. |
Responses:
| Status | Description |
|---|---|
| 200 | Password changed. |
| 400 | Request body failed schema validation |
| 401 | Missing, invalid, or expired token |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}Access: Public
Always responds 200 with the same generic message, whether or not the email is registered, to avoid leaking which emails exist. If the account exists, emails a single-use, 1-hour reset token (or logs it server-side as a dev-mode fallback if SMTP isn't configured) — the token itself is never returned in this response.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Generic acknowledgement — see description. |
// 200 — Generic acknowledgement — see description.
{
"success": true,
"message": "If an account with that email exists, a password reset link has been sent."
}Access: Public
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
emailOrUsername |
string | yes | |
password |
string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Login successful. |
| 401 | Invalid credentials, or account deactivated. |
| 429 | Too many failed attempts — account temporarily locked out. |
Access: Authenticated (Bearer JWT)
Revokes the token used on this request — it's rejected on every subsequent request even though it hasn't naturally expired yet.
Responses:
| Status | Description |
|---|---|
| 200 | Logged out. |
| 401 | Missing, invalid, or expired token |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}Access: Authenticated (Bearer JWT)
Responses:
| Status | Description |
|---|---|
| 200 | Current user (no password field). |
| 401 | Missing, invalid, or expired token |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}Access: Authenticated (Bearer JWT)
Responses:
| Status | Description |
|---|---|
| 200 | New token issued. |
| 401 | Missing, invalid, or expired token |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}Access: Public
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | yes | |
last_name |
string | yes | |
user_name |
string | yes | |
email |
string | yes | |
phone |
string | no | |
password |
string | yes | 8+ characters, at least one uppercase, one lowercase, one number. |
image_url |
string | no |
Responses:
| Status | Description |
|---|---|
| 201 | Account created. |
| 400 | Request body failed schema validation |
| 409 | Email or username already taken. |
Access: Public
Authorization comes from possessing a valid reset token, not from a session. Single-use — a second attempt with the same token returns 400.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | yes | Token from the forgot-password email. |
new_password |
string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Password reset. |
| 400 | Invalid, expired, or already-used reset token; or validation failure. |
Access: Public
Public, but gated by INITIAL_SETUP_KEY and refuses if any user already exists in the system — intended for first-run initialization only, not general admin creation.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
admin_email |
string | yes | |
admin_password |
string | yes | |
setup_key |
string | yes | Value of the INITIAL_SETUP_KEY env var. |
first_name |
string | no | |
last_name |
string | no |
Responses:
| Status | Description |
|---|---|
| 201 | Admin account created. |
| 401 | Invalid setup key. |
| 403 | System already initialized (a user already exists). |
Author records
Access: Public
Responses are cached for AUTHORS_CACHE_TTL_SECONDS (default 30s), keyed by the full query string, and invalidated on any create/update/delete.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
page |
query | integer | no | 1 |
|
limit |
query | integer | no | 10 |
|
search |
query | string | no | Matches first_name, last_name, or email. | |
sort_by |
query | string | no | created_at |
|
order |
query | "asc" | "desc" | no | desc |
Responses:
| Status | Description |
|---|---|
| 200 | Paginated author list. |
Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | yes | |
last_name |
string | yes | |
email |
string | yes | |
date_of_birth |
string | no | |
biography |
string | no | |
phone |
string | no | |
image |
string | no |
Responses:
| Status | Description |
|---|---|
| 201 | Author created. |
| 400 | Request body failed schema validation |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 409 | An author with this email already exists. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role. Fails if the author still has books attached.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Author deleted. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | Resource not found |
| 409 | Author has books attached — remove or reassign them first. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Public
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes | ||
include_books |
query | boolean | no | false |
Embed the author's books and a books_count. |
Responses:
| Status | Description |
|---|---|
| 200 | The author. |
| 404 | Resource not found |
Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role. Partial update — every field is optional.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | no | |
last_name |
string | no | |
email |
string | no | |
date_of_birth |
string | no | |
biography |
string | no | |
phone |
string | no | |
image |
string | no |
Responses:
| Status | Description |
|---|---|
| 200 | Author updated. |
| 400 | Request body failed schema validation |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | Resource not found |
| 409 | Another author already has this email. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Book catalog, borrowing, and returns
Access: Public
Responses are cached for BOOKS_CACHE_TTL_SECONDS (default 30s), keyed by the full query string, and invalidated on any create/update/delete.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
page |
query | integer | no | 1 |
|
limit |
query | integer | no | 10 |
|
search |
query | string | no | ||
author_id |
query | integer | no | ||
genre |
query | string | no | ||
status |
query | "Available" | "Borrowed" | "Reserved" | "Lost" | no |
Responses:
| Status | Description |
|---|---|
| 200 | Paginated book list. |
Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
isbn |
string | yes | |
title |
string | yes | |
author_id |
integer | yes | |
published_date |
string | no | |
description |
string | no | |
cover_image |
string | no | |
genre |
string | no | |
language |
string | no | |
pages |
integer | no | |
publisher |
string | no | |
available_copies |
integer | no | |
total_copies |
integer | no | |
status |
"Available" | "Borrowed" | "Reserved" | "Lost" | no |
Responses:
| Status | Description |
|---|---|
| 201 | Book created. |
| 400 | Validation failure, or available_copies exceeds total_copies. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | author_id does not exist. |
| 409 | A book with this ISBN already exists. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role. Fails if the book has active borrows.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Book deleted. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | Resource not found |
| 409 | Book has active borrows. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Public
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes | ||
include_author |
query | boolean | no | false |
|
include_borrows |
query | boolean | no | false |
Responses:
| Status | Description |
|---|---|
| 200 | The book. |
| 404 | Resource not found |
Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role. Partial update — every field is optional.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
isbn |
string | no | |
title |
string | no | |
author_id |
integer | no | |
published_date |
string | no | |
description |
string | no | |
cover_image |
string | no | |
genre |
string | no | |
language |
string | no | |
pages |
integer | no | |
publisher |
string | no | |
available_copies |
integer | no | |
total_copies |
integer | no | |
status |
"Available" | "Borrowed" | "Reserved" | "Lost" | no |
Responses:
| Status | Description |
|---|---|
| 200 | Book updated. |
| 400 | Request body failed schema validation |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | Resource not found |
| 409 | ISBN already taken by another book. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the book to have available_copies > 0 and status "Available", the caller to have fewer than 5 active borrows, and no existing active borrow of the same book by the same caller.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
due_days |
integer | no |
Responses:
| Status | Description |
|---|---|
| 201 | Book borrowed. |
| 401 | Missing, invalid, or expired token |
| 404 | Resource not found |
| 409 | Book unavailable, borrow limit exceeded, or already borrowed by this user. |
// 201 — Book borrowed.
{
"success": true,
"message": "Book borrowed successfully",
"data": {
"borrow_record": {
"id": 10,
"due_date": "2026-08-31T00:00:00.000Z",
"status": "Borrowed"
},
"book": {
"id": 1,
"title": "1984",
"available_copies": 4,
"status": "Available"
},
"due_date": "2026-08-31T00:00:00.000Z",
"days_allowed": 14
}
}// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}Access: Authenticated (Bearer JWT)
Applies a $1/day late fee if returned past the due date.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Book returned. |
| 401 | Missing, invalid, or expired token |
| 404 | Book not found, or no active borrow record for this book/user. |
// 200 — Book returned.
{
"success": true,
"message": "Book returned successfully",
"data": {
"borrow_record": {
"id": 10,
"status": "Returned"
},
"book": {
"id": 1,
"available_copies": 5,
"status": "Available"
},
"return_details": {
"is_overdue": false,
"days_late": 0,
"late_fee": 0
}
}
}// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}User accounts and profiles
Access: Authenticated (Bearer JWT)
Requires the Admin role.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
page |
query | integer | no | 1 |
|
limit |
query | integer | no | 10 |
|
search |
query | string | no | ||
role |
query | "Admin" | "Librarian" | "User" | no | ||
is_active |
query | boolean | no |
Responses:
| Status | Description |
|---|---|
| 200 | Paginated user list. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the Admin role. Can create any role, unlike self-registration.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | yes | |
last_name |
string | yes | |
user_name |
string | yes | |
email |
string | yes | |
phone |
string | no | |
password |
string | yes | |
image_url |
string | no | |
role |
"Admin" | "Librarian" | "User" | no | |
is_active |
boolean | no | |
email_verified |
boolean | no |
Responses:
| Status | Description |
|---|---|
| 201 | User created. |
| 400 | Request body failed schema validation |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 409 | Email or username already taken. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the Admin role. Fails if the user has active borrows, or is the last active Admin.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Responses:
| Status | Description |
|---|---|
| 200 | User deleted. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | Resource not found |
| 409 | User has active borrows, or is the last active Admin. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the Admin/Librarian role, or that the caller is requesting their own profile.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes | ||
include_borrows |
query | boolean | no | false |
|
include_stats |
query | boolean | no | false |
Responses:
| Status | Description |
|---|---|
| 200 | The user. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | Resource not found |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the resource owner, a Librarian, or an Admin. Which fields a given caller may actually set is enforced server-side and depends on who's calling — see the field-restriction table in docs/API.md. Attempting to set role without the Admin role returns 403; every other disallowed field is silently dropped rather than rejected.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | no | |
last_name |
string | no | |
phone |
string | no | |
email |
string | no | |
password |
string | no | |
image_url |
string | no | |
role |
"Admin" | "Librarian" | "User" | no | Admin-only. |
is_active |
boolean | no | Admin-only. |
email_verified |
boolean | no | Admin-only. |
Responses:
| Status | Description |
|---|---|
| 200 | User updated. |
| 400 | Request body failed schema validation |
| 401 | Missing, invalid, or expired token |
| 403 | Not the resource owner/Librarian/Admin, or attempted to set role without Admin. |
| 404 | Resource not found |
| 409 | Email already taken by another user. |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}Access: Authenticated (Bearer JWT)
Requires the Admin/Librarian role, or that the caller owns the records.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes | ||
page |
query | integer | no | 1 |
|
limit |
query | integer | no | 10 |
|
status |
query | "Borrowed" | "Returned" | "Overdue" | no |
Responses:
| Status | Description |
|---|---|
| 200 | Paginated borrow records for this user. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | Resource not found |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Responses:
| Status | Description |
|---|---|
| 200 | Current user's profile. |
| 401 | Missing, invalid, or expired token |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}Access: Public
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
page |
query | integer | no | 1 |
|
limit |
query | integer | no | 20 |
|
role |
query | "Admin" | "Librarian" | "User" | no | ||
is_active |
query | boolean | no | true |
Responses:
| Status | Description |
|---|---|
| 200 | Paginated public user list. |
Borrow/return history, overdue tracking, statistics
Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
page |
query | integer | no | 1 |
|
limit |
query | integer | no | 10 |
|
user_id |
query | integer | no | ||
book_id |
query | integer | no | ||
status |
query | "Borrowed" | "Returned" | "Overdue" | no | ||
overdue_only |
query | boolean | no | false |
Responses:
| Status | Description |
|---|---|
| 200 | Paginated borrow records. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the record's owner, a Librarian, or an Admin.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | integer | yes |
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
extension_days |
integer | no |
Responses:
| Status | Description |
|---|---|
| 200 | Due date extended. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
| 404 | Resource not found |
| 409 | Record is not currently active (already returned). |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role.
| Param | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
page |
query | integer | no | 1 |
|
limit |
query | integer | no | 10 |
Responses:
| Status | Description |
|---|---|
| 200 | Paginated overdue borrow records. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}Access: Authenticated (Bearer JWT)
Requires the Admin or Librarian role.
Responses:
| Status | Description |
|---|---|
| 200 | Aggregate borrowing statistics. |
| 401 | Missing, invalid, or expired token |
| 403 | Authenticated, but wrong role or not the resource owner |
// 200 — Aggregate borrowing statistics.
{
"success": true,
"message": "Borrowing statistics retrieved successfully",
"data": {
"total_borrows": 120,
"active_borrows": 34,
"returned_borrows": 80,
"overdue_borrows": 6,
"avg_borrow_days": 12.4,
"generated_at": "2026-08-19T12:00:00.000Z"
}
}// 401 — Missing, invalid, or expired token
{
"success": false,
"message": "Access denied. No token provided"
}// 403 — Authenticated, but wrong role or not the resource owner
{
"success": false,
"message": "Access denied. Required role(s): Admin. Your role: User"
}