M3 Mailversand und Public-Landingpage vorbereiten
- Mail-Transport fuer Reset- und Verifizierungslinks ergaenzen - Public-Landingpage mit Hero-Asset und App-CTAs bauen - Mail-Flow-Test, Smoke-Abdeckung und SaaS-Doku aktualisieren
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
function saas_mail_transport(): string
|
||||
{
|
||||
$default = app_is_dev() ? 'log' : 'mail';
|
||||
|
||||
return strtolower((string)app_env('APP_MAIL_TRANSPORT', $default));
|
||||
}
|
||||
|
||||
function saas_mail_from(): string
|
||||
{
|
||||
return (string)app_env('APP_MAIL_FROM', 'noreply@kaffeeliste.local');
|
||||
}
|
||||
|
||||
function saas_mail_from_name(): string
|
||||
{
|
||||
return (string)app_env('APP_MAIL_FROM_NAME', 'Kaffeeliste');
|
||||
}
|
||||
|
||||
function saas_app_base_url(): string
|
||||
{
|
||||
$configured = app_env('APP_BASE_URL');
|
||||
if ($configured !== null) {
|
||||
return rtrim($configured, '/');
|
||||
}
|
||||
|
||||
if (PHP_SAPI !== 'cli' && !empty($_SERVER['HTTP_HOST'])) {
|
||||
$scheme = app_is_https() ? 'https' : 'http';
|
||||
|
||||
return $scheme . '://' . rtrim((string)$_SERVER['HTTP_HOST'], '/');
|
||||
}
|
||||
|
||||
return 'http://localhost';
|
||||
}
|
||||
|
||||
function saas_app_url(string $path): string
|
||||
{
|
||||
return saas_app_base_url() . '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
function saas_mail_headers(): string
|
||||
{
|
||||
$fromName = str_replace(["\r", "\n"], '', saas_mail_from_name());
|
||||
$from = str_replace(["\r", "\n"], '', saas_mail_from());
|
||||
|
||||
return implode("\r\n", [
|
||||
"From: {$fromName} <{$from}>",
|
||||
"Reply-To: {$from}",
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
function saas_log_mail(string $to, string $subject, string $body): array
|
||||
{
|
||||
$logDir = (string)app_env('APP_MAIL_LOG_DIR', APP_ROOT . '/var/mail');
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0700, true);
|
||||
}
|
||||
|
||||
if (!is_dir($logDir) || !is_writable($logDir)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'transport' => 'log',
|
||||
'error' => 'mail log directory is not writable',
|
||||
];
|
||||
}
|
||||
|
||||
$file = rtrim($logDir, '/') . '/mail_' . date('Ymd_His') . '_' . bin2hex(random_bytes(6)) . '.txt';
|
||||
$content = "To: {$to}\nSubject: {$subject}\n\n{$body}\n";
|
||||
|
||||
if (file_put_contents($file, $content) === false) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'transport' => 'log',
|
||||
'error' => 'mail log write failed',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'transport' => 'log',
|
||||
'path' => $file,
|
||||
];
|
||||
}
|
||||
|
||||
function saas_send_mail(string $to, string $subject, string $body): array
|
||||
{
|
||||
$to = trim($to);
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'transport' => saas_mail_transport(),
|
||||
'error' => 'invalid recipient',
|
||||
];
|
||||
}
|
||||
|
||||
if (saas_mail_transport() === 'log') {
|
||||
return saas_log_mail($to, $subject, $body);
|
||||
}
|
||||
|
||||
$sent = mail($to, $subject, $body, saas_mail_headers());
|
||||
|
||||
return [
|
||||
'ok' => $sent,
|
||||
'transport' => 'mail',
|
||||
'error' => $sent ? null : 'mail transport failed',
|
||||
];
|
||||
}
|
||||
|
||||
function saas_send_password_reset_mail(string $to, string $token): array
|
||||
{
|
||||
$link = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($token));
|
||||
$body = implode("\n", [
|
||||
'Hallo,',
|
||||
'',
|
||||
'fuer dein Kaffeeliste-Konto wurde ein Passwort-Reset angefordert.',
|
||||
'Oeffne diesen Link, um ein neues Passwort zu setzen:',
|
||||
'',
|
||||
$link,
|
||||
'',
|
||||
'Der Link ist zeitlich begrenzt und kann nur einmal verwendet werden.',
|
||||
'Falls du den Reset nicht angefordert hast, kannst du diese Nachricht ignorieren.',
|
||||
'',
|
||||
'Deine Kaffeeliste',
|
||||
]);
|
||||
|
||||
return saas_send_mail($to, 'Kaffeeliste Passwort zuruecksetzen', $body);
|
||||
}
|
||||
|
||||
function saas_send_email_verification_mail(string $to, string $token): array
|
||||
{
|
||||
$link = saas_app_url('email-verifizieren.php?token=' . urlencode($token));
|
||||
$body = implode("\n", [
|
||||
'Hallo,',
|
||||
'',
|
||||
'bitte bestaetige deine E-Mail-Adresse fuer Kaffeeliste.',
|
||||
'Oeffne dazu diesen Link:',
|
||||
'',
|
||||
$link,
|
||||
'',
|
||||
'Der Link ist zeitlich begrenzt und kann nur einmal verwendet werden.',
|
||||
'',
|
||||
'Deine Kaffeeliste',
|
||||
]);
|
||||
|
||||
return saas_send_mail($to, 'Kaffeeliste E-Mail bestaetigen', $body);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -26,6 +26,12 @@ export DEV_AUTH_EMAIL="admin@test.local"
|
||||
export DEV_AUTH_NAME="Test Admin"
|
||||
# Optional: Standard ist var/sessions im Repository.
|
||||
export APP_SESSION_PATH="/path/to/writable/sessions"
|
||||
# Optional fuer Passwort-Reset und E-Mail-Verifikation.
|
||||
export APP_BASE_URL="http://127.0.0.1:8080"
|
||||
export APP_MAIL_TRANSPORT="log"
|
||||
export APP_MAIL_LOG_DIR="/path/to/writable/mail-log"
|
||||
export APP_MAIL_FROM="noreply@example.test"
|
||||
export APP_MAIL_FROM_NAME="Kaffeeliste"
|
||||
```
|
||||
|
||||
## Testdatenbank initialisieren
|
||||
@@ -63,6 +69,10 @@ der Port ueber die Ports-Ansicht weitergeleitet werden.
|
||||
## Hinweise
|
||||
|
||||
- Der Dev-Login umgeht AD/LDAP und nutzt `DEV_AUTH_EMAIL`.
|
||||
- Der Dev-Mailversand nutzt standardmaessig `APP_MAIL_TRANSPORT=log` und legt
|
||||
Nachrichten unter `var/mail` ab. Fuer Produktion kann zunaechst
|
||||
`APP_MAIL_TRANSPORT=mail` mit passendem `APP_BASE_URL` und Absender gesetzt
|
||||
werden; eine SMTP-/Provider-Anbindung bleibt ein spaeterer Betriebsausbau.
|
||||
- Die Tabellen werden ueber `database/migrations/` angelegt.
|
||||
- `database/mysql-dev-schema.sql` bleibt als historische Dev-Schema-Baseline
|
||||
erhalten; der aktive Weg ist `scripts/migrate.php`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# M2 Technisches Fundament
|
||||
|
||||
Stand: 2026-07-12
|
||||
Stand: 2026-07-13
|
||||
|
||||
M2 fuehrt die technischen Grundbausteine fuer den SaaS-Umbau ein, ohne das
|
||||
Legacy-Verhalten oder das bestehende Design zu veraendern.
|
||||
@@ -140,7 +140,7 @@ Die Skripte erwarten die bekannten Dev-Umgebungsvariablen `DB_HOST`, `DB_NAME`,
|
||||
eine PayPal-Alias-Dublette und hinterlaesst keine Datei in `var/uploads`.
|
||||
- CSV-Upload negative Tests: Nicht-CSV-Dateien werden abgewiesen.
|
||||
- Golden Master weiterhin gruen mit 104 Assertions.
|
||||
- HTTP-Smoke weiterhin gruen mit 21 sicheren Seiten inklusive Login,
|
||||
- HTTP-Smoke weiterhin gruen mit 22 sicheren Seiten inklusive Landingpage, Login,
|
||||
Registrierung, Passwort-Reset, E-Mail-Verifikation und geschuetzter
|
||||
Mandant-Einstellungen sowie geschuetzter Mandantenauswahl.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# M3 SaaS-Basis Vorbereitung
|
||||
|
||||
Stand: 2026-07-12
|
||||
Stand: 2026-07-13
|
||||
|
||||
M3 fuehrt Mandanten, Benutzer, Mitgliedschaften und Grundeinstellungen additiv
|
||||
ein. Die bestehende Legacy-App bleibt dabei lauffaehig und wird noch nicht auf
|
||||
@@ -53,6 +53,7 @@ scripts/check-m3-auth-flow.php
|
||||
scripts/check-m3-settings-flow.php
|
||||
scripts/check-m3-password-email-flow.php
|
||||
scripts/check-m3-tenant-resolution-flow.php
|
||||
scripts/check-m3-mail-flow.php
|
||||
```
|
||||
|
||||
Wichtige Regeln:
|
||||
@@ -122,6 +123,7 @@ Umgesetzte Dateien:
|
||||
```text
|
||||
app/database.php
|
||||
app/saas-auth.php
|
||||
app/saas-mail.php
|
||||
login.php
|
||||
register.php
|
||||
konto.php
|
||||
@@ -131,6 +133,8 @@ passwort-vergessen.php
|
||||
passwort-zuruecksetzen.php
|
||||
email-verifikation-senden.php
|
||||
email-verifizieren.php
|
||||
landing.php
|
||||
assets/images/landing-hero.png
|
||||
```
|
||||
|
||||
Umgesetzter Umfang:
|
||||
@@ -151,13 +155,33 @@ Umgesetzter Umfang:
|
||||
- Passwort-Reset erzeugt Single-Use-Tokens und speichert nur Token-Hashes.
|
||||
- E-Mail-Verifikation erzeugt Single-Use-Tokens und setzt
|
||||
`users.email_verified_at`.
|
||||
- Bis zum produktiven Mailversand werden Links nur im Dev-Modus angezeigt
|
||||
beziehungsweise in CLI-Checks direkt verwendet.
|
||||
- Reset- und Verifikationslinks werden ueber `app/saas-mail.php` versendet.
|
||||
Der Dev-Standard schreibt Mails nach `var/mail`; produktiv kann auf PHP
|
||||
`mail()` umgestellt werden.
|
||||
- Dev-Links werden weiterhin nur im Dev-Modus angezeigt, damit lokale Checks
|
||||
ohne echten Postausgang reproduzierbar bleiben.
|
||||
|
||||
Noch offen im M3-Auth-Scope:
|
||||
|
||||
- Weitergehende Rollenmatrix fuer spaetere SaaS-Seiten.
|
||||
- Produktiver Mailversand fuer Reset- und Verifikationslinks.
|
||||
- Produktive SMTP-/Provider-Anbindung inklusive Bounce-/Fehlerprotokoll.
|
||||
|
||||
## Public-Landingpage
|
||||
|
||||
Umgesetzte Dateien:
|
||||
|
||||
```text
|
||||
landing.php
|
||||
assets/images/landing-hero.png
|
||||
```
|
||||
|
||||
Umgesetzter Umfang:
|
||||
|
||||
- Oeffentliche Landingpage ohne Legacy-DB-Zugriff.
|
||||
- Hero mit generiertem Bild-Asset, bestehender Typografie und gruenem Akzent.
|
||||
- CTA zu Login und Registrierung.
|
||||
- Kurzabschnitt zu Stricherfassung, Mandantenfaehigkeit und Webspace-Betrieb.
|
||||
- Die geschuetzte App-Sidebar bleibt von der Public-Seite getrennt.
|
||||
|
||||
## Tenant-Aufloesung
|
||||
|
||||
@@ -212,7 +236,10 @@ Umgesetzter Umfang:
|
||||
- Owner-/Admin-Grundeinstellungen koennen aktualisiert werden: erfuellt.
|
||||
- Passwort-Reset und E-Mail-Verifikation funktionieren mit Single-Use-Tokens:
|
||||
erfuellt.
|
||||
- Reset- und Verifikationslinks werden im Dev-/Testmodus als Mail-Log erzeugt:
|
||||
erfuellt.
|
||||
- Mandantenauswahl funktioniert fuer User mit mehreren Mandanten: erfuellt.
|
||||
- Oeffentliche Landingpage ist per HTTP-Smoke erreichbar: erfuellt.
|
||||
- Golden-Master und HTTP-Smoke bleiben gruen: erfuellt.
|
||||
|
||||
## Risiken
|
||||
@@ -243,5 +270,7 @@ Umgesetzter Umfang:
|
||||
14. Mandantenauswahl und zentrale Session-Aufloesung bauen: erledigt.
|
||||
15. Tenant-Resolution-Kontrollskript schreiben: erledigt.
|
||||
16. Golden-Master und HTTP-Smoke ausfuehren: erledigt.
|
||||
17. Produktiven Mailversand oder Landingpage vorbereiten:
|
||||
17. Mail-Transport-Abstraktion und Mail-Flow-Kontrollskript bauen: erledigt.
|
||||
18. Public-Landingpage mit erstem Hero-Asset vorbereiten: erledigt.
|
||||
19. M3-Abschlusscheck dokumentieren und M4-Datenmigration starten:
|
||||
naechster Schritt.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SaaS-Umstrukturierungsplan Kaffeeliste
|
||||
|
||||
Stand: 2026-07-11
|
||||
Stand: 2026-07-13
|
||||
|
||||
Dieses Dokument beschreibt den geplanten Umbau der bestehenden Kaffeelisten-App zu
|
||||
einer mehrkundenfaehigen SaaS-Anwendung mit oeffentlicher Landingpage,
|
||||
@@ -283,11 +283,11 @@ Uebersicht:
|
||||
| M0 | Baseline | Bestand, Sicherheit und Designreferenz sind dokumentiert |
|
||||
| M1 | Golden Master | Legacy-Ergebnisse sind als Vergleichsbasis eingefroren; HTTP-Smoke prueft sichere Seiten |
|
||||
| M2 | Technisches Fundament | Abgeschlossen: Migrationen, Bootstrap, Session, CSRF-Helper und Legacy-Schreibseitenschutz stehen |
|
||||
| M3 | SaaS-Basis | Tenants, User, Registrierung, Login und Rollen funktionieren |
|
||||
| M3 | SaaS-Basis | Tenants, User, Registrierung, Login, Rollen, Mail-Links und zentrale Mandantenauswahl funktionieren |
|
||||
| M4 | Datenmigration | Legacy-Daten sind tenant-sicher im Zielmodell abgebildet |
|
||||
| M5 | App-Kern | Dashboard, Striche, Einzahlungen, Mitglieder und Liste laufen |
|
||||
| M6 | Betriebsflows | Import, Export, Mail und Jahresprozesse sind auditierbar |
|
||||
| M7 | Landingpage | Werbliche Seite und Registrierung sind oeffentlich nutzbar |
|
||||
| M7 | Landingpage | Erste werbliche Seite ist oeffentlich nutzbar; spaetere Ausbaustufen folgen |
|
||||
| M8 | Haertung | Betrieb, Datenschutz, Monitoring und Isolation sind geprueft |
|
||||
| M9 | Cutover | Produktivumstellung ist vorbereitet und Legacy ist read-only |
|
||||
|
||||
@@ -421,16 +421,19 @@ Schritte:
|
||||
Flow erledigt.
|
||||
- Login und Logout bauen: erster Flow erledigt.
|
||||
- Passwort-Reset und E-Mail-Verifikation bauen: Dev-Flow mit Single-Use-Tokens
|
||||
erledigt.
|
||||
und Mail-Transport-Abstraktion erledigt.
|
||||
- Tenant-Aufloesung definieren: zentrale App-Domain mit Session-Kontext und
|
||||
Mandantenauswahl erledigt; feste Domains vorbereitet, keine Wildcards.
|
||||
- Rollenpruefung zentralisieren: erster Owner/Admin-Check erledigt.
|
||||
- Erste Admin-/Owner-Seite fuer Grundeinstellungen: erledigt.
|
||||
- Erste Public-Landingpage mit CTA zu Login und Registrierung: erledigt.
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- Ein neuer Kunde kann sich registrieren und in seine eigene App gelangen.
|
||||
- Rollen und Tenant-Kontext sind serverseitig verfuegbar.
|
||||
- Reset- und Verifikationslinks koennen im Dev-Modus geloggt und produktiv ueber
|
||||
einen konfigurierten Mail-Transport versendet werden.
|
||||
|
||||
Abhaengigkeiten:
|
||||
|
||||
@@ -515,10 +518,11 @@ zu verwischen.
|
||||
|
||||
Schritte:
|
||||
|
||||
- Public-Layout mit bestehender Typografie und gruenem Akzent bauen.
|
||||
- Landingpage-Inhalte erstellen.
|
||||
- Public-Layout mit bestehender Typografie und gruenem Akzent bauen. Erster
|
||||
Stand als `landing.php` erledigt.
|
||||
- Landingpage-Inhalte erstellen. Erster Stand erledigt.
|
||||
- Demo-Screenshot oder Demo-Ansicht einbinden.
|
||||
- CTA zu Registrierung und Login.
|
||||
- CTA zu Registrierung und Login. Erledigt.
|
||||
- FAQ-Auszug strukturieren.
|
||||
- Keine App-Sidebar im Public-Bereich.
|
||||
|
||||
@@ -588,12 +592,12 @@ umgebaut werden.
|
||||
MVP-Paket:
|
||||
|
||||
1. Tenant, User, Login, Registrierung.
|
||||
2. Teilnehmer, Settings, Ledger.
|
||||
3. Dashboard, Striche, Einzahlungen, Gesamtuebersicht.
|
||||
4. Mitgliederverwaltung.
|
||||
5. Hinweise.
|
||||
6. CSV-Import, Export.
|
||||
7. Landingpage.
|
||||
2. Mail-Links, zentrale Mandantenauswahl und erste Landingpage.
|
||||
3. Teilnehmer, Settings, Ledger.
|
||||
4. Dashboard, Striche, Einzahlungen, Gesamtuebersicht.
|
||||
5. Mitgliederverwaltung.
|
||||
6. Hinweise.
|
||||
7. CSV-Import, Export.
|
||||
|
||||
Spaeter:
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/functions.php';
|
||||
require_once __DIR__ . '/app/saas-mail.php';
|
||||
|
||||
$pdo = app_db_pdo();
|
||||
$user = saas_require_login();
|
||||
@@ -20,6 +21,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (saas_should_show_auth_links() && !empty($result['token'])) {
|
||||
$verificationLink = 'email-verifizieren.php?token=' . urlencode((string)$result['token']);
|
||||
}
|
||||
if (!empty($result['token'])) {
|
||||
saas_send_email_verification_mail((string)$user['email'], (string)$result['token']);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$errors = $result['errors'];
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<title>Kaffeeliste</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<link rel="stylesheet" href="assets/css/main.css" />
|
||||
<style>
|
||||
body {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.landing-shell {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.landing-hero {
|
||||
min-height: 82vh;
|
||||
background-image: url("assets/images/landing-hero.png");
|
||||
background-size: cover;
|
||||
background-position: center right;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4rem 6vw;
|
||||
}
|
||||
|
||||
.landing-hero:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
}
|
||||
|
||||
.landing-hero-content {
|
||||
position: relative;
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.landing-hero h1 {
|
||||
margin-bottom: 0.35em;
|
||||
}
|
||||
|
||||
.landing-hero p {
|
||||
color: #3d4449;
|
||||
font-size: 1.15em;
|
||||
max-width: 34rem;
|
||||
}
|
||||
|
||||
.landing-nav {
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
left: 6vw;
|
||||
right: 6vw;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.landing-nav strong {
|
||||
color: #3d4449;
|
||||
font-family: "Roboto Slab", serif;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.landing-section {
|
||||
padding: 4rem 6vw;
|
||||
}
|
||||
|
||||
.landing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
max-width: 72rem;
|
||||
}
|
||||
|
||||
.landing-feature {
|
||||
border: solid 1px rgba(210, 215, 217, 0.75);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.landing-feature h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.landing-feature p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
.landing-hero {
|
||||
min-height: 78vh;
|
||||
padding: 5.5rem 2rem 3rem 2rem;
|
||||
background-position: center right 30%;
|
||||
}
|
||||
|
||||
.landing-nav {
|
||||
left: 2rem;
|
||||
right: 2rem;
|
||||
}
|
||||
|
||||
.landing-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.landing-section {
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="is-preload">
|
||||
<div class="landing-shell">
|
||||
<section class="landing-hero">
|
||||
<nav class="landing-nav" aria-label="Hauptnavigation">
|
||||
<strong>Kaffeeliste</strong>
|
||||
<ul class="actions">
|
||||
<li><a href="login.php" class="button">Login</a></li>
|
||||
<li><a href="register.php" class="button primary">Registrieren</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="landing-hero-content">
|
||||
<h1>Kaffeeliste</h1>
|
||||
<p>Die digitale Kaffeekasse fuer Teams, Bueros und Vereine: Striche, Einzahlungen und offene Betraege bleiben nachvollziehbar an einem Ort.</p>
|
||||
<ul class="actions">
|
||||
<li><a href="register.php" class="button primary big">Kundenkonto anlegen</a></li>
|
||||
<li><a href="login.php" class="button big">Zur App</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="landing-section">
|
||||
<header class="major">
|
||||
<h2>Alles rund um die Kaffeekasse</h2>
|
||||
</header>
|
||||
<div class="landing-grid">
|
||||
<article class="landing-feature">
|
||||
<h3>Striche erfassen</h3>
|
||||
<p>Teilnehmer und Admins behalten Verbrauch, Einzahlungen und Salden im Blick.</p>
|
||||
</article>
|
||||
<article class="landing-feature">
|
||||
<h3>Mandantenfaehig</h3>
|
||||
<p>Jeder Kunde arbeitet in seinem eigenen Bereich mit eigenen Einstellungen.</p>
|
||||
</article>
|
||||
<article class="landing-feature">
|
||||
<h3>Webspace-tauglich</h3>
|
||||
<p>Der Start erfolgt ueber eine zentrale App-Adresse mit Login und Mandantenauswahl.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/jquery.min.js"></script>
|
||||
<script src="assets/js/browser.min.js"></script>
|
||||
<script src="assets/js/breakpoints.min.js"></script>
|
||||
<script src="assets/js/util.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/functions.php';
|
||||
require_once __DIR__ . '/app/saas-mail.php';
|
||||
|
||||
$pdo = app_db_pdo();
|
||||
$email = trim((string)($_POST['email'] ?? ''));
|
||||
@@ -18,6 +19,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (saas_should_show_auth_links() && !empty($result['token'])) {
|
||||
$resetLink = 'passwort-zuruecksetzen.php?token=' . urlencode((string)$result['token']);
|
||||
}
|
||||
if (!empty($result['token'])) {
|
||||
saas_send_password_reset_mail($email, (string)$result['token']);
|
||||
}
|
||||
} else {
|
||||
$errors = $result['errors'];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/functions.php';
|
||||
require_once __DIR__ . '/app/saas-mail.php';
|
||||
|
||||
$pdo = app_db_pdo();
|
||||
$errors = [];
|
||||
@@ -24,6 +25,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
]);
|
||||
|
||||
if ($result['ok']) {
|
||||
if (!empty($result['email_verification_token'])) {
|
||||
saas_send_email_verification_mail($values['email'], (string)$result['email_verification_token']);
|
||||
}
|
||||
saas_session_login($result['identity']);
|
||||
if (saas_should_show_auth_links() && !empty($result['email_verification_token'])) {
|
||||
$_SESSION['saas_dev_email_verification_token'] = (string)$result['email_verification_token'];
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../app/saas-mail.php';
|
||||
|
||||
function mail_check_assert(string $label, bool $condition, array &$failures, int &$passes): void
|
||||
{
|
||||
if ($condition) {
|
||||
$passes++;
|
||||
echo "PASS {$label}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$failures[] = $label;
|
||||
echo "FAIL {$label}\n";
|
||||
}
|
||||
|
||||
$logDir = APP_ROOT . '/var/test-mail';
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0700, true);
|
||||
}
|
||||
|
||||
foreach (glob($logDir . '/*.txt') ?: [] as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
putenv('APP_MAIL_TRANSPORT=log');
|
||||
putenv('APP_MAIL_LOG_DIR=' . $logDir);
|
||||
putenv('APP_BASE_URL=https://app.example.test');
|
||||
putenv('APP_MAIL_FROM=noreply@example.test');
|
||||
putenv('APP_MAIL_FROM_NAME=Kaffeeliste Test');
|
||||
|
||||
$failures = [];
|
||||
$passes = 0;
|
||||
|
||||
$reset = saas_send_password_reset_mail('mail-flow@test.local', str_repeat('a', 64));
|
||||
mail_check_assert('password reset mail logs successfully', $reset['ok'] === true, $failures, $passes);
|
||||
mail_check_assert('password reset mail file exists', isset($reset['path']) && is_file($reset['path']), $failures, $passes);
|
||||
if (isset($reset['path']) && is_file($reset['path'])) {
|
||||
$content = file_get_contents($reset['path']) ?: '';
|
||||
mail_check_assert('password reset mail contains app url', str_contains($content, 'https://app.example.test/passwort-zuruecksetzen.php?token='), $failures, $passes);
|
||||
}
|
||||
|
||||
$verification = saas_send_email_verification_mail('mail-flow@test.local', str_repeat('b', 64));
|
||||
mail_check_assert('email verification mail logs successfully', $verification['ok'] === true, $failures, $passes);
|
||||
mail_check_assert('email verification mail file exists', isset($verification['path']) && is_file($verification['path']), $failures, $passes);
|
||||
if (isset($verification['path']) && is_file($verification['path'])) {
|
||||
$content = file_get_contents($verification['path']) ?: '';
|
||||
mail_check_assert('email verification mail contains app url', str_contains($content, 'https://app.example.test/email-verifizieren.php?token='), $failures, $passes);
|
||||
}
|
||||
|
||||
$invalid = saas_send_mail('not-an-email', 'Test', 'Body');
|
||||
mail_check_assert('invalid recipient is rejected', $invalid['ok'] === false, $failures, $passes);
|
||||
|
||||
foreach (glob($logDir . '/*.txt') ?: [] as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
if ($failures !== []) {
|
||||
echo "\nM3 mail flow check failed with " . count($failures) . " failure(s):\n";
|
||||
foreach ($failures as $failure) {
|
||||
echo "- {$failure}\n";
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "\nM3 mail flow check passed with {$passes} assertions.\n";
|
||||
@@ -6,6 +6,11 @@ $baseUrl = rtrim((string)(getenv('SMOKE_BASE_URL') ?: 'http://127.0.0.1:8080'),
|
||||
$issuePattern = '~(?:Deprecated|Warning|Fatal error|Parse error|Notice|Uncaught (?:Error|Exception))~i';
|
||||
|
||||
$checks = [
|
||||
[
|
||||
'label' => 'Landingpage',
|
||||
'path' => 'landing.php',
|
||||
'contains' => ['Kaffeeliste', 'Kundenkonto anlegen', 'Mandantenfaehig'],
|
||||
],
|
||||
[
|
||||
'label' => 'Dashboard',
|
||||
'path' => 'index.php',
|
||||
|
||||
Reference in New Issue
Block a user