mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 02:53:58 -06:00
Flip API key logic: no key = browser-only, add confirmation gates
- No API key configured: external API access blocked, browser-only - API key generated: external access enabled with that key - Added 'Sure?' confirmation on Regenerate and Revoke buttons (3s timeout) - Updated hint text to reflect security-first default
This commit is contained in:
parent
9b4ffbb754
commit
b0d2781980
2 changed files with 54 additions and 21 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ const passwordError = ref<string | null>(null)
|
|||
const apiKey = ref<string | null>(null)
|
||||
const showApiKey = ref(false)
|
||||
const apiKeyCopied = ref(false)
|
||||
const regenConfirming = ref(false)
|
||||
const revokeConfirming = ref(false)
|
||||
let regenConfirmTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let revokeConfirmTimer: ReturnType<typeof setTimeout> | 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 {
|
|||
<div class="settings-field">
|
||||
<label>API Key</label>
|
||||
<p class="field-hint">
|
||||
When set, external API access requires this key via <code>X-API-Key</code> 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.
|
||||
</p>
|
||||
<div v-if="apiKey" class="api-key-display">
|
||||
<div class="api-key-value">
|
||||
|
|
@ -633,13 +659,25 @@ function formatFilesize(bytes: number | null): string {
|
|||
</button>
|
||||
</div>
|
||||
<div class="api-key-actions">
|
||||
<button class="btn-regen" @click="regenerateApiKey">Regenerate</button>
|
||||
<button class="btn-revoke" @click="revokeApiKey">Revoke</button>
|
||||
<button
|
||||
class="btn-regen"
|
||||
:class="{ 'btn-confirm': regenConfirming }"
|
||||
@click="handleRegenClick"
|
||||
>
|
||||
{{ regenConfirming ? 'Sure?' : 'Regenerate' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn-revoke"
|
||||
:class="{ 'btn-confirm': revokeConfirming }"
|
||||
@click="handleRevokeClick"
|
||||
>
|
||||
{{ revokeConfirming ? 'Sure?' : 'Revoke' }}
|
||||
</button>
|
||||
</div>
|
||||
<span v-if="apiKeyCopied" class="save-confirm">✓ Copied</span>
|
||||
</div>
|
||||
<div v-else class="api-key-empty">
|
||||
<p class="field-hint" style="margin-bottom: var(--space-sm);">No API key set — download API is open.</p>
|
||||
<p class="field-hint" style="margin-bottom: var(--space-sm);">No API key set — external API access is disabled.</p>
|
||||
<button class="btn-save" @click="generateApiKey">Generate API Key</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue