Angemeldet bleiben: /workspace/ zeigte die Anmeldung trotz Sitzung
Filipe: „ich will dass wenn sich jemand bei der workspace seite
anmeldet, angemeldet bleibt bis er sich selbst abmeldet."
ERST GEMESSEN, DANN GEBAUT -- und das hat die Richtung gedreht.
Die naheliegende Antwort waere gewesen, die Sitzung zu verlaengern.
Sie war aber nie zu kurz:
* Der Keks gilt 180 Tage und verlaengert sich beim Benutzen.
* In der echten Datenbank lief KEINE der 28 Sitzungen vor Maerz
2027 ab.
* Am echten Browser nachgestellt: Er uebersteht dreimaliges
Neuladen, den Service Worker UND das Schliessen des Browsers.
Trotzdem meldeten sich dieselben Geraete staendig neu an. DogFathers
Android am 05.10. um 22:54 UND um 22:55 -- gleiche IP, gleiche
Browserkennung, eine Minute auseinander. Kein zweites Geraet.
DER GRUND: `/workspace/` lieferte die Anmeldung aus, OHNE zu fragen,
ob schon jemand angemeldet ist. Das ist die Adresse, die man tippt,
als Lesezeichen hat und weitergibt. Wer sie oeffnete, sah das
Codefeld und tippte den Code noch einmal -- obwohl seine Sitzung die
ganze Zeit gueltig war. Jede Eingabe legt eine neue Sitzung an;
deshalb standen sieben davon bei einer Person.
Haette ich die Sitzungsdauer erhoeht, waere der Fehler geblieben und
die Sicherheit schlechter geworden.
(Die installierte App startet auf `start.html` und war nie betroffen.
Es traf nur den Weg ueber die Adresszeile -- also den haeufigsten.)
WARUM DIE WEITERLEITUNG AUF DEM SERVER STEHT
Mein erster Entwurf stand in gate.js und fragte `/api/ich`. Er
funktionierte und war trotzdem die schlechtere Loesung:
* Ohne Sitzung antwortet `/api/ich` mit 401 -- danach stand bei
JEDEM Aufruf der Anmeldung ein Fehler in der Browserkonsole.
`pruef-neue-seiten` wurde davon zehnmal rot, und eine Meldung,
die immer kommt, liest nach dem zweiten Mal niemand mehr.
* Die Seite musste waehrend der Abfrage verdeckt werden, und diese
Verdeckung brauchte wieder eine Notfrist, damit sie bei
schlechtem Netz nicht haengen bleibt.
* Und es brauchte eine Schleifensperre mit Zeitstempel in zwei
Dateien -- deren erste Fassung prompt jeden ZWEITEN Aufruf auf
der Anmeldung stehen liess. Die Pruefung hat genau das gefunden.
Drei Hilfskonstruktionen fuer etwas, das der Server in einer Zeile
weiss: Er liest die Sitzung ohnehin bei jeder Anfrage. Jetzt sieht
man die Anmeldung gar nicht erst, es gibt keinen Fehler in der
Konsole, und es geht auch ohne JavaScript. Die drei Hilfskonstruktionen
sind wieder entfernt; gate.css ist unveraendert.
BEIDE HAEUSER, OHNE SIE ZU VERMISCHEN: Auf der Crew-Adresse liefert
`/workspace/` die Datei crew-index.html -- derselbe Weg, dasselbe
Ziel. Der Keks bleibt host-gebunden: Wer nur im anderen Haus
angemeldet ist, bekommt weiterhin die Anmeldung, genau wie es die
Haustrennung vom 24.09.2026 verlangt. pruef-crew-adresse bleibt gruen.
NEU: pruef-angemeldet-bleiben.mjs (22 Pruefungen, 0 Fehler)
Mit einem echten Browserprofil auf der Platte -- nur so ueberlebt ein
Keks das Schliessen des Browsers; ein gewoehnlicher Kontext vergisst
alles, und dann maesse die Pruefung ihre eigene Einrichtung.
DIE WICHTIGSTE ZEILE IST DIE ZAHL DER SITZUNGEN. „Man bleibt
angemeldet" laesst sich leicht vortaeuschen: Wer bei jedem Besuch
stillschweigend neu anmeldet, sieht auch nie ein Codefeld. Deshalb
wird durchgehend gezaehlt -- es bleibt bei EINER, ueber drei Aufrufe
und einen Browserneustart hinweg.
Dazu die Gegenprobe in beide Richtungen: Abmelden beendet es
wirklich (die Sitzung ist weg, die Anmeldung bleibt stehen, kein Hin
und Her), und ohne Keks fuehrt `/workspace/` nicht zur Startseite --
die Messung kann also auch Nein sagen.
DREI MEINER EIGENEN MESSUNGEN WAREN ZUERST FALSCH, nicht der Code:
Die Pruefung klickte „Abmelden" ohne die Rueckfrage zu bestaetigen
und meldete dann „die Sitzung ist nicht weg"; ihre Knopfauswahl traf
einen unsichtbaren Knopf und lief 30 Sekunden in eine
Zeitueberschreitung; und die erste Schleifensperre war zu grob.
Alle drei berichtigt, bevor sie als Befund durchgingen.
NICHT VON MIR: pruef-css-klassen meldet weiterhin 44 statt 42
Schriftgroessen unter 11,5 px (uebersicht.css, wissen.css) -- derselbe
aeltere Befund wie gestern.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
/* =====================================================================
|
||||
ANGEMELDET BLEIBEN, BIS MAN SICH SELBST ABMELDET (06.10.2026)
|
||||
=====================================================================
|
||||
|
||||
Filipe: „ich will dass wenn sich jemand bei der workspace seite
|
||||
anmeldet, angemeldet bleibt bis er sich selbst abmeldet."
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WAS DIE MESSUNG VORHER ERGEBEN HAT -- und warum die naheliegende
|
||||
Antwort die falsche gewesen waere
|
||||
|
||||
Die naheliegende Antwort waere gewesen, die Sitzung zu verlaengern.
|
||||
Sie war aber nie zu kurz:
|
||||
|
||||
* Der Keks gilt 180 Tage und verlaengert sich beim Benutzen.
|
||||
* In der echten Datenbank lief KEINE der 28 Sitzungen vor Maerz
|
||||
2027 ab.
|
||||
* Am echten Browser: Er uebersteht Neuladen, den Service Worker
|
||||
und das Schliessen des Browsers.
|
||||
|
||||
Trotzdem meldeten sich dieselben Geraete staendig neu an --
|
||||
dieselbe IP, dieselbe Browserkennung, einmal zweimal innerhalb
|
||||
einer Minute. Der Grund lag woanders: `/workspace/` zeigte die
|
||||
Anmeldung, OHNE zu fragen, ob schon jemand angemeldet ist. Das ist
|
||||
die Adresse, die man tippt und weitergibt.
|
||||
|
||||
Haette ich die Sitzungsdauer erhoeht, waere der Fehler geblieben
|
||||
und die Sicherheit schlechter geworden.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DIE WICHTIGSTE PRUEFUNG IST DIE ZAHL DER SITZUNGEN
|
||||
|
||||
„Man bleibt angemeldet" laesst sich leicht vortaeuschen: Wer bei
|
||||
jedem Besuch stillschweigend neu anmeldet, sieht auch nie ein
|
||||
Codefeld. Deshalb wird hier durchgehend gezaehlt, wie viele
|
||||
Sitzungen in der Datenbank stehen. Bleibt es bei EINER, ist
|
||||
niemand neu angemeldet worden.
|
||||
===================================================================== */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-ang-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
const { eigenerPort } = await import("./helfer-port.mjs");
|
||||
const PORT = await eigenerPort(import.meta, "die Anmeldepruefung");
|
||||
|
||||
process.env.PORT = String(PORT);
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) {
|
||||
return ec.call(this, n, w, { ...(o || {}), secure: false });
|
||||
};
|
||||
await import("./index.js");
|
||||
notbremse(240_000, "pruef-angemeldet-bleiben");
|
||||
await new Promise((r) => setTimeout(r, 900));
|
||||
|
||||
let fehler = 0, geprueft = 0;
|
||||
const melde = (t) => console.log(t);
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
function braucht(bedingung, was) {
|
||||
if (bedingung) return;
|
||||
console.log(`\nABBRUCH: konnte nicht nachsehen — ${was}\n`);
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
/* ---------- Ein Zugang ------------------------------------------- */
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const kenn = randomBytes(32).toString("hex");
|
||||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||||
.run("code_kennung_schluessel", kenn, new Date().toISOString());
|
||||
const CODE = "CODE-BLEIB-0001";
|
||||
const salz = randomBytes(16).toString("hex");
|
||||
const h = scryptSync(CODE, salz, 64,
|
||||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n,"
|
||||
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
|
||||
.run("Bleibt", "scout", h, salz, 32768,
|
||||
createHmac("sha256", kenn).update(CODE).digest("hex"), "2025-10-01T08:00:00Z");
|
||||
d.close();
|
||||
|
||||
const sitzungen = () => {
|
||||
const db2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
|
||||
const n = db2.prepare("SELECT COUNT(*) n FROM sitzungen").get().n;
|
||||
db2.close();
|
||||
return n;
|
||||
};
|
||||
|
||||
/* ---------- Ein Browser MIT Profil auf der Platte -----------------
|
||||
Nur so ueberlebt ein Keks das Schliessen des Browsers. Ein
|
||||
gewoehnlicher Kontext vergisst alles -- dann maesse diese Pruefung
|
||||
ihre eigene Einrichtung statt der Software. */
|
||||
const pw = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const profil = join(ordner, "profil");
|
||||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||||
const auf = () => pw.chromium.launchPersistentContext(profil, {
|
||||
viewport: { width: 412, height: 800 },
|
||||
});
|
||||
|
||||
/** Wohin fuehrt diese Adresse -- und ist die Seite danach sichtbar? */
|
||||
async function hin(seite, pfad) {
|
||||
await seite.goto(BASIS + pfad, { waitUntil: "networkidle" });
|
||||
/* Laenger als die feste Frist im Skript (900 ms): Sonst misst diese
|
||||
Pruefung den Zwischenzustand und nicht das Ergebnis. */
|
||||
await seite.waitForTimeout(1300);
|
||||
return {
|
||||
weg: seite.url().replace(BASIS, ""),
|
||||
sichtbar: await seite.evaluate(() => getComputedStyle(document.body).visibility),
|
||||
codefeld: await seite.evaluate(() => !!document.getElementById("code")),
|
||||
};
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
1 · OHNE ANMELDUNG
|
||||
===================================================================== */
|
||||
melde("\n=== Ohne Anmeldung ===");
|
||||
let kontext = await auf();
|
||||
let seite = await kontext.newPage();
|
||||
let r = await hin(seite, "/workspace/");
|
||||
ok(r.weg === "/workspace/", `die Anmeldung bleibt stehen (${r.weg})`);
|
||||
ok(r.codefeld, "das Codefeld ist da");
|
||||
/* DIE WICHTIGSTE ZEILE DIESES ABSCHNITTS. Die Verdeckung waehrend der
|
||||
Abfrage darf NIE haengenbleiben -- sonst koennte sich niemand mehr
|
||||
anmelden, und zwar genau dann, wenn das Netz schlecht ist. */
|
||||
ok(r.sichtbar === "visible", `und die Seite ist sichtbar (${r.sichtbar})`);
|
||||
ok(sitzungen() === 0, `noch keine Sitzung in der Datenbank (${sitzungen()})`);
|
||||
|
||||
/* =====================================================================
|
||||
2 · ANMELDEN
|
||||
===================================================================== */
|
||||
melde("\n=== Anmelden ===");
|
||||
await seite.click('.rolle[data-rolle="scout"]');
|
||||
await seite.fill("#code", CODE);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
ok(sitzungen() === 1, `genau eine Sitzung entstanden (${sitzungen()})`);
|
||||
|
||||
const keks = (await kontext.cookies()).find((c) => c.name === "dfw_sitzung");
|
||||
braucht(!!keks, "der Sitzungskeks wurde gar nicht gesetzt");
|
||||
const tage = Math.round((keks.expires * 1000 - Date.now()) / 86400000);
|
||||
ok(tage > 170, `der Keks ueberlebt das Schliessen des Browsers (${tage} Tage)`);
|
||||
|
||||
/* =====================================================================
|
||||
3 · DIE ADRESSE, DIE MAN TIPPT
|
||||
=====================================================================
|
||||
Genau hier lag der Fehler: `/workspace/` zeigte die Anmeldung,
|
||||
obwohl die Sitzung gueltig war.
|
||||
===================================================================== */
|
||||
melde("\n=== Die Adresse, die man tippt ===");
|
||||
for (const runde of [1, 2, 3]) {
|
||||
r = await hin(seite, "/workspace/");
|
||||
ok(r.weg === "/workspace/start.html",
|
||||
`Aufruf ${runde}: fuehrt direkt zur Startseite (${r.weg})`);
|
||||
ok(!r.codefeld, `Aufruf ${runde}: kein Codefeld`);
|
||||
}
|
||||
ok(sitzungen() === 1,
|
||||
`nach drei Aufrufen immer noch EINE Sitzung (${sitzungen()})`
|
||||
+ " — niemand wurde stillschweigend neu angemeldet");
|
||||
|
||||
/* =====================================================================
|
||||
4 · BROWSER ZU UND WIEDER AUF
|
||||
===================================================================== */
|
||||
melde("\n=== Browser schliessen und neu oeffnen ===");
|
||||
await kontext.close();
|
||||
kontext = await auf();
|
||||
seite = await kontext.newPage();
|
||||
r = await hin(seite, "/workspace/");
|
||||
ok(r.weg === "/workspace/start.html", `auch danach direkt zur Startseite (${r.weg})`);
|
||||
ok(sitzungen() === 1, `und immer noch eine einzige Sitzung (${sitzungen()})`);
|
||||
|
||||
/* =====================================================================
|
||||
5 · ABMELDEN -- und dann muss Schluss sein
|
||||
=====================================================================
|
||||
Die Gegenprobe zu allem davor. Ohne sie hiesse „man bleibt
|
||||
angemeldet" moeglicherweise „man kommt gar nicht mehr heraus".
|
||||
===================================================================== */
|
||||
melde("\n=== Abmelden ===");
|
||||
await seite.goto(`${BASIS}/workspace/start.html`, { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(900);
|
||||
const abmelden = await seite.$("#abmelden");
|
||||
braucht(!!abmelden, "der Abmelden-Knopf ist nicht da");
|
||||
await abmelden.click();
|
||||
/* ABMELDEN FRAGT ERST NACH (kopf.js, seit 19.09.2026) -- und das aus
|
||||
gutem Grund: Zum Wiederanmelden braucht man den persoenlichen
|
||||
Zugangscode, und den bekommt jeder nur einmal.
|
||||
|
||||
Beim ersten Lauf hat diese Pruefung nur geklickt und dann gemeldet
|
||||
„die Sitzung ist nicht weg". Das war kein Befund am Code, sondern
|
||||
eine Pruefung, die auf halbem Weg stehen blieb. */
|
||||
await seite.waitForTimeout(600);
|
||||
/* GENAU der Bestaetigungsknopf. Beim ersten Anlauf stand hier eine
|
||||
Auswahl mit Komma, und die traf den erstbesten `.knopf` im Dialog
|
||||
-- das war „Bild waehlen", unsichtbar, und der Klick lief 30
|
||||
Sekunden in eine Zeitueberschreitung. */
|
||||
const bestaetigen = await seite.$('dialog[open] [data-teil="ja"]');
|
||||
braucht(!!bestaetigen, "die Rueckfrage beim Abmelden kam nicht");
|
||||
await bestaetigen.click();
|
||||
await seite.waitForTimeout(1800);
|
||||
ok(sitzungen() === 0, `die Sitzung ist wirklich weg (${sitzungen()})`);
|
||||
|
||||
r = await hin(seite, "/workspace/");
|
||||
ok(r.weg === "/workspace/", `danach steht die Anmeldung da (${r.weg})`);
|
||||
ok(r.codefeld, "mit Codefeld");
|
||||
ok(r.sichtbar === "visible", `und sichtbar (${r.sichtbar})`);
|
||||
|
||||
/* Und sie bleibt stehen -- kein Hin und Her zwischen zwei Seiten. */
|
||||
r = await hin(seite, "/workspace/");
|
||||
ok(r.weg === "/workspace/" && r.codefeld,
|
||||
"auch beim zweiten Aufruf — kein Hin und Her");
|
||||
|
||||
/* =====================================================================
|
||||
6 · GEGENPROBE: MISST DIESE PRUEFUNG UEBERHAUPT ETWAS?
|
||||
=====================================================================
|
||||
Wenn „fuehrt zur Startseite" auch ohne gueltige Sitzung herauskaeme,
|
||||
waere der ganze Abschnitt 3 wertlos. Also noch einmal anmelden, den
|
||||
Keks von Hand wegnehmen und nachsehen, ob die Messung es merkt.
|
||||
===================================================================== */
|
||||
melde("\n=== Gegenprobe ===");
|
||||
await seite.goto(`${BASIS}/workspace/`, { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(900);
|
||||
await seite.click('.rolle[data-rolle="scout"]');
|
||||
await seite.fill("#code", CODE);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
r = await hin(seite, "/workspace/");
|
||||
ok(r.weg === "/workspace/start.html", "angemeldet: fuehrt zur Startseite");
|
||||
|
||||
await kontext.clearCookies();
|
||||
r = await hin(seite, "/workspace/");
|
||||
ok(r.weg === "/workspace/" && r.codefeld,
|
||||
"ohne Keks: die Anmeldung — die Messung kann also auch Nein sagen");
|
||||
|
||||
await kontext.close();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
melde(`\n${geprueft} Pruefungen, ${fehler} Fehler`);
|
||||
process.exit(fehler ? 1 : 0);
|
||||
@@ -6984,6 +6984,64 @@ export function gehoertAufDieseAdresse(person, pfad) {
|
||||
return ziele.has(pfad);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
WER SCHON ANGEMELDET IST, SIEHT DIE ANMELDUNG NICHT (06.10.2026)
|
||||
=====================================================================
|
||||
|
||||
Filipe: „ich will dass wenn sich jemand bei der workspace seite
|
||||
anmeldet, angemeldet bleibt bis er sich selbst abmeldet."
|
||||
|
||||
GEMESSEN, BEVOR ETWAS GEBAUT WURDE -- und das hat die Richtung
|
||||
gedreht. Die Sitzung war nie zu kurz:
|
||||
|
||||
* Der Keks gilt 180 Tage und verlaengert sich beim Benutzen.
|
||||
* In der echten Datenbank lief KEINE der 28 Sitzungen vor
|
||||
Maerz 2027 ab.
|
||||
* Am echten Browser: Er uebersteht Neuladen, den Service Worker
|
||||
und das Schliessen des Browsers.
|
||||
|
||||
Trotzdem meldeten sich dieselben Geraete staendig neu an --
|
||||
DogFathers Android am 05.10. um 22:54 UND um 22:55, gleiche IP,
|
||||
gleiche Browserkennung. Der Grund: `/workspace/` lieferte die
|
||||
Anmeldung aus, ohne zu fragen, ob schon jemand angemeldet ist.
|
||||
Das ist die Adresse, die man tippt, als Lesezeichen hat und
|
||||
weitergibt. Wer sie oeffnete, sah das Codefeld und tippte den Code
|
||||
noch einmal -- obwohl seine Sitzung gueltig war. Jede Eingabe legt
|
||||
eine neue Sitzung an; deshalb standen sieben davon bei einer
|
||||
Person.
|
||||
|
||||
Haette ich die Sitzungsdauer erhoeht -- die naheliegende Antwort --,
|
||||
waere der Fehler geblieben und die Sicherheit schlechter geworden.
|
||||
|
||||
(Die installierte App startet auf `start.html` und war nie
|
||||
betroffen. Es traf nur den Weg ueber die Adresszeile, also den
|
||||
haeufigsten.)
|
||||
|
||||
WARUM AUF DEM SERVER UND NICHT IM BROWSER: Der erste Entwurf stand
|
||||
in gate.js und fragte `/api/ich`. Das brauchte eine Verdeckung
|
||||
gegen das Aufblitzen, eine Notfrist gegen eine haengende Abfrage
|
||||
und eine Schleifensperre mit Zeitstempel in zwei Dateien -- und
|
||||
hinterliess bei jedem Aufruf der Anmeldung ein 401 in der
|
||||
Browserkonsole (pruef-neue-seiten wurde davon zehnmal rot). Hier
|
||||
ist es eine Zeile: Die Sitzung wird ohnehin bei jeder Anfrage
|
||||
gelesen.
|
||||
|
||||
KEINE SCHLEIFENGEFAHR: Schickt die Startseite jemanden zurueck
|
||||
(nur bei einer Sitzung, die zwischendurch ungueltig wurde), findet
|
||||
diese Stelle keine Sitzung mehr und liefert die Anmeldung aus.
|
||||
|
||||
BEIDE HAEUSER: Auf der Crew-Adresse liefert `/workspace/` die
|
||||
Datei crew-index.html; der Weg dorthin ist derselbe, und das Ziel
|
||||
`start.html` gilt dort ebenso. Der Keks ist host-gebunden -- wer
|
||||
nur im anderen Haus angemeldet ist, bekommt hier weiterhin die
|
||||
Anmeldung, genau wie es die Haustrennung vom 24.09.2026 verlangt. */
|
||||
workspaceRouter.get(
|
||||
["/workspace/", "/workspace/index.html", "/workspace/crew-index.html"],
|
||||
(req, res, next) => {
|
||||
if (!sitzungLesen(req)) return next();
|
||||
return res.redirect(302, "/workspace/start.html");
|
||||
});
|
||||
|
||||
workspaceRouter.use((req, res, next) => {
|
||||
const pfad = schrankenPfad(req.path);
|
||||
if (!pfad.startsWith("/workspace/") || !pfad.endsWith(".html")) return next();
|
||||
|
||||
Reference in New Issue
Block a user