feat(admin): hemmeligheds-drop til Claude

/admin/secrets tager imod et token eller kodeord og krypterer det med det
samme med PVE-værtens offentlige X25519-nøgle (sealed box: HKDF-SHA256 +
ChaCha20-Poly1305, navnet som associated data). Kun ciphertext gemmes;
portalen kan ikke læse det igen. På værten henter secret-drop get <navn>
det én gang og sletter kopien her.

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
This commit is contained in:
agw
2026-09-30 21:59:59 +02:00
parent e05c376452
commit fd50bed5fd
4 changed files with 186 additions and 0 deletions
+29
View File
@@ -20,6 +20,7 @@ import ytdl as yt
import mcpkeys
import mcp_server
import mcp_gateway
import secretdrop
# Portalens egen MCP (FastMCP), monteret in-process på /mcp. json_response=True
# => almindelig JSON (ingen SSE), så det passerer edge'en utunet. Bearer-gated.
@@ -324,6 +325,34 @@ async def admin_invitations_delete(request: Request, pk: str):
status_code=303)
@app.get("/admin/secrets", response_class=HTMLResponse)
async def admin_secrets(request: Request, saved: str = "", gemt: str = ""):
user = require_admin(request)
return templates.TemplateResponse(request, "admin_secrets.html", {
"request": request, "user": user, "is_admin": True,
"ventende": secretdrop.pending(), "saved": saved, "gemt": gemt})
@app.post("/admin/secrets")
async def admin_secrets_store(request: Request):
user = require_admin(request)
f = await request.form()
navn = (f.get("navn") or "").strip().lower()
fejl = secretdrop.store(navn, f.get("vaerdi") or "", f.get("note") or "",
(user or {}).get("preferred_username") or (user or {}).get("email") or "?")
if fejl:
return RedirectResponse(f"/admin/secrets?saved={fejl}", status_code=303)
# Værdien sendes aldrig tilbage; kun navnet, så siden kan bekræfte det.
return RedirectResponse(f"/admin/secrets?gemt={navn}", status_code=303)
@app.post("/admin/secrets/{navn}/slet")
async def admin_secrets_delete(request: Request, navn: str):
require_admin(request)
ok = secretdrop.delete(navn)
return RedirectResponse(f"/admin/secrets?saved={'Slettet' if ok else 'Fandtes ikke'}", status_code=303)
@app.get("/admin/groups", response_class=HTMLResponse)
async def admin_groups(request: Request, saved: str = ""):
user = require_admin(request)