asas
This commit is contained in:
@@ -6,3 +6,9 @@
|
|||||||
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);
|
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);
|
||||||
define('APP_DB_ENABLED', true); // set true to enable DB connection
|
define('APP_DB_ENABLED', true); // set true to enable DB connection
|
||||||
|
|
||||||
|
// Crypto-Key für verschlüsselte Felder (Telefon, Kinder etc.)
|
||||||
|
// Bitte in Staging per Hosting-ENV setzen; dieses putenv dient nur als Fallback/Beispiel.
|
||||||
|
if (getenv('DATA_KEY') === false) {
|
||||||
|
// Beispiel-Key (unbedingt in Staging durch sicheren Wert ersetzen, 32 Byte, base64)
|
||||||
|
putenv('DATA_KEY=base64:TSLBgK39KnwqMGT+ytJ+O8FwpVm+99VYZwi97TeloBw=');
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,3 +6,9 @@
|
|||||||
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);
|
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);
|
||||||
define('APP_DB_ENABLED', true); // set true to enable DB connection
|
define('APP_DB_ENABLED', true); // set true to enable DB connection
|
||||||
|
|
||||||
|
// Crypto-Key für verschlüsselte Felder (Telefon, Kinder etc.)
|
||||||
|
// Bitte in Staging per Hosting-ENV setzen; dieses putenv dient nur als Fallback/Beispiel.
|
||||||
|
if (getenv('DATA_KEY') === false) {
|
||||||
|
// Beispiel-Key (unbedingt in Staging durch sicheren Wert ersetzen, 32 Byte, base64)
|
||||||
|
putenv('DATA_KEY=base64:FIanxMlz5/bn7Oyqv57BXVcFelqHV9qj3hkiTDyerls=');
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,32 +9,44 @@ $flash = $app->flash()->get();
|
|||||||
$userId = (int)$_SESSION['user_id'];
|
$userId = (int)$_SESSION['user_id'];
|
||||||
$error = '';
|
$error = '';
|
||||||
$info = '';
|
$info = '';
|
||||||
|
$crypto = null;
|
||||||
|
try { $crypto = new \App\Crypto($app->config()); } catch (\Throwable) {}
|
||||||
|
|
||||||
// POST Aktionen
|
// POST Aktionen
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
try {
|
try {
|
||||||
if ($action === 'profile') {
|
if ($action === 'profile') {
|
||||||
$stmt = $pdo->prepare('UPDATE user_profiles SET display_name=:name, zip=:zip, city=:city, profession=:prof, languages=:langs, about=:about, updated_at=NOW() WHERE user_id=:id');
|
$languages = $_POST['languages'] ?? '';
|
||||||
|
if (is_array($languages)) {
|
||||||
|
$languages = implode(', ', array_map('trim', $languages));
|
||||||
|
}
|
||||||
|
$phoneEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['contact_phone'])) : trim((string)$_POST['contact_phone']);
|
||||||
|
$stmt = $pdo->prepare('UPDATE user_profiles SET display_name=:name, first_name=:fname, last_name=:lname, zip=:zip, city=:city, profession=:prof, languages=:langs, about=:about, contact_phone=:phone, updated_at=NOW() WHERE user_id=:id');
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'name' => trim((string)$_POST['display_name']),
|
'name' => trim((string)$_POST['display_name']),
|
||||||
|
'fname' => trim((string)$_POST['first_name']),
|
||||||
|
'lname' => trim((string)$_POST['last_name']),
|
||||||
'zip' => trim((string)$_POST['zip']),
|
'zip' => trim((string)$_POST['zip']),
|
||||||
'city' => trim((string)$_POST['city']),
|
'city' => trim((string)$_POST['city']),
|
||||||
'prof' => trim((string)$_POST['profession']),
|
'prof' => trim((string)$_POST['profession']),
|
||||||
'langs' => trim((string)$_POST['languages']),
|
'langs' => trim((string)$languages),
|
||||||
'about' => trim((string)$_POST['about']),
|
'about' => trim((string)$_POST['about']),
|
||||||
|
'phone' => $phoneEnc,
|
||||||
'id' => $userId,
|
'id' => $userId,
|
||||||
]);
|
]);
|
||||||
$info = 'Profil gespeichert.';
|
$info = 'Profil gespeichert.';
|
||||||
} elseif ($action === 'child_add') {
|
} elseif ($action === 'child_add') {
|
||||||
|
$firstNameEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['first_name'])) : trim((string)$_POST['first_name']);
|
||||||
|
$noteEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['note'])) : trim((string)$_POST['note']);
|
||||||
$stmt = $pdo->prepare('INSERT INTO children (user_id, gender, birthdate, age_years, encrypted_first_name, note, created_at, updated_at) VALUES (:uid, :gender, :birthdate, :age, :name, :note, NOW(), NOW())');
|
$stmt = $pdo->prepare('INSERT INTO children (user_id, gender, birthdate, age_years, encrypted_first_name, note, created_at, updated_at) VALUES (:uid, :gender, :birthdate, :age, :name, :note, NOW(), NOW())');
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'uid' => $userId,
|
'uid' => $userId,
|
||||||
'gender' => $_POST['gender'] ?? 'unknown',
|
'gender' => $_POST['gender'] ?? 'unknown',
|
||||||
'birthdate' => $_POST['birthdate'] ?: null,
|
'birthdate' => $_POST['birthdate'] ?: null,
|
||||||
'age' => $_POST['age_years'] ?: null,
|
'age' => $_POST['age_years'] ?: null,
|
||||||
'name' => trim((string)$_POST['first_name']),
|
'name' => $firstNameEnc,
|
||||||
'note' => trim((string)$_POST['note']),
|
'note' => $noteEnc,
|
||||||
]);
|
]);
|
||||||
$info = 'Kind hinzugefügt.';
|
$info = 'Kind hinzugefügt.';
|
||||||
} elseif ($action === 'event_add') {
|
} elseif ($action === 'event_add') {
|
||||||
@@ -63,23 +75,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
// Daten laden
|
// Daten laden
|
||||||
$profile = [
|
$profile = [
|
||||||
'display_name' => '',
|
'display_name' => '',
|
||||||
|
'first_name' => '',
|
||||||
|
'last_name' => '',
|
||||||
'zip' => '',
|
'zip' => '',
|
||||||
'city' => '',
|
'city' => '',
|
||||||
'profession' => '',
|
'profession' => '',
|
||||||
'languages' => '',
|
'languages' => '',
|
||||||
'about' => '',
|
'about' => '',
|
||||||
|
'email' => '',
|
||||||
|
'contact_phone' => '',
|
||||||
];
|
];
|
||||||
$stmt = $pdo->prepare('SELECT u.email, u.status, p.display_name, p.zip, p.city, p.profession, p.languages, p.about FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1');
|
$stmt = $pdo->prepare('SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.zip, p.city, p.profession, p.languages, p.about, p.contact_phone FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1');
|
||||||
$stmt->execute(['id' => $userId]);
|
$stmt->execute(['id' => $userId]);
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
if ($row) {
|
if ($row) {
|
||||||
$profile = array_merge($profile, array_filter($row, fn($v) => $v !== null));
|
$profile = array_merge($profile, array_filter($row, fn($v) => $v !== null));
|
||||||
|
if ($crypto && !empty($profile['contact_phone'])) {
|
||||||
|
$profile['contact_phone'] = $crypto->decrypt((string)$profile['contact_phone']) ?: '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$children = [];
|
$children = [];
|
||||||
$stmt = $pdo->prepare('SELECT id, encrypted_first_name AS first_name, gender, birthdate, age_years FROM children WHERE user_id = :id ORDER BY id DESC');
|
$stmt = $pdo->prepare('SELECT id, encrypted_first_name AS first_name, note, gender, birthdate, age_years FROM children WHERE user_id = :id ORDER BY id DESC');
|
||||||
$stmt->execute(['id' => $userId]);
|
$stmt->execute(['id' => $userId]);
|
||||||
$children = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
$childrenRaw = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
foreach ($childrenRaw as $c) {
|
||||||
|
if ($crypto) {
|
||||||
|
$c['first_name'] = $crypto->decrypt((string)$c['first_name']) ?: '';
|
||||||
|
$c['note'] = $crypto->decrypt((string)($c['note'] ?? '')) ?: '';
|
||||||
|
}
|
||||||
|
$children[] = $c;
|
||||||
|
}
|
||||||
|
|
||||||
$events = [];
|
$events = [];
|
||||||
$stmt = $pdo->prepare('SELECT id, title, teaser_public, starts_at, city, visibility FROM events WHERE created_by = :id ORDER BY starts_at DESC');
|
$stmt = $pdo->prepare('SELECT id, title, teaser_public, starts_at, city, visibility FROM events WHERE created_by = :id ORDER BY starts_at DESC');
|
||||||
@@ -113,8 +139,11 @@ $events = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
|||||||
<div class="badge">Profil</div>
|
<div class="badge">Profil</div>
|
||||||
<h3>Deine Angaben</h3>
|
<h3>Deine Angaben</h3>
|
||||||
<ul class="dash-list">
|
<ul class="dash-list">
|
||||||
|
<li>Name: <?= htmlspecialchars(trim($profile['first_name'] . ' ' . $profile['last_name']), ENT_QUOTES) ?></li>
|
||||||
<li>Anzeigename: <?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?></li>
|
<li>Anzeigename: <?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?></li>
|
||||||
<li>Ort: <?= htmlspecialchars($profile['city'], ENT_QUOTES) ?> <?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?></li>
|
<li>Ort: <?= htmlspecialchars($profile['city'], ENT_QUOTES) ?> <?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?></li>
|
||||||
|
<li>E-Mail: <?= htmlspecialchars($profile['email'], ENT_QUOTES) ?></li>
|
||||||
|
<li>Telefon: <?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?></li>
|
||||||
<li>Beruf: <?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?></li>
|
<li>Beruf: <?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?></li>
|
||||||
<li>Sprachen: <?= htmlspecialchars($profile['languages'], ENT_QUOTES) ?></li>
|
<li>Sprachen: <?= htmlspecialchars($profile['languages'], ENT_QUOTES) ?></li>
|
||||||
<li>About: <?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></li>
|
<li>About: <?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></li>
|
||||||
@@ -180,6 +209,16 @@ $events = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
|||||||
<label class="label" for="pName">Anzeigename</label>
|
<label class="label" for="pName">Anzeigename</label>
|
||||||
<input id="pName" name="display_name" class="input" value="<?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?>">
|
<input id="pName" name="display_name" class="input" value="<?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?>">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="pFirst">Vorname</label>
|
||||||
|
<input id="pFirst" name="first_name" class="input" value="<?= htmlspecialchars($profile['first_name'], ENT_QUOTES) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="pLast">Nachname</label>
|
||||||
|
<input id="pLast" name="last_name" class="input" value="<?= htmlspecialchars($profile['last_name'], ENT_QUOTES) ?>">
|
||||||
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="pCity">Ort</label>
|
<label class="label" for="pCity">Ort</label>
|
||||||
<input id="pCity" name="city" class="input" value="<?= htmlspecialchars($profile['city'], ENT_QUOTES) ?>">
|
<input id="pCity" name="city" class="input" value="<?= htmlspecialchars($profile['city'], ENT_QUOTES) ?>">
|
||||||
@@ -191,13 +230,30 @@ $events = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
|||||||
<input id="pZip" name="zip" class="input" value="<?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?>">
|
<input id="pZip" name="zip" class="input" value="<?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?>">
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="pProf">Beruf</label>
|
<label class="label" for="pPhone">Telefon (mobil)</label>
|
||||||
<input id="pProf" name="profession" class="input" value="<?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?>">
|
<input id="pPhone" name="contact_phone" class="input" value="<?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?>">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="pLang">Sprachen</label>
|
<label class="label">Sprachen (Mehrfachauswahl)</label>
|
||||||
<input id="pLang" name="languages" class="input" value="<?= htmlspecialchars($profile['languages'], ENT_QUOTES) ?>">
|
<div class="chips" style="flex-wrap: wrap;">
|
||||||
|
<?php
|
||||||
|
$langOptions = ['Deutsch','Englisch','Französisch','Spanisch','Türkisch','Arabisch','Polnisch'];
|
||||||
|
$currentLangs = array_filter(array_map('trim', explode(',', (string)$profile['languages'])));
|
||||||
|
?>
|
||||||
|
<?php foreach ($langOptions as $opt): ?>
|
||||||
|
<label class="chip" style="cursor:pointer;">
|
||||||
|
<input type="checkbox" name="languages[]" value="<?= htmlspecialchars($opt, ENT_QUOTES) ?>" <?= in_array($opt, $currentLangs, true) ? 'checked' : '' ?> style="margin-right:6px;">
|
||||||
|
<?= htmlspecialchars($opt, ENT_QUOTES) ?>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<label class="label" for="pLangCustom">Weitere Sprachen (Kommagetrennt)</label>
|
||||||
|
<input id="pLangCustom" name="languages[]" class="input" placeholder="z. B. Italienisch, Niederländisch">
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="pProf">Beruf</label>
|
||||||
|
<input id="pProf" name="profession" class="input" value="<?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?>">
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="pAbout">Kurzvorstellung</label>
|
<label class="label" for="pAbout">Kurzvorstellung</label>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ $isLoggedIn = isset($_SESSION['user_id']);
|
|||||||
<div class="nav-actions">
|
<div class="nav-actions">
|
||||||
<?php if ($isLoggedIn): ?>
|
<?php if ($isLoggedIn): ?>
|
||||||
<a class="btn ghost" href="/dashboard">Dashboard</a>
|
<a class="btn ghost" href="/dashboard">Dashboard</a>
|
||||||
<a class="btn" href="/dashboard#events">Neues Event</a>
|
<a class="btn ghost" href="/logout">Logout</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<a class="btn ghost" href="/login">Anmelden</a>
|
<a class="btn ghost" href="/login">Anmelden</a>
|
||||||
<a class="btn" href="/register">Kostenlos registrieren</a>
|
<a class="btn" href="/register">Kostenlos registrieren</a>
|
||||||
@@ -35,7 +35,7 @@ $isLoggedIn = isset($_SESSION['user_id']);
|
|||||||
<a href="/#faq">FAQ</a>
|
<a href="/#faq">FAQ</a>
|
||||||
<?php if ($isLoggedIn): ?>
|
<?php if ($isLoggedIn): ?>
|
||||||
<a class="btn ghost" href="/dashboard">Dashboard</a>
|
<a class="btn ghost" href="/dashboard">Dashboard</a>
|
||||||
<a class="btn block" href="/dashboard#events">Neues Event</a>
|
<a class="btn block" href="/logout">Logout</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<a class="btn ghost" href="/login">Anmelden</a>
|
<a class="btn ghost" href="/login">Anmelden</a>
|
||||||
<a class="btn block" href="/register">Kostenlos registrieren</a>
|
<a class="btn block" href="/register">Kostenlos registrieren</a>
|
||||||
|
|||||||
8
public/page/logout.php
Normal file
8
public/page/logout.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$app = app();
|
||||||
|
$app->session()->start();
|
||||||
|
session_destroy();
|
||||||
|
$app->flash()->set('info', 'Du wurdest abgemeldet.');
|
||||||
|
redirect('/login');
|
||||||
@@ -20,6 +20,8 @@ CREATE TABLE users (
|
|||||||
CREATE TABLE user_profiles (
|
CREATE TABLE user_profiles (
|
||||||
user_id BIGINT UNSIGNED PRIMARY KEY,
|
user_id BIGINT UNSIGNED PRIMARY KEY,
|
||||||
display_name VARCHAR(120) NOT NULL,
|
display_name VARCHAR(120) NOT NULL,
|
||||||
|
first_name VARCHAR(120) NULL,
|
||||||
|
last_name VARCHAR(120) NULL,
|
||||||
share_level ENUM('basic','papa','papa_contact') NOT NULL DEFAULT 'basic',
|
share_level ENUM('basic','papa','papa_contact') NOT NULL DEFAULT 'basic',
|
||||||
children_visibility ENUM('hidden','age_only','details') NOT NULL DEFAULT 'hidden',
|
children_visibility ENUM('hidden','age_only','details') NOT NULL DEFAULT 'hidden',
|
||||||
zip CHAR(5) NULL,
|
zip CHAR(5) NULL,
|
||||||
|
|||||||
68
src/App/Crypto.php
Normal file
68
src/App/Crypto.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
final class Crypto
|
||||||
|
{
|
||||||
|
private string $key;
|
||||||
|
|
||||||
|
public function __construct(Config $config)
|
||||||
|
{
|
||||||
|
if (!extension_loaded('sodium')) {
|
||||||
|
throw new \RuntimeException('libsodium extension not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = getenv('DATA_KEY') ?: '';
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') {
|
||||||
|
throw new \RuntimeException('DATA_KEY env not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64?
|
||||||
|
if (str_starts_with($raw, 'base64:')) {
|
||||||
|
$raw = substr($raw, 7);
|
||||||
|
}
|
||||||
|
$decoded = base64_decode($raw, true);
|
||||||
|
if ($decoded !== false && strlen($decoded) >= SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES) {
|
||||||
|
$raw = $decoded;
|
||||||
|
} elseif (ctype_xdigit($raw) && strlen($raw) >= SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES * 2) {
|
||||||
|
$raw = hex2bin($raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($raw) < SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES) {
|
||||||
|
throw new \RuntimeException('DATA_KEY invalid length');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->key = substr($raw, 0, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function encrypt(string $plaintext): string
|
||||||
|
{
|
||||||
|
if ($plaintext === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
|
||||||
|
$cipher = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($plaintext, '', $nonce, $this->key);
|
||||||
|
return base64_encode($nonce . $cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decrypt(?string $blob): string
|
||||||
|
{
|
||||||
|
if ($blob === null || $blob === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$raw = base64_decode($blob, true);
|
||||||
|
if ($raw === false || strlen($raw) <= SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$nonce = substr($raw, 0, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
|
||||||
|
$cipher = substr($raw, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
|
||||||
|
try {
|
||||||
|
$plain = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($cipher, '', $nonce, $this->key);
|
||||||
|
return $plain === false ? '' : $plain;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user