Hinweise: - Neue Tabelle notices (tenant-scoped, Soft-Delete via deleted_at) loest die global unscoped kl_hinweise als aktive Datenquelle ab; Migration uebernimmt einmalig aktuell gueltige kl_hinweise-Eintraege fuer den Default-Mandanten. kl_hinweise bleibt als Golden-Master-Referenz stehen. - hinweise.php und der Banner in header.php sind tenant-scoped umgestellt. Mitgliederverwaltung: - mitarbeiterverwalten.php verwaltet jetzt participants (tenant-scoped) statt der global unscoped kl_Mitarbeiter-Tabelle als primaere Quelle. Das behebt nebenbei ein Mandanten-Datenleck: jeder SaaS-Mandant mit Owner/Admin-Rolle haette zuvor die komplette Default-Mandanten- Mitgliederliste sehen und bearbeiten koennen. - Fuer den Default-Mandanten bleibt Dual-Write nach kl_Mitarbeiter bestehen, damit stricheintragen.php/einzahlung.php weiter funktionieren; andere Mandanten werden rein participant-nativ verwaltet. - Die Legacy-Administrator-Checkbox ist raus. Stattdessen kann ein Admin je Mitglied unabhaengig von Name/E-Mail einen Login-Zugang mit Rolle (member/treasurer/admin) gewaehren oder entziehen (saas_grant_participant_access / saas_revoke_participant_access). Einladung laeuft ueber den bestehenden Passwort-Reset-Mechanismus, Entzug setzt die Mitgliedschaft auf revoked statt sie zu loeschen. - Kompletter Flow live getestet: anlegen, Zugang gewaehren, Einladungsmail, Passwort setzen, Login, Rollenschutz, Zugang entziehen, Login-Sperre. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
172 lines
4.7 KiB
PHP
172 lines
4.7 KiB
PHP
<?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,',
|
|
'',
|
|
'für dein Kaffeeliste-Konto wurde ein Passwort-Reset angefordert.',
|
|
'Öffne 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 zurücksetzen', $body);
|
|
}
|
|
|
|
function saas_send_invite_mail(string $to, string $tenantName, string $role, string $token): array
|
|
{
|
|
$link = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($token));
|
|
$body = implode("\n", [
|
|
'Hallo,',
|
|
'',
|
|
"du wurdest für \"{$tenantName}\" in der Kaffeeliste als {$role} eingeladen.",
|
|
'Öffne diesen Link, um ein Passwort zu setzen und dich anzumelden:',
|
|
'',
|
|
$link,
|
|
'',
|
|
'Der Link ist zeitlich begrenzt und kann nur einmal verwendet werden.',
|
|
'Falls du diese Einladung nicht erwartet hast, kannst du diese Nachricht ignorieren.',
|
|
'',
|
|
'Deine Kaffeeliste',
|
|
]);
|
|
|
|
return saas_send_mail($to, 'Einladung zur Kaffeeliste', $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 bestätige deine E-Mail-Adresse für Kaffeeliste.',
|
|
'Öffne 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 bestätigen', $body);
|
|
}
|