diff --git a/backend/app/routers/downloads.py b/backend/app/routers/downloads.py index 75cdca6..78d7fa6 100644 --- a/backend/app/routers/downloads.py +++ b/backend/app/routers/downloads.py @@ -27,37 +27,32 @@ router = APIRouter(tags=["downloads"]) def _check_api_access(request: Request) -> None: """Verify the caller is a browser user or has a valid API key. - When no API key is configured, all requests are allowed (open access). - When an API key is set: - - Requests with a valid X-API-Key header pass. - - Requests from the web UI pass (have a Referer from the same origin - or an X-Requested-With header set by the frontend). - - All other requests are rejected with 403. + Browser users (X-Requested-With: XMLHttpRequest) always pass. + Non-browser callers must provide a valid X-API-Key header. + If no API key is configured, non-browser requests are blocked entirely. """ + # Browser users always pass + if request.headers.get("x-requested-with") == "XMLHttpRequest": + return + config = request.app.state.config api_key = config.server.api_key if not api_key: - return # No key configured — open access + # No key configured — block non-browser access + raise_api_key_required("API access is disabled. Generate an API key in the admin panel, then provide it via X-API-Key header.") # Check API key header provided_key = request.headers.get("x-api-key", "") if provided_key and secrets.compare_digest(provided_key, api_key): return - # Check browser origin — frontend sends X-Requested-With: XMLHttpRequest - if request.headers.get("x-requested-with") == "XMLHttpRequest": - return - raise_api_key_required() -def raise_api_key_required(): +def raise_api_key_required(detail: str = "Invalid or missing API key. Provide X-API-Key header."): from fastapi import HTTPException - raise HTTPException( - status_code=403, - detail="API key required. Provide X-API-Key header or use the web UI.", - ) + raise HTTPException(status_code=403, detail=detail) @router.post("/downloads", response_model=Job, status_code=201) diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue index 3eb65f0..181dcbf 100644 --- a/frontend/src/components/AdminPanel.vue +++ b/frontend/src/components/AdminPanel.vue @@ -47,6 +47,10 @@ const passwordError = ref(null) const apiKey = ref(null) const showApiKey = ref(false) const apiKeyCopied = ref(false) +const regenConfirming = ref(false) +const revokeConfirming = ref(false) +let regenConfirmTimer: ReturnType | null = null +let revokeConfirmTimer: ReturnType | null = null const canChangePassword = computed(() => currentPassword.value.length > 0 && @@ -193,6 +197,7 @@ async function generateApiKey() { async function regenerateApiKey() { await generateApiKey() + regenConfirming.value = false } async function revokeApiKey() { @@ -203,6 +208,27 @@ async function revokeApiKey() { showApiKey.value = false } } catch { /* ignore */ } + revokeConfirming.value = false +} + +function handleRegenClick() { + if (regenConfirming.value) { + regenerateApiKey() + return + } + regenConfirming.value = true + if (regenConfirmTimer) clearTimeout(regenConfirmTimer) + regenConfirmTimer = setTimeout(() => { regenConfirming.value = false }, 3000) +} + +function handleRevokeClick() { + if (revokeConfirming.value) { + revokeApiKey() + return + } + revokeConfirming.value = true + if (revokeConfirmTimer) clearTimeout(revokeConfirmTimer) + revokeConfirmTimer = setTimeout(() => { revokeConfirming.value = false }, 3000) } function copyApiKey() { @@ -618,8 +644,8 @@ function formatFilesize(bytes: number | null): string {

- When set, external API access requires this key via X-API-Key header. - Browser users are not affected. Without a key, the download API is open to anyone who can reach the server. + Generate a key to enable external API access (e.g. scripts, automation). + Without a key, downloads can only be submitted through the web UI.

@@ -633,13 +659,25 @@ function formatFilesize(bytes: number | null): string {
- - + +
✓ Copied
-

No API key set — download API is open.

+

No API key set — external API access is disabled.