diff --git a/hifi-src/src/App.jsx b/hifi-src/src/App.jsx
index 682c5e4..d95057e 100644
--- a/hifi-src/src/App.jsx
+++ b/hifi-src/src/App.jsx
@@ -37,6 +37,7 @@ import AdminUsers from './pages/admin/AdminUsers.jsx';
import PermissionGroups from './pages/admin/PermissionGroups.jsx';
import SettingsLayout from './pages/admin/settings/SettingsLayout.jsx';
import WebsiteSettings from './pages/admin/settings/WebsiteSettings.jsx';
+import EmailSettings from './pages/admin/settings/EmailSettings.jsx';
import DatabaseSettings from './pages/admin/settings/DatabaseSettings.jsx';
import ExportImportSettings from './pages/admin/settings/ExportImportSettings.jsx';
import MaintenanceSettings from './pages/admin/settings/MaintenanceSettings.jsx';
@@ -89,6 +90,7 @@ export default function App() {
>
Lädt…
; + } + + return ( + + ); +} diff --git a/hifi-src/src/pages/admin/settings/SettingsLayout.jsx b/hifi-src/src/pages/admin/settings/SettingsLayout.jsx index fecbfe2..0190c2a 100644 --- a/hifi-src/src/pages/admin/settings/SettingsLayout.jsx +++ b/hifi-src/src/pages/admin/settings/SettingsLayout.jsx @@ -2,6 +2,7 @@ import { NavLink, Outlet } from 'react-router-dom'; const tabs = [ { to: '/admin/settings/website', label: 'Website' }, + { to: '/admin/settings/email', label: 'E-Mail' }, { to: '/admin/settings/database', label: 'Datenbank' }, { to: '/admin/settings/export-import', label: 'Export & Import' }, { to: '/admin/settings/maintenance', label: 'Wartungsmodus' }, diff --git a/hifi/api/database/migration_mail_settings.sql b/hifi/api/database/migration_mail_settings.sql new file mode 100644 index 0000000..6bdd517 --- /dev/null +++ b/hifi/api/database/migration_mail_settings.sql @@ -0,0 +1,18 @@ +-- SMTP-Einstellungen und E-Mail-Vorlagen (Kundenbestaetigung + Shop-Benachrichtigung) +-- admin-konfigurierbar machen. Additiv, nichts wird geloescht. +-- Hinweis: Auf der Live-Seite reicht stattdessen ein Klick auf "Datenbankstruktur aktualisieren" +-- unter Admin-Panel -> Einstellungen -> Datenbank. + +ALTER TABLE app_settings + ADD COLUMN IF NOT EXISTS mail_host VARCHAR(255) NULL, + ADD COLUMN IF NOT EXISTS mail_port INT NULL, + ADD COLUMN IF NOT EXISTS mail_username VARCHAR(255) NULL, + ADD COLUMN IF NOT EXISTS mail_password VARCHAR(255) NULL, + ADD COLUMN IF NOT EXISTS mail_encryption VARCHAR(10) NOT NULL DEFAULT 'tls', + ADD COLUMN IF NOT EXISTS mail_from_email VARCHAR(150) NULL, + ADD COLUMN IF NOT EXISTS mail_from_name VARCHAR(150) NULL, + ADD COLUMN IF NOT EXISTS mail_notify_email VARCHAR(150) NULL, + ADD COLUMN IF NOT EXISTS mail_customer_subject VARCHAR(255) NULL, + ADD COLUMN IF NOT EXISTS mail_customer_body TEXT NULL, + ADD COLUMN IF NOT EXISTS mail_owner_subject VARCHAR(255) NULL, + ADD COLUMN IF NOT EXISTS mail_owner_body TEXT NULL; diff --git a/hifi/api/database/schema.sql b/hifi/api/database/schema.sql index 5f21c78..1d254af 100644 --- a/hifi/api/database/schema.sql +++ b/hifi/api/database/schema.sql @@ -24,6 +24,18 @@ CREATE TABLE app_settings ( whatsapp VARCHAR(50) NULL, contact_email VARCHAR(150) NULL, hero_image_path VARCHAR(255) NULL, + mail_host VARCHAR(255) NULL, + mail_port INT NULL, + mail_username VARCHAR(255) NULL, + mail_password VARCHAR(255) NULL, + mail_encryption VARCHAR(10) NOT NULL DEFAULT 'tls', + mail_from_email VARCHAR(150) NULL, + mail_from_name VARCHAR(150) NULL, + mail_notify_email VARCHAR(150) NULL, + mail_customer_subject VARCHAR(255) NULL, + mail_customer_body TEXT NULL, + mail_owner_subject VARCHAR(255) NULL, + mail_owner_body TEXT NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB; diff --git a/hifi/api/public/index.php b/hifi/api/public/index.php index 0f1f471..098db03 100644 --- a/hifi/api/public/index.php +++ b/hifi/api/public/index.php @@ -16,6 +16,7 @@ use App\Controllers\DatabaseConfigController; use App\Controllers\GalleryBrandController; use App\Controllers\GalleryPhotoController; use App\Controllers\GalleryProjectController; +use App\Controllers\MailSettingsController; use App\Controllers\MaintenanceController; use App\Controllers\ModelController; use App\Controllers\PackageController; @@ -174,6 +175,10 @@ $router->post('/settings/schema-migrate', $perm('settings.manage', fn($p) => Sch $router->get('/settings/database', $perm('settings.manage', fn($p) => DatabaseConfigController::show())); $router->post('/settings/database', $perm('settings.manage', fn($p) => DatabaseConfigController::update())); +$router->get('/settings/mail', $perm('settings.manage', fn($p) => MailSettingsController::show())); +$router->post('/settings/mail', $perm('settings.manage', fn($p) => MailSettingsController::update())); +$router->post('/settings/mail/test', $perm('settings.manage', fn($p) => MailSettingsController::test())); + $router->get('/maintenance', fn($p) => MaintenanceController::status()); $router->post('/maintenance', $perm('settings.manage', fn($p) => MaintenanceController::update())); diff --git a/hifi/api/src/Controllers/ContactController.php b/hifi/api/src/Controllers/ContactController.php index cbf671b..9fb7c31 100644 --- a/hifi/api/src/Controllers/ContactController.php +++ b/hifi/api/src/Controllers/ContactController.php @@ -4,7 +4,7 @@ namespace App\Controllers; use App\Config\Database; use App\Support\Http; -use PHPMailer\PHPMailer\PHPMailer; +use App\Support\Mailer; class ContactController { @@ -43,7 +43,9 @@ class ContactController ]); $id = (int) $db->lastInsertId(); - self::sendNotificationMail($body, $name, $email, $selectedUpgrades); + $vars = self::templateVars($body, $name, $email, $selectedUpgrades); + self::sendOwnerNotification($vars); + self::sendCustomerConfirmation($vars, $email); Http::send(['ok' => true, 'id' => $id], 201); } @@ -105,51 +107,65 @@ class ContactController Http::send(['ok' => true]); } - private static function sendNotificationMail(array $body, string $name, string $email, array $selectedUpgrades = []): void + /** + * Baut die Platzhalter-Werte fuer beide Vorlagen (Kundenbestaetigung + Shop-Benachrichtigung) + * einmal zentral, statt sie in jeder Mail-Methode einzeln zusammenzusetzen. + */ + private static function templateVars(array $body, string $name, string $email, array $selectedUpgrades): array { - $config = require __DIR__ . '/../../config/config.php'; - $mailConfig = $config['mail']; + $upgradesText = '-'; + if ($selectedUpgrades) { + $upgradesText = implode("\n", array_map( + fn($u) => sprintf('- %s (%.2f €)', $u['name'], $u['price']), + $selectedUpgrades + )); + } - if (empty($mailConfig['host'])) { + return [ + 'name' => $name, + 'email' => $email, + 'phone' => $body['phone'] ?? null, + 'vin' => $body['vin'] ?? null, + 'message' => $body['message'] ?? null, + 'brand' => $body['brand_name'] ?? null, + 'model' => $body['model_name'] ?? null, + 'package' => $body['package_name'] ?? null, + 'product' => $body['product_name'] ?? null, + 'upgrades' => $upgradesText, + ]; + } + + private static function sendOwnerNotification(array $vars): void + { + $config = Mailer::resolveConfig(); + if (empty($config['host']) || empty($config['notify_email'])) { return; } try { - $mail = new PHPMailer(true); - $mail->isSMTP(); - $mail->Host = $mailConfig['host']; - $mail->Port = $mailConfig['port']; - $mail->SMTPAuth = true; - $mail->Username = $mailConfig['username']; - $mail->Password = $mailConfig['password']; - $mail->SMTPSecure = $mailConfig['encryption']; - $mail->CharSet = 'UTF-8'; + $mail = Mailer::build($config); + $mail->addAddress($config['notify_email']); + $mail->addReplyTo($vars['email'], $vars['name']); + $mail->Subject = Mailer::render($config['owner_subject'], $vars); + $mail->Body = Mailer::render($config['owner_body'], $vars); + $mail->send(); + } catch (\Throwable $e) { + // Mailversand ist best effort – die Anfrage ist bereits in der DB gespeichert. + } + } - $mail->setFrom($mailConfig['from_email'], $mailConfig['from_name']); - $mail->addAddress($mailConfig['to_email']); - $mail->addReplyTo($email, $name); - - $context = trim(sprintf( - "Marke: %s\nModell: %s\nPaket: %s\nProdukt: %s", - $body['brand_name'] ?? '-', - $body['model_name'] ?? '-', - $body['package_name'] ?? '-', - $body['product_name'] ?? '-' - )); - - $upgradesText = '-'; - if ($selectedUpgrades) { - $upgradesText = implode("\n", array_map( - fn($u) => sprintf('- %s (%.2f €)', $u['name'], $u['price']), - $selectedUpgrades - )); - } - - $mail->Subject = 'Neue Kontaktanfrage von ' . $name; - $mail->Body = "Name: {$name}\nE-Mail: {$email}\nTelefon: " . ($body['phone'] ?? '-') - . "\nFahrgestellnummer (FIN): " . ($body['vin'] ?? '-') . "\n\n" - . $context . "\n\nGewünschte Upgrades:\n" . $upgradesText . "\n\nNachricht:\n" . ($body['message'] ?? '-'); + private static function sendCustomerConfirmation(array $vars, string $email): void + { + $config = Mailer::resolveConfig(); + if (empty($config['host'])) { + return; + } + try { + $mail = Mailer::build($config); + $mail->addAddress($email, $vars['name']); + $mail->Subject = Mailer::render($config['customer_subject'], $vars); + $mail->Body = Mailer::render($config['customer_body'], $vars); $mail->send(); } catch (\Throwable $e) { // Mailversand ist best effort – die Anfrage ist bereits in der DB gespeichert. diff --git a/hifi/api/src/Controllers/MailSettingsController.php b/hifi/api/src/Controllers/MailSettingsController.php new file mode 100644 index 0000000..315707c --- /dev/null +++ b/hifi/api/src/Controllers/MailSettingsController.php @@ -0,0 +1,134 @@ + '', + 'mail_port' => 587, + 'mail_username' => '', + 'mail_encryption' => 'tls', + 'mail_from_email' => '', + 'mail_from_name' => 'HifiPlanet', + 'mail_notify_email' => '', + 'mail_customer_subject' => Mailer::DEFAULT_CUSTOMER_SUBJECT, + 'mail_customer_body' => Mailer::DEFAULT_CUSTOMER_BODY, + 'mail_owner_subject' => Mailer::DEFAULT_OWNER_SUBJECT, + 'mail_owner_body' => Mailer::DEFAULT_OWNER_BODY, + ]; + + private static function currentRow(): array + { + try { + $stmt = Database::connection()->query( + 'SELECT mail_host, mail_port, mail_username, mail_password, mail_encryption, + mail_from_email, mail_from_name, mail_notify_email, + mail_customer_subject, mail_customer_body, mail_owner_subject, mail_owner_body + FROM app_settings WHERE id = 1' + ); + return $stmt->fetch() ?: []; + } catch (\Throwable $e) { + return []; + } + } + + public static function show(): void + { + $row = self::currentRow(); + + $result = []; + foreach (self::DEFAULTS as $key => $default) { + $result[$key] = ($row[$key] ?? null) !== null && $row[$key] !== '' ? $row[$key] : $default; + } + unset($result['mail_password']); + + Http::send(array_merge($result, [ + 'has_password' => !empty($row['mail_password']), + ])); + } + + public static function update(): void + { + $body = Http::jsonBody(); + $db = Database::connection(); + $current = self::currentRow(); + + // Leeres Passwort im Formular = aktuelles Passwort beibehalten (wird beim Laden nie + // im Klartext an den Browser geschickt, siehe show()). + $password = ($body['mail_password'] ?? '') !== '' ? $body['mail_password'] : ($current['mail_password'] ?? null); + + $db->exec('INSERT IGNORE INTO app_settings (id) VALUES (1)'); + $stmt = $db->prepare( + 'UPDATE app_settings SET + mail_host = ?, mail_port = ?, mail_username = ?, mail_password = ?, mail_encryption = ?, + mail_from_email = ?, mail_from_name = ?, mail_notify_email = ?, + mail_customer_subject = ?, mail_customer_body = ?, mail_owner_subject = ?, mail_owner_body = ? + WHERE id = 1' + ); + $stmt->execute([ + trim($body['mail_host'] ?? '') ?: null, + !empty($body['mail_port']) ? (int) $body['mail_port'] : null, + trim($body['mail_username'] ?? '') ?: null, + $password ?: null, + trim($body['mail_encryption'] ?? '') ?: 'tls', + trim($body['mail_from_email'] ?? '') ?: null, + trim($body['mail_from_name'] ?? '') ?: null, + trim($body['mail_notify_email'] ?? '') ?: null, + trim($body['mail_customer_subject'] ?? '') ?: null, + $body['mail_customer_body'] ?? null, + trim($body['mail_owner_subject'] ?? '') ?: null, + $body['mail_owner_body'] ?? null, + ]); + + self::show(); + } + + /** + * Schickt eine echte Testmail mit den (moeglicherweise noch ungespeicherten) Formularwerten, + * damit der Admin sich nicht erst durchklicken muss, um zu sehen, ob die SMTP-Daten stimmen. + */ + public static function test(): void + { + $body = Http::jsonBody(); + $current = self::currentRow(); + + $host = trim($body['mail_host'] ?? ''); + $fromEmail = trim($body['mail_from_email'] ?? ''); + $target = trim($body['mail_notify_email'] ?? '') ?: $fromEmail; + $password = ($body['mail_password'] ?? '') !== '' ? $body['mail_password'] : ($current['mail_password'] ?? ''); + + if ($host === '' || $fromEmail === '') { + Http::error('Server und Absender-E-Mail werden fuer den Test benoetigt', 422); + } + if (!filter_var($target, FILTER_VALIDATE_EMAIL)) { + Http::error('Empfaenger-E-Mail (Benachrichtigungs- oder Absenderadresse) ist ungueltig', 422); + } + + try { + $mail = Mailer::build([ + 'host' => $host, + 'port' => $body['mail_port'] ?? 587, + 'username' => trim($body['mail_username'] ?? ''), + 'password' => $password, + 'encryption' => trim($body['mail_encryption'] ?? '') ?: 'tls', + 'from_email' => $fromEmail, + 'from_name' => trim($body['mail_from_name'] ?? '') ?: 'HifiPlanet', + ]); + $mail->addAddress($target); + $mail->Subject = 'Testmail von HifiPlanet'; + $mail->Body = "Diese Testmail bestaetigt, dass deine SMTP-Einstellungen funktionieren."; + $mail->send(); + } catch (\Throwable $e) { + Http::error('Test fehlgeschlagen: ' . $e->getMessage(), 422); + } + + Http::send(['ok' => true, 'sent_to' => $target]); + } +} diff --git a/hifi/api/src/Support/Mailer.php b/hifi/api/src/Support/Mailer.php new file mode 100644 index 0000000..21a8d93 --- /dev/null +++ b/hifi/api/src/Support/Mailer.php @@ -0,0 +1,127 @@ +query( + 'SELECT mail_host, mail_port, mail_username, mail_password, mail_encryption, + mail_from_email, mail_from_name, mail_notify_email, + mail_customer_subject, mail_customer_body, mail_owner_subject, mail_owner_body + FROM app_settings WHERE id = 1' + ); + $row = $stmt->fetch() ?: []; + } catch (\Throwable $e) { + $row = []; + } + + $str = fn($value) => $value !== null && $value !== '' ? $value : null; + + return [ + 'host' => $str($row['mail_host'] ?? null) ?? $fallback['host'], + 'port' => $str($row['mail_port'] ?? null) ?? $fallback['port'], + 'username' => $str($row['mail_username'] ?? null) ?? $fallback['username'], + 'password' => $str($row['mail_password'] ?? null) ?? $fallback['password'], + 'encryption' => $str($row['mail_encryption'] ?? null) ?? $fallback['encryption'], + 'from_email' => $str($row['mail_from_email'] ?? null) ?? $fallback['from_email'], + 'from_name' => $str($row['mail_from_name'] ?? null) ?? $fallback['from_name'], + 'notify_email' => $str($row['mail_notify_email'] ?? null) ?? $fallback['to_email'], + 'customer_subject' => $str($row['mail_customer_subject'] ?? null) ?? self::DEFAULT_CUSTOMER_SUBJECT, + 'customer_body' => $str($row['mail_customer_body'] ?? null) ?? self::DEFAULT_CUSTOMER_BODY, + 'owner_subject' => $str($row['mail_owner_subject'] ?? null) ?? self::DEFAULT_OWNER_SUBJECT, + 'owner_body' => $str($row['mail_owner_body'] ?? null) ?? self::DEFAULT_OWNER_BODY, + ]; + } + + /** + * Baut einen fertig konfigurierten, aber noch nicht abgeschickten PHPMailer aus den + * uebergebenen SMTP-Feldern (host/port/username/password/encryption/from_email/from_name). + */ + public static function build(array $config): PHPMailer + { + $mail = new PHPMailer(true); + $mail->isSMTP(); + $mail->Host = $config['host']; + $mail->Port = (int) $config['port']; + $mail->SMTPAuth = true; + $mail->Username = $config['username']; + $mail->Password = $config['password']; + $mail->SMTPSecure = ($config['encryption'] ?? '') !== 'none' ? $config['encryption'] : false; + $mail->CharSet = 'UTF-8'; + $mail->setFrom($config['from_email'], $config['from_name'] ?: $config['from_email']); + + return $mail; + } + + /** + * Ersetzt {{platzhalter}} in einer Vorlage. Unbekannte Platzhalter bleiben unveraendert + * stehen (auffaelliger als sie einfach zu leeren, falls sich im Text ein Tippfehler einschleicht). + */ + public static function render(string $template, array $vars): string + { + $replacements = []; + foreach ($vars as $key => $value) { + $replacements['{{' . $key . '}}'] = $value ?? '-'; + } + + return strtr($template, $replacements); + } +} diff --git a/hifi/api/src/Support/Schema.php b/hifi/api/src/Support/Schema.php index 598fa40..c24ecae 100644 --- a/hifi/api/src/Support/Schema.php +++ b/hifi/api/src/Support/Schema.php @@ -35,6 +35,18 @@ class Schema whatsapp VARCHAR(50) NULL, contact_email VARCHAR(150) NULL, hero_image_path VARCHAR(255) NULL, + mail_host VARCHAR(255) NULL, + mail_port INT NULL, + mail_username VARCHAR(255) NULL, + mail_password VARCHAR(255) NULL, + mail_encryption VARCHAR(10) NOT NULL DEFAULT 'tls', + mail_from_email VARCHAR(150) NULL, + mail_from_name VARCHAR(150) NULL, + mail_notify_email VARCHAR(150) NULL, + mail_customer_subject VARCHAR(255) NULL, + mail_customer_body TEXT NULL, + mail_owner_subject VARCHAR(255) NULL, + mail_owner_body TEXT NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB", @@ -230,6 +242,18 @@ class Schema 'whatsapp' => 'VARCHAR(50) NULL', 'contact_email' => 'VARCHAR(150) NULL', 'hero_image_path' => 'VARCHAR(255) NULL', + 'mail_host' => 'VARCHAR(255) NULL', + 'mail_port' => 'INT NULL', + 'mail_username' => 'VARCHAR(255) NULL', + 'mail_password' => 'VARCHAR(255) NULL', + 'mail_encryption' => "VARCHAR(10) NOT NULL DEFAULT 'tls'", + 'mail_from_email' => 'VARCHAR(150) NULL', + 'mail_from_name' => 'VARCHAR(150) NULL', + 'mail_notify_email' => 'VARCHAR(150) NULL', + 'mail_customer_subject' => 'VARCHAR(255) NULL', + 'mail_customer_body' => 'TEXT NULL', + 'mail_owner_subject' => 'VARCHAR(255) NULL', + 'mail_owner_body' => 'TEXT NULL', 'updated_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', ], 'services' => [