Compare commits

...
2 Commits
Author SHA1 Message Date
agw 58471b1a4d fix(claude-login): siden opgiver ikke mens serveren starter login
CD / deploy (push) Successful in 20s
CI / check (push) Successful in 27s
Efter et tryk på Start kunne siden nå at læse den gamle hvile-status (fx
"Logget ind"), før serveren havde skrevet "starter". Så sprang den tilbage
til knappen og målte først igen efter 30 sekunder, og login-knappen kom
aldrig frem. Nu venter siden i op til et minut på en status, serveren har
skrevet efter trykket, og måler hvert 2. sekund imens.

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
2026-10-03 09:03:48 +02:00
agw 11399bb860 fix(auth): admin-sider sender til login i stedet for 403
CD / deploy (push) Successful in 19s
CI / check (push) Successful in 27s
Et link til fx /admin/claude-login (fra en ntfy) i en browser uden
portal-session gav "403 Kun for administratorer". Tre af dem på /admin fik
CrowdSec til at bandlyse IP'en som admin-probing, midt i et login.

Sidevisninger uden session sendes nu til /auth/login?next=<sti> og efter
login tilbage dertil; next accepteres kun som en sti på denne side.
Fetch- og POST-kald uden session får 401. Logget ind uden admin er
stadig 403. claude-login skriver nu brugernavnet i request-filen.

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
2026-10-03 08:56:24 +02:00
2 changed files with 25 additions and 3 deletions
+19 -3
View File
@@ -8,6 +8,7 @@ from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
import time import time
import urllib.parse
import authentik as ak import authentik as ak
import cache import cache
@@ -56,14 +57,29 @@ def is_admin(user) -> bool:
def require_admin(request: Request): def require_admin(request: Request):
user = current_user(request) user = current_user(request)
if not user and request.method == "GET" and "text/html" in request.headers.get("accept", ""):
# Ikke logget ind: login og tilbage hertil (fx et link fra en ntfy),
# i stedet for en 403-blindgyde.
nxt = request.url.path + (f"?{request.url.query}" if request.url.query else "")
raise HTTPException(303, headers={"Location": "/auth/login?next=" + urllib.parse.quote(nxt, safe="")})
if not user:
raise HTTPException(401, "Log ind først")
if not is_admin(user): if not is_admin(user):
raise HTTPException(403, "Kun for administratorer") raise HTTPException(403, "Kun for administratorer")
return user return user
def _sikker_next(nxt: str | None) -> str:
# Kun stier på denne side; "//host" og "/\\host" ville sende videre ud af huset.
if nxt and nxt.startswith("/") and not nxt.startswith(("//", "/\\")):
return nxt
return "/"
# ---------------- auth ---------------- # ---------------- auth ----------------
@app.get("/auth/login") @app.get("/auth/login")
async def login(request: Request): async def login(request: Request, next: str = ""):
request.session["next"] = _sikker_next(next)
return await oauth.authentik.authorize_redirect(request, config.REDIRECT_URI) return await oauth.authentik.authorize_redirect(request, config.REDIRECT_URI)
@@ -78,7 +94,7 @@ async def callback(request: Request):
"name": info.get("name") or info.get("preferred_username"), "name": info.get("name") or info.get("preferred_username"),
"username": info.get("preferred_username"), "email": info.get("email"), "username": info.get("preferred_username"), "email": info.get("email"),
"groups": info.get("groups") or []} "groups": info.get("groups") or []}
return RedirectResponse("/", status_code=302) return RedirectResponse(_sikker_next(request.session.pop("next", "/")), status_code=302)
@app.get("/auth/logout") @app.get("/auth/logout")
@@ -370,7 +386,7 @@ async def admin_claude_login_status(request: Request):
@app.post("/admin/claude-login/start") @app.post("/admin/claude-login/start")
async def admin_claude_login_start(request: Request): async def admin_claude_login_start(request: Request):
user = require_admin(request) user = require_admin(request)
claudelogin.request((user or {}).get("preferred_username") or "?") claudelogin.request((user or {}).get("username") or "?")
return JSONResponse(claudelogin.state()) return JSONResponse(claudelogin.state())
+6
View File
@@ -48,6 +48,9 @@
}; };
var $ = function (id) { return document.getElementById(id); }; var $ = function (id) { return document.getElementById(id); };
var timer = null; var timer = null;
// Efter et tryk på Start venter vi i op til et minut på en status, serveren har
// skrevet EFTER trykket; en ældre hvile-status betyder bare, at den ikke er nået dertil.
var klik = 0;
function dato(t) { function dato(t) {
var d = new Date(t * 1000); var d = new Date(t * 1000);
@@ -56,6 +59,8 @@
function vis(st) { function vis(st) {
var s = st.status || "ukendt"; var s = st.status || "ukendt";
var hvile = s === "ok" || s === "snart" || s === "udloebet" || s === "ukendt";
if (hvile && klik && Date.now() - klik < 60000 && (st.updated || 0) * 1000 < klik) s = "starter";
var linje = TEKST[s] || s; var linje = TEKST[s] || s;
if (st.expires && (s === "ok" || s === "snart")) { if (st.expires && (s === "ok" || s === "snart")) {
var dage = Math.floor((st.expires * 1000 - Date.now()) / 86400000); var dage = Math.floor((st.expires * 1000 - Date.now()) / 86400000);
@@ -93,6 +98,7 @@
$("cl-start-btn").addEventListener("click", function () { $("cl-start-btn").addEventListener("click", function () {
$("cl-start-btn").disabled = true; $("cl-start-btn").disabled = true;
klik = Date.now();
fetch("/admin/claude-login/start", { method: "POST", credentials: "same-origin" }) fetch("/admin/claude-login/start", { method: "POST", credentials: "same-origin" })
.then(function (r) { return r.json(); }).then(vis) .then(function (r) { return r.json(); }).then(vis)
.finally(function () { $("cl-start-btn").disabled = false; }); .finally(function () { $("cl-start-btn").disabled = false; });