Admin: eigenen Einladungslink festlegen
Statt des zufälligen Links kann der Admin einen sprechenden Link wählen (z. B. /z/sommerfest2026, mind. 6 Zeichen: A–Z, a–z, 0–9, - und _). Alte Zugänge werden dabei ungültig; der QR-Code passt sich an. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+19
-3
@@ -354,15 +354,19 @@ def neu_mischen(db: sqlite3.Connection = Depends(get_db)) -> dict:
|
|||||||
|
|
||||||
# --- Gastzugang (Einladungslink / QR-Code) ---------------------------------------
|
# --- Gastzugang (Einladungslink / QR-Code) ---------------------------------------
|
||||||
|
|
||||||
|
def _link_basis(request: Request) -> str:
|
||||||
|
return (konfig().public_url or str(request.base_url).rstrip("/")) + "/z/"
|
||||||
|
|
||||||
|
|
||||||
def _einladungslink(request: Request, token: str) -> str:
|
def _einladungslink(request: Request, token: str) -> str:
|
||||||
basis = konfig().public_url or str(request.base_url).rstrip("/")
|
return _link_basis(request) + token
|
||||||
return f"{basis}/z/{token}"
|
|
||||||
|
|
||||||
|
|
||||||
@api.get("/access")
|
@api.get("/access")
|
||||||
def zugang(request: Request, db: sqlite3.Connection = Depends(get_db)) -> dict:
|
def zugang(request: Request, db: sqlite3.Connection = Depends(get_db)) -> dict:
|
||||||
e = einstellungen(db)
|
e = einstellungen(db)
|
||||||
return {"enabled": e["access_enabled"], "link": _einladungslink(request, e["access_token"])}
|
return {"enabled": e["access_enabled"], "link": _einladungslink(request, e["access_token"]),
|
||||||
|
"base": _link_basis(request), "token": e["access_token"]}
|
||||||
|
|
||||||
|
|
||||||
class ZugangSchalter(BaseModel):
|
class ZugangSchalter(BaseModel):
|
||||||
@@ -382,6 +386,18 @@ def zugang_erneuern(db: sqlite3.Connection = Depends(get_db)) -> dict:
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
class EigenerLink(BaseModel):
|
||||||
|
# sprechend, aber nicht zu kurz (Raten wird zusätzlich durch die Einladungs-Drossel gebremst)
|
||||||
|
token: str = Field(pattern=r"^[A-Za-z0-9_-]{6,64}$")
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("/access/token")
|
||||||
|
def zugang_eigener_link(z: EigenerLink, db: sqlite3.Connection = Depends(get_db)) -> dict:
|
||||||
|
"""Eigenen Link festlegen (z. B. /z/sommerfest2026) – alte Zugänge werden ungültig."""
|
||||||
|
einstellung_setzen(db, "access_token", z.token)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@api.get("/access/qr")
|
@api.get("/access/qr")
|
||||||
def zugang_qr(request: Request, format: Literal["svg", "png"] = "svg",
|
def zugang_qr(request: Request, format: Literal["svg", "png"] = "svg",
|
||||||
db: sqlite3.Connection = Depends(get_db)) -> Response:
|
db: sqlite3.Connection = Depends(get_db)) -> Response:
|
||||||
|
|||||||
@@ -108,8 +108,16 @@
|
|||||||
<button id="link-kopieren" class="gross" type="button">Link kopieren</button>
|
<button id="link-kopieren" class="gross" type="button">Link kopieren</button>
|
||||||
<a id="qr-png" class="gross" href="/admin/api/access/qr?format=png" download>QR als Bild</a>
|
<a id="qr-png" class="gross" href="/admin/api/access/qr?format=png" download>QR als Bild</a>
|
||||||
</div>
|
</div>
|
||||||
<button id="link-erneuern" class="gross voll" type="button">Neuen Link erzeugen</button>
|
<button id="link-erneuern" class="gross voll" type="button">Neuen Link erzeugen (zufällig)</button>
|
||||||
<p class="hinweis klein">Ein neuer Link macht den alten und alle damit erteilten Zugänge ungültig.</p>
|
<label class="name-zeile">
|
||||||
|
<span>Eigener Link: <span id="link-basis"></span>…</span>
|
||||||
|
<input id="link-eigen" type="text" maxlength="64" autocomplete="off" autocapitalize="off" spellcheck="false"
|
||||||
|
placeholder="z. B. sommerfest2026" pattern="[A-Za-z0-9_\-]{6,64}">
|
||||||
|
</label>
|
||||||
|
<button id="link-festlegen" class="gross voll" type="button">Eigenen Link übernehmen</button>
|
||||||
|
<p class="hinweis klein">Ein geänderter Link macht den alten und alle damit erteilten Zugänge ungültig.
|
||||||
|
Eigener Link: mindestens 6 Zeichen, nur Buchstaben, Ziffern, - und _; Groß-/Kleinschreibung zählt.
|
||||||
|
Je kürzer und gebräuchlicher, desto leichter zu erraten – für eine Party ist z. B. „sommerfest2026“ in Ordnung.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="karte">
|
<div class="karte">
|
||||||
|
|||||||
@@ -293,6 +293,8 @@ async function ladeZugang() {
|
|||||||
const z = await api("/access");
|
const z = await api("/access");
|
||||||
setzeSchalter("s-zugang", z.enabled);
|
setzeSchalter("s-zugang", z.enabled);
|
||||||
$("einladungslink").value = z.link;
|
$("einladungslink").value = z.link;
|
||||||
|
$("link-basis").textContent = z.base;
|
||||||
|
setzeWert("link-eigen", z.token);
|
||||||
$("qr").src = "/admin/api/access/qr?t=" + Date.now();
|
$("qr").src = "/admin/api/access/qr?t=" + Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,6 +428,17 @@ function start() {
|
|||||||
toast("Neuer Link erzeugt");
|
toast("Neuer Link erzeugt");
|
||||||
ladeZugang();
|
ladeZugang();
|
||||||
});
|
});
|
||||||
|
$("link-festlegen").addEventListener("click", async () => {
|
||||||
|
const token = $("link-eigen").value.trim();
|
||||||
|
if (!/^[A-Za-z0-9_-]{6,64}$/.test(token)) {
|
||||||
|
toast("Mindestens 6 Zeichen: nur Buchstaben, Ziffern, - und _", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`Link auf „${token}“ ändern? Der alte Link und alle bisherigen Gastzugänge werden ungültig.`)) return;
|
||||||
|
await api("/access/token", { token });
|
||||||
|
toast("Eigener Link übernommen");
|
||||||
|
ladeZugang();
|
||||||
|
});
|
||||||
|
|
||||||
$("playlist-wechseln").addEventListener("click", async () => {
|
$("playlist-wechseln").addEventListener("click", async () => {
|
||||||
await aktion("/playlist", { link: $("playlist-neu").value }, "Playlist gewechselt").catch(() => {});
|
await aktion("/playlist", { link: $("playlist-neu").value }, "Playlist gewechselt").catch(() => {});
|
||||||
|
|||||||
@@ -211,6 +211,23 @@ def test_gastzugang_und_qr(client):
|
|||||||
assert png.content[:8] == b"\x89PNG\r\n\x1a\n" and "attachment" in png.headers["content-disposition"]
|
assert png.content[:8] == b"\x89PNG\r\n\x1a\n" and "attachment" in png.headers["content-disposition"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_eigener_einladungslink(client):
|
||||||
|
h = anmelden(client)
|
||||||
|
assert client.post("/admin/api/access/token", json={"token": "sommerfest2026"}, headers=h).status_code == 200
|
||||||
|
z = client.get("/admin/api/access").json()
|
||||||
|
assert z["link"] == "https://musik.example.de/z/sommerfest2026" and z["token"] == "sommerfest2026"
|
||||||
|
assert z["base"] == "https://musik.example.de/z/"
|
||||||
|
for falsch in ("kurz", "mit leerzeichen", "../../etc", "ümlaut1", "x" * 65, ""):
|
||||||
|
assert client.post("/admin/api/access/token", json={"token": falsch}, headers=h).status_code == 422, falsch
|
||||||
|
assert client.post("/admin/api/access/token", json={"token": "sommerfest2026"}).status_code == 403 # ohne CSRF
|
||||||
|
# Gäste kommen mit dem eigenen Link hinein, mit dem alten zufälligen nicht mehr
|
||||||
|
client.post("/admin/api/access", json={"enabled": True}, headers=h)
|
||||||
|
gast = TestClient(client.app)
|
||||||
|
assert gast.get("/z/sommerfest2026", follow_redirects=False).status_code == 303
|
||||||
|
assert gast.get("/api/status").status_code == 200
|
||||||
|
assert gast.get("/z/Sommerfest2026", follow_redirects=False).status_code == 403 # Groß/klein zählt
|
||||||
|
|
||||||
|
|
||||||
def test_verlauf(client):
|
def test_verlauf(client):
|
||||||
anmelden(client)
|
anmelden(client)
|
||||||
w = wunsch(k="A")
|
w = wunsch(k="A")
|
||||||
|
|||||||
Reference in New Issue
Block a user