Ein neuer Link macht den alten und alle damit erteilten Zugänge ungültig.
+
+
+
+
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.
diff --git a/server/app/static/admin.js b/server/app/static/admin.js
index b6098bf..b6054b4 100644
--- a/server/app/static/admin.js
+++ b/server/app/static/admin.js
@@ -293,6 +293,8 @@ async function ladeZugang() {
const z = await api("/access");
setzeSchalter("s-zugang", z.enabled);
$("einladungslink").value = z.link;
+ $("link-basis").textContent = z.base;
+ setzeWert("link-eigen", z.token);
$("qr").src = "/admin/api/access/qr?t=" + Date.now();
}
@@ -426,6 +428,17 @@ function start() {
toast("Neuer Link erzeugt");
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 () => {
await aktion("/playlist", { link: $("playlist-neu").value }, "Playlist gewechselt").catch(() => {});
diff --git a/server/tests/test_admin.py b/server/tests/test_admin.py
index 23b3dad..eaaea25 100644
--- a/server/tests/test_admin.py
+++ b/server/tests/test_admin.py
@@ -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"]
+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):
anmelden(client)
w = wunsch(k="A")