This commit is contained in:
2025-12-27 02:02:42 +01:00
parent 8139b1b47e
commit 54e6e10f4f
7 changed files with 164 additions and 18 deletions

View File

@@ -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=');
}

View File

@@ -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=');
}

View File

@@ -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>

View File

@@ -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
View 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');

View File

@@ -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
View 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 '';
}
}
}