Преглед на файлове

expanding admin management capabilities.
Moved admin management in app.

Medowar преди 1 месец
родител
ревизия
953e771d0c
променени са 7 файла, в които са добавени 639 реда и са изтрити 5 реда
  1. 292 0
      admin/admins.php
  2. 1 0
      admin/index.php
  3. 4 2
      admin/login.php
  4. 6 1
      config.sample.php
  5. 12 0
      data/admins.json
  6. 59 0
      docs/ADMIN_SYSTEM.md
  7. 265 2
      includes/functions.php

+ 292 - 0
admin/admins.php

@@ -0,0 +1,292 @@
+<?php
+require_once __DIR__ . '/../config.php';
+require_once __DIR__ . '/../includes/functions.php';
+
+// Check admin login
+if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
+    header('Location: login.php');
+    exit;
+}
+
+$pageTitle = 'Admins verwalten';
+$message = '';
+$messageType = '';
+
+function isValidAdminPasswordInput($password) {
+    return is_string($password) && strlen($password) >= 8;
+}
+
+$adminAccounts = getAdminAccounts();
+
+function isValidAdminEmailInput($email) {
+    return isValidAdminEmail($email);
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    if (isset($_POST['add_admin'])) {
+        $username = normalizeAdminUsername($_POST['username'] ?? '');
+        $description = normalizeAdminDescription($_POST['description'] ?? '');
+        $email = normalizeAdminEmail($_POST['email'] ?? '');
+        $password = $_POST['password'] ?? '';
+        $passwordConfirm = $_POST['password_confirm'] ?? '';
+
+        if (!isValidAdminUsername($username)) {
+            $message = 'Ungültiger Benutzername. Erlaubt: 3-50 Zeichen (Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich).';
+            $messageType = 'error';
+        } elseif (isset($adminAccounts[$username])) {
+            $message = 'Dieser Benutzername existiert bereits.';
+            $messageType = 'error';
+        } elseif (!isValidAdminDescription($description)) {
+            $message = 'Beschreibung ist erforderlich (max. 120 Zeichen).';
+            $messageType = 'error';
+        } elseif (!isValidAdminEmailInput($email)) {
+            $message = 'Gültige E-Mail ist erforderlich.';
+            $messageType = 'error';
+        } elseif (!isValidAdminPasswordInput($password)) {
+            $message = 'Passwort muss mindestens 8 Zeichen lang sein.';
+            $messageType = 'error';
+        } elseif ($password !== $passwordConfirm) {
+            $message = 'Passwort und Bestätigung stimmen nicht überein.';
+            $messageType = 'error';
+        } else {
+            $adminAccounts[$username] = [
+                'password_hash' => password_hash($password, PASSWORD_BCRYPT),
+                'description' => $description,
+                'email' => $email
+            ];
+            saveAdminAccounts($adminAccounts);
+            $message = 'Admin wurde erfolgreich angelegt.';
+            $messageType = 'success';
+        }
+    }
+
+    if (isset($_POST['update_description'])) {
+        $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? '');
+        $description = normalizeAdminDescription($_POST['description'] ?? '');
+        $email = normalizeAdminEmail($_POST['email'] ?? '');
+
+        if (!isset($adminAccounts[$targetUsername])) {
+            $message = 'Admin nicht gefunden.';
+            $messageType = 'error';
+        } elseif (!isValidAdminDescription($description)) {
+            $message = 'Beschreibung ist erforderlich (max. 120 Zeichen).';
+            $messageType = 'error';
+        } elseif (!isValidAdminEmailInput($email)) {
+            $message = 'Gültige E-Mail ist erforderlich.';
+            $messageType = 'error';
+        } else {
+            $adminAccounts[$targetUsername]['description'] = $description;
+            $adminAccounts[$targetUsername]['email'] = $email;
+            saveAdminAccounts($adminAccounts);
+            $message = 'Beschreibung und E-Mail wurden aktualisiert.';
+            $messageType = 'success';
+        }
+    }
+
+    if (isset($_POST['change_password'])) {
+        $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? '');
+        $newPassword = $_POST['new_password'] ?? '';
+        $newPasswordConfirm = $_POST['new_password_confirm'] ?? '';
+
+        if (!isset($adminAccounts[$targetUsername])) {
+            $message = 'Admin nicht gefunden.';
+            $messageType = 'error';
+        } elseif (!isValidAdminPasswordInput($newPassword)) {
+            $message = 'Passwort muss mindestens 8 Zeichen lang sein.';
+            $messageType = 'error';
+        } elseif ($newPassword !== $newPasswordConfirm) {
+            $message = 'Passwort und Bestätigung stimmen nicht überein.';
+            $messageType = 'error';
+        } else {
+            $adminAccounts[$targetUsername]['password_hash'] = password_hash($newPassword, PASSWORD_BCRYPT);
+            saveAdminAccounts($adminAccounts);
+            $message = 'Passwort wurde aktualisiert.';
+            $messageType = 'success';
+        }
+    }
+
+    if (isset($_POST['delete_admin'])) {
+        $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? '');
+
+        if (!isset($adminAccounts[$targetUsername])) {
+            $message = 'Admin nicht gefunden.';
+            $messageType = 'error';
+        } else {
+            unset($adminAccounts[$targetUsername]);
+            saveAdminAccounts($adminAccounts);
+
+            if (isset($_SESSION['admin_username']) && $_SESSION['admin_username'] === $targetUsername) {
+                $_SESSION['admin_logged_in'] = false;
+                unset($_SESSION['admin_username']);
+                session_destroy();
+                header('Location: login.php');
+                exit;
+            }
+
+            $message = 'Admin wurde gelöscht.';
+            $messageType = 'success';
+        }
+    }
+
+    $adminAccounts = getAdminAccounts();
+}
+
+$currentAdmin = isset($_SESSION['admin_username']) ? normalizeAdminUsername($_SESSION['admin_username']) : '';
+$changeUsername = normalizeAdminUsername($_GET['change'] ?? '');
+$selectedChangeUser = null;
+$editDescriptionUsername = normalizeAdminUsername($_GET['edit_description'] ?? '');
+$selectedDescriptionUser = null;
+
+if ($changeUsername !== '') {
+    if (!isset($adminAccounts[$changeUsername])) {
+        if ($message === '') {
+            $message = 'Ausgewählter Admin wurde nicht gefunden.';
+            $messageType = 'error';
+        }
+    } else {
+        $selectedChangeUser = $changeUsername;
+    }
+}
+
+if ($editDescriptionUsername !== '') {
+    if (!isset($adminAccounts[$editDescriptionUsername])) {
+        if ($message === '') {
+            $message = 'Ausgewählter Admin wurde nicht gefunden.';
+            $messageType = 'error';
+        }
+    } else {
+        $selectedDescriptionUser = $editDescriptionUsername;
+    }
+}
+
+ksort($adminAccounts);
+
+$bodyClass = 'admin-page';
+include __DIR__ . '/../includes/header.php';
+?>
+
+<div class="admin-header">
+    <h2>Admins verwalten</h2>
+    <div>
+        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
+    </div>
+</div>
+
+<?php if ($message !== ''): ?>
+    <div class="alert alert-<?php echo $messageType; ?>">
+        <?php echo htmlspecialchars($message); ?>
+    </div>
+<?php endif; ?>
+
+<div class="panel">
+    <p><strong>Eingeloggt als:</strong> <?php echo htmlspecialchars($currentAdmin !== '' ? $currentAdmin : 'Unbekannt'); ?></p>
+</div>
+
+<div class="panel">
+    <h3>Neuen Admin anlegen</h3>
+    <form method="POST">
+        <div class="form-group">
+            <label for="username">Benutzername *</label>
+            <input type="text" id="username" name="username" required maxlength="50" pattern="[A-Za-z0-9][A-Za-z0-9._-]{2,49}" placeholder="z.B. max.mustermann">
+        </div>
+        <div class="form-group">
+                <label for="description">Beschreibung *</label>
+                <input type="text" id="description" name="description" required maxlength="120" placeholder="z.B. Kassierer, Shop-Team">
+            </div>
+            <div class="form-group">
+                <label for="email">E-Mail *</label>
+                <input type="email" id="email" name="email" required maxlength="190" placeholder="z.B. max.mustermann@example.org">
+            </div>
+            <div class="form-group">
+                <label for="password">Passwort (mind. 8 Zeichen) *</label>
+                <input type="password" id="password" name="password" required minlength="8">
+        </div>
+        <div class="form-group">
+            <label for="password_confirm">Passwort bestätigen *</label>
+            <input type="password" id="password_confirm" name="password_confirm" required minlength="8">
+        </div>
+        <button type="submit" name="add_admin" class="btn">Admin anlegen</button>
+    </form>
+</div>
+
+<div class="panel">
+    <h3>Admin-Liste</h3>
+    <div class="table-responsive">
+        <table class="responsive-table">
+            <thead>
+                <tr>
+                    <th>Benutzername</th>
+                    <th>Beschreibung</th>
+                    <th>E-Mail</th>
+                    <th>Aktionen</th>
+                </tr>
+            </thead>
+            <tbody>
+            <?php foreach ($adminAccounts as $username => $account): ?>
+                <tr>
+                    <td data-label="Benutzername">
+                        <strong><?php echo htmlspecialchars($username); ?></strong>
+                        <?php if ($username === $currentAdmin): ?>
+                            <span class="status status-open" style="margin-left: 0.5rem;">Du</span>
+                        <?php endif; ?>
+                    </td>
+                    <td data-label="Beschreibung">
+                        <?php echo htmlspecialchars($account['description']); ?>
+                    </td>
+                    <td data-label="E-Mail">
+                        <?php echo htmlspecialchars($account['email']); ?>
+                    </td>
+                    <td data-label="Aktionen">
+                        <a href="admins.php?edit_description=<?php echo urlencode($username); ?>" class="btn btn-small btn-secondary">Profil ändern</a>
+                        <a href="admins.php?change=<?php echo urlencode($username); ?>" class="btn btn-small btn-secondary">Passwort ändern</a>
+                        <form method="POST" style="display: inline;" onsubmit="return confirm('Admin wirklich löschen?');">
+                            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars($username); ?>">
+                            <button type="submit" name="delete_admin" class="btn btn-small">Löschen</button>
+                        </form>
+                    </td>
+                </tr>
+            <?php endforeach; ?>
+            </tbody>
+        </table>
+    </div>
+</div>
+
+<?php if ($selectedDescriptionUser !== null): ?>
+    <div class="panel">
+        <h3>Profil ändern: <?php echo htmlspecialchars($selectedDescriptionUser); ?></h3>
+        <form method="POST">
+            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars($selectedDescriptionUser); ?>">
+            <div class="form-group">
+                <label for="description_edit">Beschreibung *</label>
+                <input type="text" id="description_edit" name="description" maxlength="120" required value="<?php echo htmlspecialchars($adminAccounts[$selectedDescriptionUser]['description']); ?>">
+            </div>
+            <div class="form-group">
+                <label for="email_edit">E-Mail *</label>
+                <input type="email" id="email_edit" name="email" maxlength="190" required value="<?php echo htmlspecialchars($adminAccounts[$selectedDescriptionUser]['email']); ?>">
+            </div>
+            <button type="submit" name="update_description" class="btn">Profil speichern</button>
+            <a href="admins.php" class="btn btn-secondary">Abbrechen</a>
+        </form>
+    </div>
+<?php endif; ?>
+
+<?php if ($selectedChangeUser !== null): ?>
+    <div class="panel">
+        <h3>Passwort ändern: <?php echo htmlspecialchars($selectedChangeUser); ?></h3>
+        <form method="POST">
+            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars($selectedChangeUser); ?>">
+            <div class="form-group">
+                <label for="new_password">Neues Passwort (mind. 8 Zeichen) *</label>
+                <input type="password" id="new_password" name="new_password" required minlength="8">
+            </div>
+            <div class="form-group">
+                <label for="new_password_confirm">Neues Passwort bestätigen *</label>
+                <input type="password" id="new_password_confirm" name="new_password_confirm" required minlength="8">
+            </div>
+            <button type="submit" name="change_password" class="btn">Passwort speichern</button>
+            <a href="admins.php" class="btn btn-secondary">Abbrechen</a>
+        </form>
+    </div>
+<?php endif; ?>
+
+<?php include __DIR__ . '/../includes/footer.php'; ?>

+ 1 - 0
admin/index.php

@@ -54,6 +54,7 @@ include __DIR__ . '/../includes/header.php';
         <a href="products.php" class="btn">Produkte verwalten</a>
         <a href="reservations.php" class="btn">Reservierungen</a>
         <a href="backorders.php" class="btn">Vorbestellungen</a>
+        <a href="admins.php" class="btn">Admins verwalten</a>
         <a href="login.php?logout=1" class="btn btn-secondary">Abmelden</a>
     </div>
 </div>

+ 4 - 2
admin/login.php

@@ -5,6 +5,7 @@ require_once __DIR__ . '/../includes/functions.php';
 // Handle logout
 if (isset($_GET['logout'])) {
     $_SESSION['admin_logged_in'] = false;
+    unset($_SESSION['admin_username']);
     session_destroy();
     header('Location: login.php');
     exit;
@@ -13,12 +14,13 @@ if (isset($_GET['logout'])) {
 $error = '';
 
 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    $username = sanitize($_POST['username'] ?? '');
+    $username = normalizeAdminUsername($_POST['username'] ?? '');
     $password = $_POST['password'] ?? '';
     
-    $users = defined('ADMIN_USERS') ? ADMIN_USERS : [];
+    $users = getAdminUsers();
     if (isset($users[$username]) && password_verify($password, $users[$username])) {
         $_SESSION['admin_logged_in'] = true;
+        $_SESSION['admin_username'] = $username;
         header('Location: index.php');
         exit;
     } else {

+ 6 - 1
config.sample.php

@@ -25,6 +25,10 @@ define('DISCLAIMER_LINES', [
 // 1) Create a new hash for the password (see commands above).
 // 2) Add a new entry to ADMIN_USERS: 'username' => 'hash'
 //
+// Note:
+// Runtime login source of truth is data/admins.json.
+// ADMIN_USERS is kept only as optional legacy reference.
+//
 // Example:
 // 'max' => '$2y$10$your_hash_here'
 //
@@ -44,7 +48,7 @@ define('ORDER_HISTORY_MAX_IDS', 10);
 define('ORDER_HISTORY_COOKIE_SECRET', 'change-this-order-history-secret'); // Change this to a long random secret
 
 // Email settings
-define('ADMIN_EMAIL', 'inbox@medowar.de'); // Change to your admin email
+define('ADMIN_EMAIL', 'inbox@medowar.de'); // Fallback recipient if no admin account emails are configured
 define('FROM_EMAIL', 'shop@med0.de'); // Change to your sender email
 define('FROM_NAME', SITE_NAME);
 
@@ -52,6 +56,7 @@ define('FROM_NAME', SITE_NAME);
 define('DATA_DIR', __DIR__ . '/data/');
 define('PRODUCTS_FILE', DATA_DIR . 'products.json');
 define('RESERVATIONS_FILE', DATA_DIR . 'reservations.json');
+define('ADMINS_FILE', DATA_DIR . 'admins.json');
 
 // Session settings
 if (session_status() === PHP_SESSION_NONE) {

+ 12 - 0
data/admins.json

@@ -0,0 +1,12 @@
+{
+    "admins": {
+        "bernd": {
+            "password_hash": "$2y$12$0ngn99MgyXnGTEPf9\/6Dz.g7vePaWOAXo7gYnnw.9XkjOKdepuzm2",
+            "description": "Bernd Risch"
+        },
+        "josef": {
+            "password_hash": "$2y$12$j25hQFkmw6oJVHNujpXe5.FILCGZNwQvH5MelcamhEjGuQO9aGJ66",
+            "description": "Josef Straßl"
+        }
+    }
+}

+ 59 - 0
docs/ADMIN_SYSTEM.md

@@ -0,0 +1,59 @@
+# Admin System
+
+## Überblick
+
+Das Admin-System nutzt einen klassischen Session-Login für den Bereich unter `admin/`.
+
+- Login-Seite: `admin/login.php`
+- Admin-Dashboard: `admin/index.php`
+- Admin-Verwaltung: `admin/admins.php`
+- Backend-Helfer: `includes/functions.php`
+- Persistenz: `data/admins.json`
+
+## Datenmodell (`data/admins.json`)
+
+Format:
+
+```json
+{
+  "admins": {
+    "username": {
+      "password_hash": "$2y$...",
+      "description": "Rolle/Beschreibung",
+      "email": "admin@example.org"
+    }
+  }
+}
+```
+
+Hinweise:
+
+- `password_hash` muss ein gültiger Hash für `password_verify()` sein.
+- `description` ist Pflicht (max. 120 Zeichen).
+- `email` ist Pflicht und wird für neue Bestell-Benachrichtigungen verwendet.
+- Ältere Einträge im Legacy-Format (`"username": "$2y$..."`) werden weiterhin gelesen.
+- Es gibt **keine automatische Migration** mehr beim Lesen.
+
+## Rechte
+
+- Jeder eingeloggte Admin darf:
+  - neue Admins anlegen,
+  - Passwörter anderer Admins ändern,
+  - andere Admins löschen,
+  - Beschreibungen bearbeiten.
+
+Diese Regeln werden serverseitig geprüft.
+
+## Bedienung in `admin/admins.php`
+
+- Tabelle zeigt Benutzername + Beschreibung + E-Mail.
+- Bearbeitung von Beschreibung + E-Mail erfolgt über den Button `Profil ändern` und ein separates Formular.
+- Passwortänderung erfolgt über den Button `Passwort ändern` und ein separates Formular.
+
+## Wichtige Betriebsdetails
+
+- Session-Status:
+  - Login setzt `$_SESSION['admin_logged_in'] = true`
+  - Login setzt zusätzlich `$_SESSION['admin_username']`
+- Logout löscht beide Werte und beendet die Session.
+- Das System schreibt `admins.json` nur bei expliziten Admin-Aktionen (anlegen, ändern, löschen), nicht während reiner Lesevorgänge.

+ 265 - 2
includes/functions.php

@@ -27,6 +27,218 @@ function writeJsonFile($file, $data) {
     file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
 }
 
+/**
+ * Normalize admin username input.
+ */
+function normalizeAdminUsername($username) {
+    return trim((string)$username);
+}
+
+/**
+ * Normalize admin description input.
+ */
+function normalizeAdminDescription($description) {
+    return trim((string)$description);
+}
+
+/**
+ * Normalize admin email input.
+ */
+function normalizeAdminEmail($email) {
+    return strtolower(trim((string)$email));
+}
+
+/**
+ * Validate admin username format.
+ */
+function isValidAdminUsername($username) {
+    $username = normalizeAdminUsername($username);
+    return preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{2,49}$/', $username) === 1;
+}
+
+/**
+ * Validate admin description.
+ */
+function isValidAdminDescription($description) {
+    $description = normalizeAdminDescription($description);
+    if ($description === '') {
+        return false;
+    }
+
+    $length = function_exists('mb_strlen') ? mb_strlen($description) : strlen($description);
+    return $length <= 120;
+}
+
+/**
+ * Validate admin email.
+ */
+function isValidAdminEmail($email) {
+    $email = normalizeAdminEmail($email);
+    return $email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
+}
+
+/**
+ * Get default description for an admin account.
+ */
+function getDefaultAdminDescription($username) {
+    return 'Admin';
+}
+
+/**
+ * Get fallback admin email address from config.
+ */
+function getDefaultAdminEmail() {
+    if (!defined('ADMIN_EMAIL') || !is_string(ADMIN_EMAIL)) {
+        return '';
+    }
+
+    $fallbackEmail = normalizeAdminEmail(ADMIN_EMAIL);
+    if (!isValidAdminEmail($fallbackEmail)) {
+        return '';
+    }
+
+    return $fallbackEmail;
+}
+
+/**
+ * Get full admin account records from JSON store.
+ */
+function getAdminAccounts() {
+    $data = readJsonFile(ADMINS_FILE);
+    $accounts = [];
+
+    if (isset($data['admins']) && is_array($data['admins'])) {
+        foreach ($data['admins'] as $username => $record) {
+            $normalizedUsername = normalizeAdminUsername($username);
+            if ($normalizedUsername === '') {
+                continue;
+            }
+
+            $hash = '';
+            $description = '';
+            $email = getDefaultAdminEmail();
+            if (is_string($record)) {
+                $hash = $record;
+                $description = getDefaultAdminDescription($normalizedUsername);
+            } elseif (is_array($record)) {
+                $hash = isset($record['password_hash']) && is_string($record['password_hash']) ? $record['password_hash'] : '';
+                $description = isset($record['description']) ? normalizeAdminDescription($record['description']) : getDefaultAdminDescription($normalizedUsername);
+                $email = isset($record['email']) ? normalizeAdminEmail($record['email']) : getDefaultAdminEmail();
+            }
+
+            if ($hash === '') {
+                continue;
+            }
+            if (!isValidAdminDescription($description)) {
+                $description = getDefaultAdminDescription($normalizedUsername);
+            }
+            if (!isValidAdminEmail($email)) {
+                $email = getDefaultAdminEmail();
+            }
+
+            $accounts[$normalizedUsername] = [
+                'password_hash' => $hash,
+                'description' => $description,
+                'email' => $email
+            ];
+        }
+    }
+
+    return $accounts;
+}
+
+/**
+ * Get admin users for authentication (username => password hash).
+ */
+function getAdminUsers() {
+    $accounts = getAdminAccounts();
+    $admins = [];
+
+    foreach ($accounts as $username => $account) {
+        if (!isset($account['password_hash']) || !is_string($account['password_hash']) || $account['password_hash'] === '') {
+            continue;
+        }
+        $admins[$username] = $account['password_hash'];
+    }
+
+    return $admins;
+}
+
+/**
+ * Save full admin accounts to JSON store.
+ */
+function saveAdminAccounts($accounts) {
+    $sanitizedAccounts = [];
+
+    foreach ($accounts as $username => $account) {
+        $normalizedUsername = normalizeAdminUsername($username);
+        if ($normalizedUsername === '' || !is_array($account)) {
+            continue;
+        }
+
+        $hash = isset($account['password_hash']) && is_string($account['password_hash']) ? $account['password_hash'] : '';
+        if ($hash === '') {
+            continue;
+        }
+
+        $description = isset($account['description']) ? normalizeAdminDescription($account['description']) : getDefaultAdminDescription($normalizedUsername);
+        if (!isValidAdminDescription($description)) {
+            $description = getDefaultAdminDescription($normalizedUsername);
+        }
+
+        $email = isset($account['email']) ? normalizeAdminEmail($account['email']) : getDefaultAdminEmail();
+        if (!isValidAdminEmail($email)) {
+            $email = getDefaultAdminEmail();
+        }
+
+        $sanitizedAccounts[$normalizedUsername] = [
+            'password_hash' => $hash,
+            'description' => $description,
+            'email' => $email
+        ];
+    }
+
+    ksort($sanitizedAccounts);
+    writeJsonFile(ADMINS_FILE, ['admins' => $sanitizedAccounts]);
+}
+
+/**
+ * Save admin users to JSON store (username => password hash).
+ */
+function saveAdminUsers($admins) {
+    $existingAccounts = getAdminAccounts();
+    $normalizedAccounts = [];
+
+    foreach ($admins as $username => $hash) {
+        $normalizedUsername = normalizeAdminUsername($username);
+        if ($normalizedUsername === '' || !is_string($hash) || $hash === '') {
+            continue;
+        }
+
+        $description = isset($existingAccounts[$normalizedUsername]['description'])
+            ? normalizeAdminDescription($existingAccounts[$normalizedUsername]['description'])
+            : getDefaultAdminDescription($normalizedUsername);
+        if (!isValidAdminDescription($description)) {
+            $description = getDefaultAdminDescription($normalizedUsername);
+        }
+
+        $email = isset($existingAccounts[$normalizedUsername]['email'])
+            ? normalizeAdminEmail($existingAccounts[$normalizedUsername]['email'])
+            : getDefaultAdminEmail();
+        if (!isValidAdminEmail($email)) {
+            $email = getDefaultAdminEmail();
+        }
+
+        $normalizedAccounts[$normalizedUsername] = [
+            'password_hash' => $hash,
+            'description' => $description,
+            'email' => $email
+        ];
+    }
+
+    saveAdminAccounts($normalizedAccounts);
+}
+
 /**
  * Get all products
  */
@@ -752,6 +964,57 @@ function sendEmail($to, $subject, $message, $isHtml = true) {
     return mail($to, $subject, $message, implode("\r\n", $headers));
 }
 
+/**
+ * Get all admin notification recipients from admin accounts.
+ * Falls back to ADMIN_EMAIL if no account email is configured.
+ */
+function getAdminNotificationEmails() {
+    $accounts = getAdminAccounts();
+    $emails = [];
+
+    foreach ($accounts as $account) {
+        if (!isset($account['email'])) {
+            continue;
+        }
+
+        $email = normalizeAdminEmail($account['email']);
+        if (!isValidAdminEmail($email)) {
+            continue;
+        }
+        $emails[] = $email;
+    }
+
+    if (empty($emails)) {
+        $fallbackEmail = getDefaultAdminEmail();
+        if ($fallbackEmail !== '') {
+            $emails[] = $fallbackEmail;
+        }
+    }
+
+    return array_values(array_unique($emails));
+}
+
+/**
+ * Send admin notifications to all configured recipients.
+ */
+function sendAdminNotificationEmails($subject, $message, $isHtml = true) {
+    $emails = getAdminNotificationEmails();
+
+    if (empty($emails)) {
+        return false;
+    }
+
+    $sent = false;
+    foreach ($emails as $email) {
+        $result = sendEmail($email, $subject, $message, $isHtml);
+        if ($result) {
+            $sent = true;
+        }
+    }
+
+    return $sent;
+}
+
 /**
  * Send reservation confirmation emails
  */
@@ -839,7 +1102,7 @@ function sendReservationEmails($reservation) {
     </body>
     </html>';
     
-    sendEmail(ADMIN_EMAIL, $adminSubject, $adminMessage);
+    sendAdminNotificationEmails($adminSubject, $adminMessage);
 }
 
 /**
@@ -929,7 +1192,7 @@ function sendBackorderEmails($reservation) {
     </body>
     </html>';
     
-    sendEmail(ADMIN_EMAIL, $adminSubject, $adminMessage);
+    sendAdminNotificationEmails($adminSubject, $adminMessage);
 }
 
 /**