Admin Endpoints
docsfy provides admin-only APIs for:
- user lifecycle management
- project access grants/revocations
- API key rotation (user keys via API,
ADMIN_KEYvia config)
Core route implementations live in src/docsfy/main.py, with persistence and validation in src/docsfy/storage.py.
Authentication and Required Configuration
Admin routes require request.state.is_admin. Middleware sets this when auth is one of:
Authorization: Bearer <ADMIN_KEY>Authorization: Bearer <user-api-key>where the DB user role isadmin- a valid admin
docsfy_sessioncookie
Note: Unauthenticated
/api/*calls return401with{"detail":"Unauthorized"}; authenticated non-admin calls to admin routes return403with{"detail":"Admin access required"}.
Environment configuration from .env.example:
# REQUIRED - Admin key for user management (minimum 16 characters)
ADMIN_KEY=your-secure-admin-key-here-min-16-chars
# Set to false for local HTTP development
# SECURE_COOKIES=false
Container runtime wiring from docker-compose.yaml:
services:
docsfy:
env_file: .env
volumes:
- ./data:/data
Endpoint Index
| Method | Path | Purpose |
|---|---|---|
GET |
/admin |
Admin UI page (HTML) |
POST |
/api/admin/users |
Create user (returns generated API key once) |
GET |
/api/admin/users |
List users |
DELETE |
/api/admin/users/{username} |
Delete user |
POST |
/api/admin/projects/{name}/access |
Grant project access |
GET |
/api/admin/projects/{name}/access |
List project access |
DELETE |
/api/admin/projects/{name}/access/{username} |
Revoke project access |
POST |
/api/admin/users/{username}/rotate-key |
Admin rotates a user key |
POST |
/api/me/rotate-key |
Logged-in user rotates own key |
User CRUD
Create User: POST /api/admin/users
Request JSON:
- username (required)
- role (optional, defaults to user; allowed: admin, user, viewer)
Actual request code from src/docsfy/templates/admin.html:
const resp = await fetch("/api/admin/users", {
method: "POST",
headers: {"Content-Type": "application/json"},
credentials: "same-origin",
redirect: "error",
body: JSON.stringify({username: username, role: role})
});
Actual success response from src/docsfy/main.py:
return JSONResponse(
content={"username": username, "api_key": raw_key, "role": role},
headers={"Cache-Control": "no-store"},
)
Validation behavior:
- username admin is reserved (case-insensitive)
- username regex: ^[a-zA-Z0-9][a-zA-Z0-9._-]{1,49}$
- invalid role -> 400
- missing username -> 400
- DB insert failures (for example duplicate username) -> 400
List Users: GET /api/admin/users
Returns:
- {"users": [...]}
Each row is selected as:
- id, username, role, created_at
api_key_hash is not returned.
Delete User: DELETE /api/admin/users/{username}
Actual request code from src/docsfy/templates/admin.html:
const resp = await fetch("/api/admin/users/" + encodeURIComponent(username), {
method: "DELETE",
credentials: "same-origin",
redirect: "error",
});
Success response:
- {"deleted":"<username>"}
Guardrails and side effects:
- admin cannot delete their own account (400)
- storage cleanup deletes that user’s sessions, owned projects (DB rows), and ACL entries where they are owner or grantee
Note: User management supports create/list/delete. There is no dedicated endpoint for username rename or role update in place.
Access Grant/Revoke/List
Access is owner-scoped: grants are keyed by project_name + project_owner + username, so grants apply to all variants for that project name under that owner.
Grant Access: POST /api/admin/projects/{name}/access
Request JSON:
- username (required)
- owner (required)
Route behavior:
- verifies user exists
- verifies project exists for that owner (list_variants(name, owner=owner))
- inserts grant via grant_project_access(...)
Example from test-plans/e2e-ui-test-plan.md:
fetch('/api/admin/projects/for-testing-only/access', { method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin', body: JSON.stringify({username: 'testviewer-e2e', owner: 'testuser-e2e'}) }).then(r => r.json()).then(d => JSON.stringify(d))
Success response shape:
- {"granted":"<name>","username":"<username>","owner":"<owner>"}
List Access: GET /api/admin/projects/{name}/access?owner=<owner>
Example from test-plans/e2e-ui-test-plan.md:
fetch('/api/admin/projects/for-testing-only/access?owner=testuser-e2e', {credentials:'same-origin'}).then(r => r.json())
Success response shape:
- {"project":"<name>","owner":"<owner>","users":[...]}
Revoke Access: DELETE /api/admin/projects/{name}/access/{username}?owner=<owner>
Example from test-plans/e2e-ui-test-plan.md:
fetch('/api/admin/projects/for-testing-only/access/testviewer-e2e?owner=testuser-e2e', {method:'DELETE', credentials:'same-origin'}).then(r => r.status)
Success response shape:
- {"revoked":"<name>","username":"<username>"}
Tip: Always pass
owneron revoke/list requests. The route reads owner from query params and applies owner-scoped ACL operations.
Key Rotation Operations
Rotate Own Key: POST /api/me/rotate-key
Available to authenticated DB users (admin, user, viewer).
Request JSON:
- optional new_key
- if omitted, server generates a new key
- if provided, minimum length is 16
Actual dashboard request from src/docsfy/templates/dashboard.html:
var resp = await fetch('/api/me/rotate-key', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'same-origin',
body: JSON.stringify(body),
});
Behavior:
- returns {"username":"<username>","new_api_key":"<key>"} with Cache-Control: no-store
- invalidates that user’s sessions
- deletes current docsfy_session cookie (forces re-login)
ADMIN_KEY super-admin sessions are explicitly rejected:
if request.state.is_admin and not request.state.user:
raise HTTPException(
status_code=400,
detail="ADMIN_KEY users cannot rotate keys. Change the ADMIN_KEY env var instead.",
)
Admin Rotate User Key: POST /api/admin/users/{username}/rotate-key
Admin-only endpoint to rotate another user’s key.
Request JSON:
- optional new_key (same min length rule)
Actual admin panel request from src/docsfy/templates/admin.html:
fetch('/api/admin/users/' + encodeURIComponent(username) + '/rotate-key', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'same-origin',
redirect: 'error',
body: JSON.stringify(body),
})
Behavior:
- success: {"username":"<username>","new_api_key":"<key>"} plus Cache-Control: no-store
- unknown user: 404
- invalid custom key: 400
- all sessions for the target user are invalidated by storage logic
Rotating ADMIN_KEY Itself (Config Operation)
There is no API endpoint for rotating ADMIN_KEY; this is done in environment config and service restart.
Startup guard from src/docsfy/main.py:
if not settings.admin_key:
logger.error("ADMIN_KEY environment variable is required")
raise SystemExit(1)
if len(settings.admin_key) < 16:
logger.error("ADMIN_KEY must be at least 16 characters long")
raise SystemExit(1)
HMAC linkage in src/docsfy/storage.py:
# NOTE: ADMIN_KEY is used as the HMAC secret. Rotating ADMIN_KEY will
# invalidate all existing api_key_hash values, requiring all users to
# regenerate their API keys.
secret = hmac_secret or os.getenv("ADMIN_KEY", "")
Warning: Rotating
ADMIN_KEYinvalidates all existing DB user API keys. After restart, log in asadminwith the new key and re-issue user keys (for example viaPOST /api/admin/users/{username}/rotate-key).
Verification Notes
This repository currently has no .github/workflows directory. Test automation entry point is tox.toml:
[env.unittests]
deps = ["uv"]
commands = [["uv", "run", "--extra", "dev", "pytest", "-n", "auto", "tests"]]
Relevant endpoint coverage is present in:
- tests/test_auth.py (reserved username, self-delete guard, key rotation behavior)
- tests/test_storage.py (ACL grant/revoke/list and cleanup behavior)
- test-plans/e2e-ui-test-plan.md (end-to-end admin/access API usage examples)