yxxx
This commit is contained in:
@@ -10,7 +10,7 @@ error_reporting(E_ALL);
|
|||||||
$appEnvFromEnv = getenv('APP_ENV') ?: 'prod';
|
$appEnvFromEnv = getenv('APP_ENV') ?: 'prod';
|
||||||
$envDir = rtrim(__DIR__, '/\\') . '/' . $appEnvFromEnv;
|
$envDir = rtrim(__DIR__, '/\\') . '/' . $appEnvFromEnv;
|
||||||
|
|
||||||
foreach (['domaindata.php','settings.php'] as $cfgFile) {
|
foreach (['domaindata.php','settings.php','emailtemplates.php'] as $cfgFile) {
|
||||||
$rootPath = __DIR__ . '/' . $cfgFile;
|
$rootPath = __DIR__ . '/' . $cfgFile;
|
||||||
$envPath = $envDir . '/' . $cfgFile;
|
$envPath = $envDir . '/' . $cfgFile;
|
||||||
|
|
||||||
|
|||||||
7
config/prod/emailtemplates.php
Normal file
7
config/prod/emailtemplates.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
$emailtemplates = [
|
||||||
|
'registration_confirm' => 'Neues Template neue Logik',
|
||||||
|
'registration_welcome' => 'Template 1',
|
||||||
|
'registration_resend_code' => 'template mit block',
|
||||||
|
'password_reset' => 'template mit block',
|
||||||
|
];
|
||||||
7
config/staging/emailtemplates.php
Normal file
7
config/staging/emailtemplates.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
$emailtemplates = [
|
||||||
|
'registration_confirm' => 'Neues Template neue Logik',
|
||||||
|
'registration_welcome' => 'Template 1',
|
||||||
|
'registration_resend_code' => 'template mit block',
|
||||||
|
'password_reset' => 'template mit block',
|
||||||
|
];
|
||||||
@@ -1,117 +1,273 @@
|
|||||||
<?php
|
<?php
|
||||||
|
use PDO;
|
||||||
|
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['user_id'])) {
|
||||||
redirect('/login');
|
redirect('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$app = app();
|
||||||
|
$pdo = $app->pdo();
|
||||||
|
$flash = $app->flash()->get();
|
||||||
|
$userId = (int)$_SESSION['user_id'];
|
||||||
|
$error = '';
|
||||||
|
$info = '';
|
||||||
|
|
||||||
|
// POST Aktionen
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
try {
|
||||||
|
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');
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => trim((string)$_POST['display_name']),
|
||||||
|
'zip' => trim((string)$_POST['zip']),
|
||||||
|
'city' => trim((string)$_POST['city']),
|
||||||
|
'prof' => trim((string)$_POST['profession']),
|
||||||
|
'langs' => trim((string)$_POST['languages']),
|
||||||
|
'about' => trim((string)$_POST['about']),
|
||||||
|
'id' => $userId,
|
||||||
|
]);
|
||||||
|
$info = 'Profil gespeichert.';
|
||||||
|
} elseif ($action === 'child_add') {
|
||||||
|
$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([
|
||||||
|
'uid' => $userId,
|
||||||
|
'gender' => $_POST['gender'] ?? 'unknown',
|
||||||
|
'birthdate' => $_POST['birthdate'] ?: null,
|
||||||
|
'age' => $_POST['age_years'] ?: null,
|
||||||
|
'name' => trim((string)$_POST['first_name']),
|
||||||
|
'note' => trim((string)$_POST['note']),
|
||||||
|
]);
|
||||||
|
$info = 'Kind hinzugefügt.';
|
||||||
|
} elseif ($action === 'event_add') {
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO events (created_by, title, teaser_public, description, location_label, zip, city, region, lat, lng, starts_at, allow_kids, visibility, status, created_at, updated_at) VALUES (:uid, :title, :teaser, :descr, :loc, :zip, :city, :region, NULL, NULL, :start, :allow, :vis, :status, NOW(), NOW())');
|
||||||
|
$stmt->execute([
|
||||||
|
'uid' => $userId,
|
||||||
|
'title' => trim((string)$_POST['title']),
|
||||||
|
'teaser' => trim((string)$_POST['teaser']),
|
||||||
|
'descr' => trim((string)$_POST['description']),
|
||||||
|
'loc' => trim((string)$_POST['location_label']),
|
||||||
|
'zip' => trim((string)$_POST['zip']),
|
||||||
|
'city' => trim((string)$_POST['city']),
|
||||||
|
'region' => trim((string)$_POST['region']),
|
||||||
|
'start' => $_POST['starts_at'] ?? null,
|
||||||
|
'allow' => isset($_POST['allow_kids']) ? 1 : 0,
|
||||||
|
'vis' => $_POST['visibility'] ?? 'public',
|
||||||
|
'status' => 'published',
|
||||||
|
]);
|
||||||
|
$info = 'Event gespeichert.';
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daten laden
|
||||||
|
$profile = [
|
||||||
|
'display_name' => '',
|
||||||
|
'zip' => '',
|
||||||
|
'city' => '',
|
||||||
|
'profession' => '',
|
||||||
|
'languages' => '',
|
||||||
|
'about' => '',
|
||||||
|
];
|
||||||
|
$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->execute(['id' => $userId]);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($row) {
|
||||||
|
$profile = array_merge($profile, array_filter($row, fn($v) => $v !== null));
|
||||||
|
}
|
||||||
|
|
||||||
|
$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->execute(['id' => $userId]);
|
||||||
|
$children = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
|
||||||
|
$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->execute(['id' => $userId]);
|
||||||
|
$events = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||||
?>
|
?>
|
||||||
<main class="section">
|
<main class="section">
|
||||||
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
|
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Mitgliederbereich</p>
|
<p class="eyebrow">Mitgliederbereich</p>
|
||||||
<h1>Hallo, <span style="color: var(--color-primary);">Papa</span>!</h1>
|
<h1>Hallo, <span style="color: var(--color-primary);"><?= htmlspecialchars($profile['display_name'] ?: 'Papa', ENT_QUOTES) ?></span>!</h1>
|
||||||
<p class="muted">Verwalte dein Profil, Kinder, Events und Teilnahmen.</p>
|
<p class="muted">Verwalte dein Profil, Kinder, Events und Teilnahmen.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pill-row">
|
|
||||||
<span class="pill">Profil</span>
|
|
||||||
<span class="pill">Kinder</span>
|
|
||||||
<span class="pill">Events</span>
|
|
||||||
<span class="pill">Teilnahmen</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container dash-section">
|
<div class="container dash-section">
|
||||||
<div class="dash-grid">
|
<?php if ($flash): ?>
|
||||||
|
<div class="toast-bar"><?= htmlspecialchars($flash['message'], ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($info): ?>
|
||||||
|
<div class="toast-bar" style="margin-top:10px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="toast-bar" style="margin-top:10px; border-color:#f87171; color:#991b1b;">Fehler: <?= htmlspecialchars($error, ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container dash-section">
|
||||||
|
<div class="dash-grid-2">
|
||||||
<div class="card dash-card">
|
<div class="card dash-card">
|
||||||
<div class="badge">Profil</div>
|
<div class="badge">Profil</div>
|
||||||
<h3>Deine Angaben</h3>
|
<h3>Deine Angaben</h3>
|
||||||
<ul class="dash-list">
|
<form method="post" class="stack gap-12">
|
||||||
<li>Anzeigename: Papa Alex</li>
|
<input type="hidden" name="action" value="profile">
|
||||||
<li>Ort: Berlin, 10437</li>
|
<div class="form-grid">
|
||||||
<li>Beruf: Entwickler</li>
|
<div class="stack gap-6">
|
||||||
<li>Sprachen: Deutsch, Englisch</li>
|
<label class="label" for="pName">Anzeigename</label>
|
||||||
</ul>
|
<input id="pName" name="display_name" class="input" value="<?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?>">
|
||||||
<div class="flex gap-12" style="margin-top:12px;">
|
|
||||||
<button class="btn ghost">Bearbeiten</button>
|
|
||||||
<button class="btn">Sichtbarkeit</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="pCity">Ort</label>
|
||||||
|
<input id="pCity" name="city" class="input" value="<?= htmlspecialchars($profile['city'], ENT_QUOTES) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="pZip">PLZ</label>
|
||||||
|
<input id="pZip" name="zip" class="input" value="<?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?>">
|
||||||
|
</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">
|
||||||
|
<label class="label" for="pLang">Sprachen</label>
|
||||||
|
<input id="pLang" name="languages" class="input" value="<?= htmlspecialchars($profile['languages'], ENT_QUOTES) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="pAbout">Kurzvorstellung</label>
|
||||||
|
<textarea id="pAbout" name="about" class="textarea" rows="3"><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></textarea>
|
||||||
|
</div>
|
||||||
|
<button class="btn" type="submit">Speichern</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card dash-card">
|
<div class="card dash-card">
|
||||||
<div class="badge">Kinder</div>
|
<div class="badge">Kinder</div>
|
||||||
<h3>Deine Kids</h3>
|
<h3>Deine Kids</h3>
|
||||||
|
<?php if (!$children): ?>
|
||||||
|
<p class="muted small">Noch keine Kinder eingetragen.</p>
|
||||||
|
<?php else: ?>
|
||||||
<ul class="dash-list">
|
<ul class="dash-list">
|
||||||
<li>Emma, 4 Jahre (weiblich)</li>
|
<?php foreach ($children as $c): ?>
|
||||||
<li>Max, 7 Jahre (männlich)</li>
|
<li><?= htmlspecialchars($c['first_name'], ENT_QUOTES) ?>, <?= htmlspecialchars($c['gender'], ENT_QUOTES) ?> <?= $c['age_years'] ? '(' . (int)$c['age_years'] . ' Jahre)' : '' ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="flex gap-12" style="margin-top:12px;">
|
<?php endif; ?>
|
||||||
<button class="btn ghost">Kind hinzufügen</button>
|
<form method="post" class="stack gap-12" style="margin-top:12px;">
|
||||||
<button class="btn">Kinder-Einstellungen</button>
|
<input type="hidden" name="action" value="child_add">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="cName">Vorname</label>
|
||||||
|
<input id="cName" name="first_name" class="input" required>
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="cGender">Geschlecht</label>
|
||||||
|
<select id="cGender" name="gender" class="select">
|
||||||
|
<option value="male">Männlich</option>
|
||||||
|
<option value="female">Weiblich</option>
|
||||||
|
<option value="diverse">Divers</option>
|
||||||
|
<option value="unknown">Unbekannt</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
<div class="card dash-card">
|
<div class="stack gap-6">
|
||||||
<div class="badge">Teilnahmen</div>
|
<label class="label" for="cBirth">Geburtsdatum</label>
|
||||||
<h3>Nächste Termine</h3>
|
<input id="cBirth" name="birthdate" class="input" type="date">
|
||||||
<ul class="dash-list">
|
|
||||||
<li>Spielplatzrunde – 10.08., Prenzlauer Berg</li>
|
|
||||||
<li>Erste Hilfe Kids – 20.08., Köln</li>
|
|
||||||
</ul>
|
|
||||||
<div class="flex gap-12" style="margin-top:12px;">
|
|
||||||
<button class="btn ghost">Übersicht öffnen</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="cAge">Alter (Jahre)</label>
|
||||||
|
<input id="cAge" name="age_years" class="input" type="number" min="0" max="18">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="cNote">Notiz</label>
|
||||||
|
<input id="cNote" name="note" class="input">
|
||||||
|
</div>
|
||||||
|
<button class="btn" type="submit">Kind hinzufügen</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container dash-section">
|
<div class="container dash-section">
|
||||||
|
<div class="card dash-card">
|
||||||
|
<div class="badge">Deine Events</div>
|
||||||
|
<?php if (!$events): ?>
|
||||||
|
<p class="muted small">Noch keine Events angelegt.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<ul class="dash-list">
|
||||||
|
<?php foreach ($events as $e): ?>
|
||||||
|
<li><?= htmlspecialchars($e['title'], ENT_QUOTES) ?> – <?= htmlspecialchars($e['city'], ENT_QUOTES) ?>, <?= htmlspecialchars($e['starts_at'], ENT_QUOTES) ?> (<?= htmlspecialchars($e['visibility'], ENT_QUOTES) ?>)</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container dash-section" id="events">
|
||||||
<div class="card dash-card">
|
<div class="card dash-card">
|
||||||
<div class="badge">Eigenes Event</div>
|
<div class="badge">Eigenes Event</div>
|
||||||
<h3>Neuen Termin erstellen</h3>
|
<h3>Neuen Termin erstellen</h3>
|
||||||
<form class="stack gap-12" style="margin-top: 10px;" method="post" action="/dashboard#events">
|
<form class="stack gap-12" style="margin-top: 10px;" method="post" action="/dashboard#events">
|
||||||
|
<input type="hidden" name="action" value="event_add">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evTitle">Titel</label>
|
<label class="label" for="evTitle">Titel</label>
|
||||||
<input id="evTitle" class="input" placeholder="z. B. Väter-Kaffee im Park">
|
<input id="evTitle" name="title" class="input" placeholder="z. B. Väter-Kaffee im Park" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evTopic">Thema</label>
|
<label class="label" for="evTeaser">Kurzbeschreibung</label>
|
||||||
<select id="evTopic" class="select">
|
<input id="evTeaser" name="teaser" class="input" placeholder="Kurztext für Gäste" required>
|
||||||
<option>Outdoor</option>
|
|
||||||
<option>Kaffee</option>
|
|
||||||
<option>Sport</option>
|
|
||||||
<option>Workshop</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="evDesc">Beschreibung (voll)</label>
|
||||||
|
<textarea id="evDesc" name="description" class="textarea" rows="3" placeholder="Was soll passieren, was mitbringen?" required></textarea>
|
||||||
|
</div>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evDate">Datum & Uhrzeit</label>
|
<label class="label" for="evDate">Datum & Uhrzeit</label>
|
||||||
<input id="evDate" class="input" type="datetime-local">
|
<input id="evDate" name="starts_at" class="input" type="datetime-local" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evLocation">Ort/PLZ</label>
|
<label class="label" for="evLoc">Ort/Label</label>
|
||||||
<input id="evLocation" class="input" placeholder="10437 Berlin">
|
<input id="evLoc" name="location_label" class="input" placeholder="Park / Café">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
|
||||||
<label class="label" for="evDesc">Beschreibung</label>
|
|
||||||
<textarea id="evDesc" class="textarea" rows="3" placeholder="Was soll passieren, was mitbringen?"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evKids">Kinder</label>
|
<label class="label" for="evZip">PLZ</label>
|
||||||
<select id="evKids" class="select">
|
<input id="evZip" name="zip" class="input" maxlength="5">
|
||||||
<option value="1">Mit Kindern</option>
|
|
||||||
<option value="0">Ohne Kinder</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stack gap-6">
|
<div class="stack gap-6">
|
||||||
<label class="label" for="evVisibility">Sichtbarkeit</label>
|
<label class="label" for="evCity">Stadt</label>
|
||||||
<select id="evVisibility" class="select">
|
<input id="evCity" name="city" class="input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="evRegion">Region/Bezirk</label>
|
||||||
|
<input id="evRegion" name="region" class="input">
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="evVis">Sichtbarkeit</label>
|
||||||
|
<select id="evVis" name="visibility" class="select">
|
||||||
<option value="public">Öffentlich</option>
|
<option value="public">Öffentlich</option>
|
||||||
<option value="members">Nur Mitglieder</option>
|
<option value="members">Nur Mitglieder</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="label" style="display:flex; align-items:center; gap:8px;">
|
||||||
|
<input type="checkbox" name="allow_kids" checked> Kinder erlaubt
|
||||||
|
</label>
|
||||||
<button class="btn" type="submit">Event anlegen</button>
|
<button class="btn" type="submit">Event anlegen</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$password = (string)($_POST['password'] ?? '');
|
$password = (string)($_POST['password'] ?? '');
|
||||||
try {
|
try {
|
||||||
$auth = new \App\Auth($app);
|
$auth = new \App\Auth($app);
|
||||||
$userId = $auth->login($email, $password);
|
$res = $auth->login($email, $password);
|
||||||
$_SESSION['user_id'] = $userId;
|
if ($res['status'] === 'pending') {
|
||||||
|
$code = $auth->createVerifyCode($res['id'], $email);
|
||||||
|
$mailer = new \App\Mailer($app);
|
||||||
|
$mailer->sendTemplate('registration_confirm', $email, [
|
||||||
|
'code' => $code,
|
||||||
|
'display_name' => $email,
|
||||||
|
]);
|
||||||
|
$_SESSION['verify_email'] = $email;
|
||||||
|
$app->flash()->set('info', 'Bitte bestätige deine Registrierung mit dem Code aus der E-Mail.');
|
||||||
|
redirect('/verify');
|
||||||
|
}
|
||||||
|
$_SESSION['user_id'] = $res['id'];
|
||||||
$app->flash()->set('success', 'Erfolgreich angemeldet.');
|
$app->flash()->set('success', 'Erfolgreich angemeldet.');
|
||||||
redirect('/dashboard');
|
redirect('/dashboard');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
@@ -24,9 +24,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
try {
|
try {
|
||||||
$auth = new \App\Auth($app);
|
$auth = new \App\Auth($app);
|
||||||
$userId = $auth->register($displayName, $email, $password);
|
$userId = $auth->register($displayName, $email, $password);
|
||||||
$_SESSION['user_id'] = $userId;
|
$code = $auth->createVerifyCode($userId, $email);
|
||||||
$app->flash()->set('success', 'Willkommen! Dein Account wurde erstellt.');
|
$mailer = new \App\Mailer($app);
|
||||||
redirect('/dashboard');
|
$mailer->sendTemplate('registration_confirm', $email, [
|
||||||
|
'code' => $code,
|
||||||
|
'display_name' => $displayName,
|
||||||
|
]);
|
||||||
|
$_SESSION['verify_email'] = $email;
|
||||||
|
$app->flash()->set('info', 'Bitte bestätige deine Registrierung mit dem Code aus der E-Mail.');
|
||||||
|
redirect('/verify');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$error = $e->getMessage();
|
$error = $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|||||||
91
partials/landing/account/verify.php
Normal file
91
partials/landing/account/verify.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
$app = app();
|
||||||
|
$pdo = $app->pdo();
|
||||||
|
$flash = $app->flash()->get();
|
||||||
|
$error = '';
|
||||||
|
$info = '';
|
||||||
|
$email = $_SESSION['verify_email'] ?? '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = $_POST['action'] ?? 'verify';
|
||||||
|
$email = trim((string)($_POST['email'] ?? ''));
|
||||||
|
$code = strtoupper(trim((string)($_POST['code'] ?? '')));
|
||||||
|
$auth = new \App\Auth($app);
|
||||||
|
$mailer = new \App\Mailer($app);
|
||||||
|
|
||||||
|
if ($action === 'resend') {
|
||||||
|
try {
|
||||||
|
$stmt = $app->pdo()->prepare('SELECT id, display_name, status FROM users u JOIN user_profiles p ON p.user_id = u.id WHERE u.email = :email LIMIT 1');
|
||||||
|
$stmt->execute(['email' => $email]);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$row) {
|
||||||
|
throw new RuntimeException('E-Mail nicht gefunden.');
|
||||||
|
}
|
||||||
|
$userId = (int)$row['id'];
|
||||||
|
$codeNew = $auth->createVerifyCode($userId, $email);
|
||||||
|
$mailer->sendTemplate('registration_resend_code', $email, [
|
||||||
|
'code' => $codeNew,
|
||||||
|
'display_name' => $row['display_name'] ?? '',
|
||||||
|
]);
|
||||||
|
$info = 'Neuer Code wurde versendet.';
|
||||||
|
$_SESSION['verify_email'] = $email;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$userId = $auth->verifyCode($email, $code);
|
||||||
|
$_SESSION['user_id'] = $userId;
|
||||||
|
unset($_SESSION['verify_email']);
|
||||||
|
$mailer->sendTemplate('registration_welcome', $email, ['display_name' => $email]);
|
||||||
|
$app->flash()->set('success', 'Registrierung bestätigt. Willkommen!');
|
||||||
|
redirect('/dashboard');
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<main class="auth-wrap">
|
||||||
|
<div class="container auth-grid">
|
||||||
|
<section class="card auth-card">
|
||||||
|
<div class="badge">Bestätigung</div>
|
||||||
|
<h1 class="mt-1" style="margin: 12px 0;">Registrierung bestätigen</h1>
|
||||||
|
<p class="muted">Wir haben dir einen 6-stelligen Code gesendet. Bitte gib ihn hier ein.</p>
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="toast-bar" style="margin-top: 10px;"><?= htmlspecialchars($flash['message'], ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($info): ?>
|
||||||
|
<div class="toast-bar" style="margin-top: 10px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="toast-bar" style="margin-top: 10px; border-color:#f87171; color:#991b1b;">Fehler: <?= htmlspecialchars($error, ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form class="stack gap-12" style="margin-top: 14px;" method="post" action="/verify">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="verEmail">E-Mail</label>
|
||||||
|
<input id="verEmail" name="email" class="input" type="email" required placeholder="du@example.com" value="<?= htmlspecialchars($email, ENT_QUOTES) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="verCode">Code</label>
|
||||||
|
<input id="verCode" name="code" class="input" maxlength="6" required placeholder="ABC123">
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="action" value="verify">
|
||||||
|
<button class="btn block" type="submit">Code prüfen</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/verify" class="mt-2">
|
||||||
|
<input type="hidden" name="email" value="<?= htmlspecialchars($email, ENT_QUOTES) ?>">
|
||||||
|
<input type="hidden" name="action" value="resend">
|
||||||
|
<button class="btn ghost block" type="submit">Code erneut senden</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="auth-aside">
|
||||||
|
<img class="auth-logo" src="/assets/bilder/logo_male.png" alt="Papa-Kind-Treff Logo">
|
||||||
|
<h3>Keine Mail erhalten?</h3>
|
||||||
|
<p class="auth-meta">Prüfe auch den Spam-Ordner. Du kannst den Code jederzeit neu senden lassen.</p>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
4
public/page/verify.php
Normal file
4
public/page/verify.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
tpl('verify', 'landing', 'account');
|
||||||
@@ -10,7 +10,7 @@ CREATE TABLE users (
|
|||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
status ENUM('active','pending','blocked') DEFAULT 'active',
|
status ENUM('active','pending','blocked') DEFAULT 'pending',
|
||||||
email_verified_at DATETIME NULL,
|
email_verified_at DATETIME NULL,
|
||||||
last_login_at DATETIME NULL,
|
last_login_at DATETIME NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -113,6 +113,7 @@ CREATE TABLE user_tokens (
|
|||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id BIGINT UNSIGNED NOT NULL,
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
type ENUM('reset','verify','magic_login') NOT NULL,
|
type ENUM('reset','verify','magic_login') NOT NULL,
|
||||||
|
code CHAR(12) NULL, -- z. B. 6-stelliger Code (optional Klartext)
|
||||||
token_hash CHAR(64) NOT NULL UNIQUE,
|
token_hash CHAR(64) NOT NULL UNIQUE,
|
||||||
expires_at DATETIME NOT NULL,
|
expires_at DATETIME NOT NULL,
|
||||||
used_at DATETIME NULL,
|
used_at DATETIME NULL,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ final class Auth
|
|||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'pw' => $hash,
|
'pw' => $hash,
|
||||||
'status' => 'active',
|
'status' => 'pending',
|
||||||
]);
|
]);
|
||||||
$userId = (int)$pdo->lastInsertId();
|
$userId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
@@ -59,7 +59,64 @@ final class Auth
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function login(string $email, string $password): int
|
public function createVerifyCode(int $userId, string $email): string
|
||||||
|
{
|
||||||
|
$pdo = $this->pdo();
|
||||||
|
$code = $this->generateCode(6);
|
||||||
|
$hash = hash('sha256', $code);
|
||||||
|
|
||||||
|
$pdo->prepare('DELETE FROM user_tokens WHERE user_id = :uid AND type = :t')->execute(['uid' => $userId, 't' => 'verify']);
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO user_tokens (user_id, type, code, token_hash, expires_at, created_at) VALUES (:uid, :type, :code, :hash, DATE_ADD(NOW(), INTERVAL 48 HOUR), NOW())');
|
||||||
|
$stmt->execute([
|
||||||
|
'uid' => $userId,
|
||||||
|
'type' => 'verify',
|
||||||
|
'code' => $code,
|
||||||
|
'hash' => $hash,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyCode(string $email, string $code): int
|
||||||
|
{
|
||||||
|
$pdo = $this->pdo();
|
||||||
|
$email = strtolower(trim($email));
|
||||||
|
$hash = hash('sha256', $code);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT u.id, u.status, t.id AS tid, t.token_hash FROM users u JOIN user_tokens t ON t.user_id = u.id AND t.type = :type WHERE u.email = :email AND (t.used_at IS NULL) AND t.expires_at > NOW() ORDER BY t.expires_at DESC LIMIT 1');
|
||||||
|
$stmt->execute(['type' => 'verify', 'email' => $email]);
|
||||||
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
if (!$row || !hash_equals((string)$row['token_hash'], $hash)) {
|
||||||
|
throw new \RuntimeException('Code ist ungültig oder abgelaufen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)$row['id'];
|
||||||
|
$tid = (int)$row['tid'];
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$pdo->prepare('UPDATE user_tokens SET used_at = NOW() WHERE id = :id')->execute(['id' => $tid]);
|
||||||
|
$pdo->prepare('UPDATE users SET status = :st, email_verified_at = NOW() WHERE id = :id')->execute(['st' => 'active', 'id' => $userId]);
|
||||||
|
$pdo->commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCode(int $len = 6): string
|
||||||
|
{
|
||||||
|
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
$out = '';
|
||||||
|
for ($i = 0; $i < $len; $i++) {
|
||||||
|
$out .= $chars[random_int(0, strlen($chars) - 1)];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(string $email, string $password): array
|
||||||
{
|
{
|
||||||
$pdo = $this->pdo();
|
$pdo = $this->pdo();
|
||||||
$email = strtolower(trim($email));
|
$email = strtolower(trim($email));
|
||||||
@@ -71,18 +128,18 @@ final class Auth
|
|||||||
if (!$row) {
|
if (!$row) {
|
||||||
throw new \RuntimeException('E-Mail oder Passwort ist falsch.');
|
throw new \RuntimeException('E-Mail oder Passwort ist falsch.');
|
||||||
}
|
}
|
||||||
if ($row['status'] !== 'active') {
|
|
||||||
throw new \RuntimeException('Account ist nicht aktiv.');
|
|
||||||
}
|
|
||||||
if (!password_verify($password, (string)$row['password_hash'])) {
|
if (!password_verify($password, (string)$row['password_hash'])) {
|
||||||
throw new \RuntimeException('E-Mail oder Passwort ist falsch.');
|
throw new \RuntimeException('E-Mail oder Passwort ist falsch.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = (int)$row['id'];
|
$userId = (int)$row['id'];
|
||||||
|
$status = (string)$row['status'];
|
||||||
|
|
||||||
|
if ($status === 'active') {
|
||||||
$upd = $pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = :id');
|
$upd = $pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = :id');
|
||||||
$upd->execute(['id' => $userId]);
|
$upd->execute(['id' => $userId]);
|
||||||
|
}
|
||||||
|
|
||||||
return $userId;
|
return ['id' => $userId, 'status' => $status];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
174
src/App/Mailer.php
Normal file
174
src/App/Mailer.php
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
final class Mailer
|
||||||
|
{
|
||||||
|
public function __construct(private App $app) {}
|
||||||
|
|
||||||
|
private function templates(): array
|
||||||
|
{
|
||||||
|
$env = $this->app->config()->env;
|
||||||
|
$root = __DIR__ . '/../../config/emailtemplates.php';
|
||||||
|
$envPath = __DIR__ . "/../../config/{$env}/emailtemplates.php";
|
||||||
|
$file = is_file($root) ? $root : $envPath;
|
||||||
|
$emailtemplates = [];
|
||||||
|
if (is_file($file)) {
|
||||||
|
/** @noinspection PhpIncludeInspection */
|
||||||
|
$emailtemplates = include $file;
|
||||||
|
}
|
||||||
|
return is_array($emailtemplates ?? null) ? $emailtemplates : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderTemplate(string $key, array $vars): array
|
||||||
|
{
|
||||||
|
$templates = $this->templates();
|
||||||
|
$id = $templates[$key] ?? $key;
|
||||||
|
|
||||||
|
$apiBase = getenv('EMAILTEMPLATE_API_BASE') ?: '';
|
||||||
|
$apiToken = getenv('EMAILTEMPLATE_API_TOKEN') ?: '';
|
||||||
|
|
||||||
|
if ($apiBase && $apiToken) {
|
||||||
|
$payload = [
|
||||||
|
'template' => $id,
|
||||||
|
'placeholders' => $vars,
|
||||||
|
];
|
||||||
|
$payload['token'] = $apiToken;
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => "Content-Type: application/json\r\n",
|
||||||
|
'timeout' => 15,
|
||||||
|
'content' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$resp = @file_get_contents($apiBase, false, $ctx);
|
||||||
|
if ($resp !== false) {
|
||||||
|
$decoded = json_decode($resp, true);
|
||||||
|
if (is_array($decoded) && !empty($decoded['ok']) && !empty($decoded['html'])) {
|
||||||
|
return [
|
||||||
|
'subject' => $decoded['subject'] ?? 'Papakind',
|
||||||
|
'html' => $decoded['html'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: einfacher Text
|
||||||
|
$subject = 'Papa-Kind-Treff';
|
||||||
|
$body = $id;
|
||||||
|
foreach ($vars as $k => $v) {
|
||||||
|
$body = str_replace(['{' . $k . '}', '{{' . $k . '}}'], (string)$v, $body);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'subject' => $subject,
|
||||||
|
'html' => nl2br(htmlspecialchars($body, ENT_QUOTES)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendTemplate(string $templateKey, string $to, array $vars = []): void
|
||||||
|
{
|
||||||
|
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new \InvalidArgumentException('Invalid recipient email.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tpl = $this->renderTemplate($templateKey, $vars);
|
||||||
|
$subject = $tpl['subject'] ?? 'Papa-Kind-Treff';
|
||||||
|
$html = $tpl['html'] ?? '';
|
||||||
|
|
||||||
|
$transport = getenv('MAIL_TRANSPORT') ?: 'mail';
|
||||||
|
$fromEmail = getenv('MAIL_FROM') ?: 'no-reply@' . $this->app->config()->primaryDomain;
|
||||||
|
$fromName = getenv('MAIL_FROM_NAME') ?: 'Papa-Kind-Treff';
|
||||||
|
|
||||||
|
if ($transport === 'smtp') {
|
||||||
|
$this->sendSmtp($to, $subject, $html, $fromEmail, $fromName);
|
||||||
|
} else {
|
||||||
|
$this->sendMailFn($to, $subject, $html, $fromEmail, $fromName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendMailFn(string $to, string $subject, string $html, string $from, string $fromName): void
|
||||||
|
{
|
||||||
|
$headers = [];
|
||||||
|
if ($from) {
|
||||||
|
$headers[] = 'From: ' . sprintf('"%s" <%s>', addslashes($fromName), $from);
|
||||||
|
}
|
||||||
|
$headers[] = 'Content-Type: text/html; charset=utf-8';
|
||||||
|
if (!@mail($to, $subject, $html, implode("\r\n", $headers))) {
|
||||||
|
throw new \RuntimeException('mail() transport failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendSmtp(string $to, string $subject, string $html, string $from, string $fromName): void
|
||||||
|
{
|
||||||
|
$host = getenv('SMTP_HOST') ?: '';
|
||||||
|
$port = (int)(getenv('SMTP_PORT') ?: 587);
|
||||||
|
$user = getenv('SMTP_USER') ?: '';
|
||||||
|
$pass = getenv('SMTP_PASS') ?: '';
|
||||||
|
$secure = strtolower(getenv('SMTP_SECURE') ?: 'tls'); // tls|ssl|none
|
||||||
|
|
||||||
|
if (!$host) {
|
||||||
|
$this->sendMailFn($to, $subject, $html, $from, $fromName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$proto = ($secure === 'ssl') ? 'ssl://' : '';
|
||||||
|
$fp = @stream_socket_client($proto . $host . ':' . $port, $errno, $errstr, 15, STREAM_CLIENT_CONNECT);
|
||||||
|
if (!$fp) {
|
||||||
|
$this->sendMailFn($to, $subject, $html, $from, $fromName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream_set_timeout($fp, 15);
|
||||||
|
|
||||||
|
$read = function () use ($fp) {
|
||||||
|
return fgets($fp, 515);
|
||||||
|
};
|
||||||
|
$write = function (string $cmd) use ($fp) {
|
||||||
|
fwrite($fp, $cmd . "\r\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
$read();
|
||||||
|
$write('EHLO ' . $this->app->config()->primaryDomain);
|
||||||
|
$read();
|
||||||
|
|
||||||
|
if ($secure === 'tls') {
|
||||||
|
$write('STARTTLS');
|
||||||
|
$read();
|
||||||
|
if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
||||||
|
fclose($fp);
|
||||||
|
$this->sendMailFn($to, $subject, $html, $from, $fromName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$write('EHLO ' . $this->app->config()->primaryDomain);
|
||||||
|
$read();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user !== '') {
|
||||||
|
$write('AUTH LOGIN');
|
||||||
|
$read();
|
||||||
|
$write(base64_encode($user));
|
||||||
|
$read();
|
||||||
|
$write(base64_encode($pass));
|
||||||
|
$read();
|
||||||
|
}
|
||||||
|
|
||||||
|
$write('MAIL FROM: <' . $from . '>');
|
||||||
|
$read();
|
||||||
|
$write('RCPT TO: <' . $to . '>');
|
||||||
|
$read();
|
||||||
|
$write('DATA');
|
||||||
|
$read();
|
||||||
|
|
||||||
|
$msg = "From: {$fromName} <{$from}>\r\n";
|
||||||
|
$msg .= "To: <{$to}>\r\n";
|
||||||
|
$msg .= "Subject: {$subject}\r\n";
|
||||||
|
$msg .= "MIME-Version: 1.0\r\n";
|
||||||
|
$msg .= "Content-Type: text/html; charset=utf-8\r\n\r\n";
|
||||||
|
$msg .= $html . "\r\n.\r\n";
|
||||||
|
$write($msg);
|
||||||
|
$read();
|
||||||
|
$write('QUIT');
|
||||||
|
fclose($fp);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user