feat(admin): invitationslinks med gruppe og udløbstid

Udrullet direkte til prod 2026-09-29; importeret her så repo == prod.
Bruger Authentik-flowet weircon-invitation (grupper og engangs via
fixed_data og politikker i flowet).

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
This commit is contained in:
agw
2026-09-29 17:13:43 +02:00
parent 9675a5b76b
commit 52b1a2bb6c
4 changed files with 195 additions and 0 deletions
+53
View File
@@ -271,6 +271,59 @@ async def admin_user_save(request: Request, pk: int):
return RedirectResponse(f"/admin/users?saved=Bruger gemt{note}", status_code=303)
INV_VARIGHED = [(30, "30 minutter"), (120, "2 timer"), (1440, "1 dag"), (4320, "3 dage"),
(10080, "7 dage"), (20160, "14 dage"), (43200, "30 dage")]
@app.get("/admin/invitations", response_class=HTMLResponse)
async def admin_invitations(request: Request, ny: str = "", saved: str = ""):
user = require_admin(request)
grupper = [g for g in await ak.list_groups() if g["name"] not in ak.INV_SKJUL]
navn = {str(g["pk"]): g["name"] for g in await ak.list_groups()}
invs = await ak.list_invitations()
from datetime import datetime, timezone
nu = datetime.now(timezone.utc)
for i in invs:
i["gruppenavne"] = [navn.get(str(x), "?") for x in i["grupper"]]
try:
rest = datetime.fromisoformat(i["expires"].replace("Z", "+00:00")) - nu
sek = int(rest.total_seconds())
i["udloebet"] = sek <= 0
i["rest"] = ("udløbet" if sek <= 0 else f"{sek // 86400} dage" if sek >= 86400
else f"{sek // 3600} timer" if sek >= 3600 else f"{max(sek // 60, 1)} min")
except Exception:
i["udloebet"], i["rest"] = False, "?"
nyt = next((i for i in invs if str(i["pk"]) == ny), None)
return templates.TemplateResponse(request, "admin_invitations.html", {
"request": request, "user": user, "is_admin": True, "grupper": grupper,
"invitationer": invs, "nyt": nyt, "varigheder": INV_VARIGHED, "saved": saved})
@app.post("/admin/invitations")
async def admin_invitations_create(request: Request):
require_admin(request)
f = await request.form()
grupper = f.getlist("grupper")
minutter = int(f.get("minutter") or 20160)
if minutter not in {m for m, _ in INV_VARIGHED}:
minutter = 20160
if not grupper:
return RedirectResponse("/admin/invitations?saved=Vælg mindst én gruppe", status_code=303)
ok, res = await ak.create_invitation((f.get("label") or "").strip(), grupper, minutter,
engangs=bool(f.get("engangs")))
if not ok:
return RedirectResponse(f"/admin/invitations?saved=Kunne ikke oprette: {res[:120]}", status_code=303)
return RedirectResponse(f"/admin/invitations?ny={res}", status_code=303)
@app.post("/admin/invitations/{pk}/slet")
async def admin_invitations_delete(request: Request, pk: str):
require_admin(request)
ok = await ak.delete_invitation(pk)
return RedirectResponse(f"/admin/invitations?saved={'Invitationen er tilbagekaldt' if ok else 'Kunne ikke tilbagekalde'}",
status_code=303)
@app.get("/admin/groups", response_class=HTMLResponse)
async def admin_groups(request: Request, saved: str = ""):
user = require_admin(request)