Admin Endpoints

docsfy provides admin-only APIs for:

  • user lifecycle management
  • project access grants/revocations
  • API key rotation (user keys via API, ADMIN_KEY via 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 is admin
  • a valid admin docsfy_session cookie

Note: Unauthenticated /api/* calls return 401 with {"detail":"Unauthorized"}; authenticated non-admin calls to admin routes return 403 with {"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 owner on 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_KEY invalidates all existing DB user API keys. After restart, log in as admin with the new key and re-issue user keys (for example via POST /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)