diff --git a/assets/components/navbar.php b/assets/components/navbar.php index 55500518..0de79ee8 100644 --- a/assets/components/navbar.php +++ b/assets/components/navbar.php @@ -102,6 +102,7 @@
  • Dashboard
  • +
  • Konfiguration
  • Updater
  • @@ -171,7 +172,8 @@ $iconClass = [ 'antrag' => 'fa-file', 'protokoll' => 'fa-truck-medical', - 'dokument' => 'fa-folder-open' + 'dokument' => 'fa-folder-open', + 'system' => 'fa-gears' ]; $icon = $iconClass[$notification['type']] ?? 'fa-bell'; ?> diff --git a/assets/config/config.php b/assets/config/config.php index 89c77bac..8391bcaa 100644 --- a/assets/config/config.php +++ b/assets/config/config.php @@ -1,6 +1,7 @@ loadAndDefineConfig(); +} catch (Exception $e) { + // Fallback to default values if database is not available or table doesn't exist + error_log("Could not load config from database: " . $e->getMessage()); + + // BASIS DATEN - Fallback defaults + if (!defined('API_KEY')) define('API_KEY', 'CHANGE_ME'); + if (!defined('SYSTEM_NAME')) define('SYSTEM_NAME', 'intraRP'); + if (!defined('SYSTEM_COLOR')) define('SYSTEM_COLOR', '#d10000'); + if (!defined('SYSTEM_URL')) define('SYSTEM_URL', 'CHANGE_ME'); + if (!defined('SYSTEM_LOGO')) define('SYSTEM_LOGO', '/assets/img/defaultLogo.webp'); + if (!defined('META_IMAGE_URL')) define('META_IMAGE_URL', ''); + + // SERVER DATEN + if (!defined('SERVER_NAME')) define('SERVER_NAME', 'CHANGE_ME'); + if (!defined('SERVER_CITY')) define('SERVER_CITY', 'Musterstadt'); + + // RP DATEN + if (!defined('RP_ORGTYPE')) define('RP_ORGTYPE', 'Berufsfeuerwehr'); + if (!defined('RP_STREET')) define('RP_STREET', 'Musterweg 0815'); + if (!defined('RP_ZIP')) define('RP_ZIP', '1337'); + + // FUNKTIONEN + if (!defined('CHAR_ID')) define('CHAR_ID', true); + if (!defined('ENOTF_PREREG')) define('ENOTF_PREREG', true); + if (!defined('ENOTF_USE_PIN')) define('ENOTF_USE_PIN', true); + if (!defined('ENOTF_PIN')) define('ENOTF_PIN', '1234'); + if (!defined('ENOTF_REQUIRE_USER_AUTH')) define('ENOTF_REQUIRE_USER_AUTH', false); + if (!defined('REGISTRATION_MODE')) define('REGISTRATION_MODE', 'open'); + if (!defined('BASE_PATH')) define('BASE_PATH', '/'); +} \ No newline at end of file diff --git a/assets/database/alter_intra_notifications_type_04112025.php b/assets/database/alter_intra_notifications_type_04112025.php new file mode 100644 index 00000000..fb6ea544 --- /dev/null +++ b/assets/database/alter_intra_notifications_type_04112025.php @@ -0,0 +1,12 @@ +exec($sql); +} catch (PDOException $e) { + $message = $e->getMessage(); + echo $message; +} diff --git a/assets/database/create_intra_config_04112025.php b/assets/database/create_intra_config_04112025.php new file mode 100644 index 00000000..60776ba5 --- /dev/null +++ b/assets/database/create_intra_config_04112025.php @@ -0,0 +1,27 @@ +exec($sql); +} catch (PDOException $e) { + $message = $e->getMessage(); + echo $message; +} diff --git a/assets/database/insert_intra_config_defaults_04112025.php b/assets/database/insert_intra_config_defaults_04112025.php new file mode 100644 index 00000000..b867b318 --- /dev/null +++ b/assets/database/insert_intra_config_defaults_04112025.php @@ -0,0 +1,57 @@ + 'API_KEY', 'value' => 'CHANGE_ME', 'type' => 'string', 'category' => 'basis', 'description' => 'API-Schlüssel für externe Schnittstellen', 'editable' => 0, 'order' => 1], + ['key' => 'SYSTEM_NAME', 'value' => 'intraRP', 'type' => 'string', 'category' => 'basis', 'description' => 'Eigenname des Intranets', 'editable' => 1, 'order' => 2], + ['key' => 'SYSTEM_COLOR', 'value' => '#d10000', 'type' => 'color', 'category' => 'basis', 'description' => 'Hauptfarbe des Systems', 'editable' => 1, 'order' => 3], + ['key' => 'SYSTEM_URL', 'value' => 'CHANGE_ME', 'type' => 'url', 'category' => 'basis', 'description' => 'Domain des Systems', 'editable' => 1, 'order' => 4], + ['key' => 'SYSTEM_LOGO', 'value' => '/assets/img/defaultLogo.webp', 'type' => 'url', 'category' => 'basis', 'description' => 'Ort des Logos (relativer Pfad oder Link)', 'editable' => 1, 'order' => 5], + ['key' => 'META_IMAGE_URL', 'value' => '', 'type' => 'url', 'category' => 'basis', 'description' => 'Bild für Link-Vorschau (als Link angeben)', 'editable' => 1, 'order' => 6], + + // SERVER DATEN + ['key' => 'SERVER_NAME', 'value' => 'CHANGE_ME', 'type' => 'string', 'category' => 'server', 'description' => 'Name des Servers', 'editable' => 1, 'order' => 10], + ['key' => 'SERVER_CITY', 'value' => 'Musterstadt', 'type' => 'string', 'category' => 'server', 'description' => 'Name der Stadt in welcher der Server spielt', 'editable' => 1, 'order' => 11], + + // RP DATEN + ['key' => 'RP_ORGTYPE', 'value' => 'Berufsfeuerwehr', 'type' => 'string', 'category' => 'rp', 'description' => 'Art/Name der Organisation', 'editable' => 1, 'order' => 20], + ['key' => 'RP_STREET', 'value' => 'Musterweg 0815', 'type' => 'string', 'category' => 'rp', 'description' => 'Straße der Organisation', 'editable' => 1, 'order' => 21], + ['key' => 'RP_ZIP', 'value' => '1337', 'type' => 'string', 'category' => 'rp', 'description' => 'PLZ der Organisation', 'editable' => 1, 'order' => 22], + + // FUNKTIONEN + ['key' => 'CHAR_ID', 'value' => 'true', 'type' => 'boolean', 'category' => 'funktionen', 'description' => 'Wird eine eindeutige Charakter-ID verwendet?', 'editable' => 1, 'order' => 30], + ['key' => 'ENOTF_PREREG', 'value' => 'true', 'type' => 'boolean', 'category' => 'funktionen', 'description' => 'Wird das Voranmeldungssystem des eNOTF verwendet?', 'editable' => 1, 'order' => 31], + ['key' => 'ENOTF_USE_PIN', 'value' => 'true', 'type' => 'boolean', 'category' => 'funktionen', 'description' => 'Wird die PIN-Funktion des eNOTF verwendet?', 'editable' => 1, 'order' => 32], + ['key' => 'ENOTF_PIN', 'value' => '1234', 'type' => 'string', 'category' => 'funktionen', 'description' => 'PIN für den Zugang zum eNOTF (4-6 Zahlen)', 'editable' => 1, 'order' => 33], + ['key' => 'ENOTF_REQUIRE_USER_AUTH', 'value' => 'false', 'type' => 'boolean', 'category' => 'funktionen', 'description' => 'Wird eine Registrierung/Anmeldung im Hauptsystem für den Zugang zum eNOTF vorausgesetzt?', 'editable' => 1, 'order' => 34], + ['key' => 'REGISTRATION_MODE', 'value' => 'open', 'type' => 'string', 'category' => 'funktionen', 'description' => 'Registrierungsmodus: open = für jeden möglich, code = nur mit Code, closed = keine Registrierung', 'editable' => 1, 'order' => 35], + ['key' => 'BASE_PATH', 'value' => '/', 'type' => 'string', 'category' => 'funktionen', 'description' => 'Basis-Pfad des Systems (z.B. /intraRP/)', 'editable' => 1, 'order' => 36], + ]; + + $stmt = $pdo->prepare(" + INSERT INTO intra_config (config_key, config_value, config_type, category, description, is_editable, display_order) + VALUES (:key, :value, :type, :category, :description, :editable, :order) + ON DUPLICATE KEY UPDATE + config_type = VALUES(config_type), + category = VALUES(category), + description = VALUES(description), + is_editable = VALUES(is_editable), + display_order = VALUES(display_order) + "); + + foreach ($configs as $config) { + $stmt->execute([ + 'key' => $config['key'], + 'value' => $config['value'], + 'type' => $config['type'], + 'category' => $config['category'], + 'description' => $config['description'], + 'editable' => $config['editable'], + 'order' => $config['order'] + ]); + } +} catch (PDOException $e) { + $message = $e->getMessage(); + echo $message; +} diff --git a/assets/database/remove_lang_config_04112025.php b/assets/database/remove_lang_config_04112025.php new file mode 100644 index 00000000..1b9f979c --- /dev/null +++ b/assets/database/remove_lang_config_04112025.php @@ -0,0 +1,12 @@ +exec($sql); +} catch (PDOException $e) { + $message = $e->getMessage(); + echo $message; +} diff --git a/assets/functions/documents/create-custom.php b/assets/functions/documents/create-custom.php index 63ccf6a9..ee91f676 100644 --- a/assets/functions/documents/create-custom.php +++ b/assets/functions/documents/create-custom.php @@ -15,6 +15,7 @@ use App\Auth\Permissions; use App\Notifications\NotificationManager; use App\Personnel\PersonalLogManager; +use App\Utils\AuditLogger; ob_clean(); @@ -104,14 +105,18 @@ ); // Logge die Aktion im Audit-Log - logAction($pdo, 'document_created', [ - 'db_id' => $dbId, - 'document_id' => $documentId, - 'template_id' => $input['template_id'], - 'template_name' => $template['name'], - 'profileid' => $input['profileid'], - 'created_by' => $_SESSION['discordtag'] ?? null - ]); + try { + $auditLogger = new AuditLogger($pdo); + $auditLogger->log( + (int)($_SESSION['userid']), + 'Dokument erstellt [' . $documentId . ']', + 'Für Profil: ' . $input['profileid'], + 'Dokumente', + 0 + ); + } catch (Exception $e) { + error_log('Audit log failed: ' . $e->getMessage()); + } // Create notification for employee if they have a user account try { @@ -162,22 +167,3 @@ ob_end_flush(); exit; } - -function logAction($pdo, $action, $data) -{ - try { - $stmt = $pdo->prepare(" - INSERT INTO intra_audit_log - (user, action, details, timestamp) - VALUES (:user, :action, :details, CURRENT_TIMESTAMP) - "); - - $stmt->execute([ - 'user' => $_SESSION['userid'] ?? 0, - 'action' => $action, - 'details' => json_encode($data) - ]); - } catch (Exception $e) { - error_log("Audit Log Fehler: " . $e->getMessage()); - } -} diff --git a/auth/callback.php b/auth/callback.php index f13800e2..73270d49 100644 --- a/auth/callback.php +++ b/auth/callback.php @@ -5,6 +5,7 @@ use League\OAuth2\Client\Provider\GenericProvider; use App\Helpers\ProtocolDetection; +use App\Notifications\NotificationManager; ini_set('display_errors', 1); ini_set('display_startup_errors', 1); @@ -114,6 +115,22 @@ 'role' => $adminRole['id'], 'full_admin' => 1 ]); + + $firstUserId = $pdo->lastInsertId(); + + // Send notification to first user about configuration + try { + $notificationManager = new NotificationManager($pdo); + $notificationManager->create( + $firstUserId, + 'system', + 'Willkommen bei intraRP!', + 'Als erster Benutzer haben Sie Administratorrechte. Bitte besuchen Sie die System-Konfiguration, um wichtige Einstellungen wie den Systemnamen, Logo und weitere Optionen anzupassen.', + BASE_PATH . 'settings/system/config.php' + ); + } catch (Exception $e) { + error_log("Failed to create first user notification: " . $e->getMessage()); + } } $stmt = $pdo->prepare("SELECT * FROM intra_users WHERE discord_id = :discord_id"); diff --git a/benachrichtigungen/index.php b/benachrichtigungen/index.php index d5fa1c5a..fb852e9a 100644 --- a/benachrichtigungen/index.php +++ b/benachrichtigungen/index.php @@ -178,7 +178,8 @@ $iconClass = [ 'antrag' => 'fa-file', 'protokoll' => 'fa-truck-medical', - 'dokument' => 'fa-folder-open' + 'dokument' => 'fa-folder-open', + 'system' => 'fa-gears' ]; $icon = $iconClass[$notification['type']] ?? 'fa-bell'; ?> diff --git a/settings/system/config.php b/settings/system/config.php new file mode 100644 index 00000000..57e5762f --- /dev/null +++ b/settings/system/config.php @@ -0,0 +1,320 @@ +getAllConfig(); + $configTypes = []; + foreach ($allConfigs as $config) { + $configTypes[$config['config_key']] = $config['config_type']; + } + + // Process POST data + foreach ($allConfigs as $config) { + if (!$config['is_editable']) continue; + + $key = $config['config_key']; + + // Get the raw database value (string) instead of converted value + $oldValue = $config['config_value']; + + // Handle different input types + if ($config['config_type'] === 'boolean') { + // Checkboxes/switches send 'on' when checked, nothing when unchecked + $value = isset($_POST[$key]) && $_POST[$key] === 'on' ? 'true' : 'false'; + } else { + // Skip if not in POST + if (!isset($_POST[$key])) continue; + $value = $_POST[$key]; + } + + // Only update if value changed (strict comparison for type safety) + if ($oldValue !== $value) { + $updates[$key] = $value; + $changes[] = [ + 'key' => $key, + 'old' => $oldValue, + 'new' => $value + ]; + } + } + + if (!empty($updates)) { + $result = $configManager->updateMultiple($updates, $_SESSION['userid']); + + if ($result['success']) { + // Log each change in audit log + foreach ($changes as $change) { + $auditLogger->log( + $_SESSION['userid'], + 'Config ' . $change['key'] . ' bearbeitet', + 'Altere Wert: ' . $change['old'] . ', Neuer Wert: ' . $change['new'], + 'System', + 1 // Config updates are global + ); + } + + Flash::set('success', 'Konfiguration erfolgreich aktualisiert.'); + } else { + Flash::set('error', 'Fehler beim Aktualisieren der Konfiguration.'); + } + } else { + Flash::set('info', 'Keine Änderungen vorgenommen.'); + } + + header("Location: " . $_SERVER['PHP_SELF']); + exit(); +} + +$configByCategory = $configManager->getConfigByCategory(); +?> + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +

    System-Konfiguration

    +
    + + +
    + $configs): ?> +
    +
    +
    +
    getCategoryDisplayName($category)) ?>
    +
    +
    + + +
    + + + +
    + + > +
    + + +
    + + +
    +
    Wählen Sie eine Farbe aus oder geben Sie einen Hex-Farbcode ein.
    + + + +
    Relativer Pfad oder vollständige URL zum Logo.
    +
    + Vorschau:
    + Logo Preview +
    + + + +
    Vollständige URL zum Bild für Link-Vorschau.
    +
    + Vorschau:
    + Meta Image Preview +
    + + + +
    + + + + +
    + + +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    + + + + + + diff --git a/setup/database-init.php b/setup/database-init.php index efe5fae1..59b78181 100644 --- a/setup/database-init.php +++ b/setup/database-init.php @@ -220,6 +220,10 @@ function isTransactionActive(PDO $pdo): bool // 04.11.2025 ['file' => 'alter_intra_mitarbeiter_log_04112025.php', 'type' => 'alter'], + ['file' => 'create_intra_config_04112025.php', 'type' => 'create'], + ['file' => 'insert_intra_config_defaults_04112025.php', 'type' => 'insert'], + ['file' => 'alter_intra_notifications_type_04112025.php', 'type' => 'alter'], + ['file' => 'remove_lang_config_04112025.php', 'type' => 'alter'], ]; $executed = 0; diff --git a/src/Config/ConfigManager.php b/src/Config/ConfigManager.php new file mode 100644 index 00000000..42dac174 --- /dev/null +++ b/src/Config/ConfigManager.php @@ -0,0 +1,229 @@ +pdo = $pdo; + } + + /** + * Load all configuration values from database and define them as constants + * This maintains backward compatibility with existing code using define() + */ + public function loadAndDefineConfig(): void + { + $configs = $this->getAllConfig(); + + foreach ($configs as $config) { + $key = $config['config_key']; + $value = $config['config_value']; + $type = $config['config_type']; + + // Convert value based on type + $definedValue = $this->convertValue($value, $type); + + // Define constant if not already defined + if (!defined($key)) { + define($key, $definedValue); + } + } + } + + /** + * Get all configuration values from database + * + * @return array Array of configuration records + */ + public function getAllConfig(): array + { + // Use cache if available + if (self::$configCache !== null) { + return self::$configCache; + } + + try { + $stmt = $this->pdo->query(" + SELECT * FROM intra_config + ORDER BY display_order ASC, config_key ASC + "); + self::$configCache = $stmt->fetchAll(PDO::FETCH_ASSOC); + return self::$configCache; + } catch (PDOException $e) { + error_log("Failed to load config: " . $e->getMessage()); + return []; + } + } + + /** + * Get configuration values grouped by category + * + * @return array Array grouped by category + */ + public function getConfigByCategory(): array + { + $configs = $this->getAllConfig(); + $grouped = []; + + foreach ($configs as $config) { + $category = $config['category']; + if (!isset($grouped[$category])) { + $grouped[$category] = []; + } + $grouped[$category][] = $config; + } + + return $grouped; + } + + /** + * Get a single configuration value + * + * @param string $key Configuration key + * @return mixed|null Configuration value or null if not found + */ + public function get(string $key) + { + try { + $stmt = $this->pdo->prepare(" + SELECT config_value, config_type FROM intra_config + WHERE config_key = ? + "); + $stmt->execute([$key]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($result) { + return $this->convertValue($result['config_value'], $result['config_type']); + } + + return null; + } catch (PDOException $e) { + error_log("Failed to get config value: " . $e->getMessage()); + return null; + } + } + + /** + * Update a configuration value + * + * @param string $key Configuration key + * @param mixed $value New value + * @param int|null $userId User ID making the change + * @return bool Success status + */ + public function update(string $key, $value, ?int $userId = null): bool + { + try { + // Clear cache + self::$configCache = null; + + $stmt = $this->pdo->prepare(" + UPDATE intra_config + SET config_value = ?, updated_by = ?, updated_at = NOW() + WHERE config_key = ? AND is_editable = 1 + "); + + return $stmt->execute([$value, $userId, $key]); + } catch (PDOException $e) { + error_log("Failed to update config value: " . $e->getMessage()); + return false; + } + } + + /** + * Update multiple configuration values at once + * + * @param array $updates Array of key => value pairs + * @param int|null $userId User ID making the changes + * @return array Array with success status and list of failed keys + */ + public function updateMultiple(array $updates, ?int $userId = null): array + { + $failed = []; + $updated = []; + + $this->pdo->beginTransaction(); + + try { + $stmt = $this->pdo->prepare(" + UPDATE intra_config + SET config_value = ?, updated_by = ?, updated_at = NOW() + WHERE config_key = ? AND is_editable = 1 + "); + + foreach ($updates as $key => $value) { + if ($stmt->execute([$value, $userId, $key])) { + $updated[] = $key; + } else { + $failed[] = $key; + } + } + + if (empty($failed)) { + $this->pdo->commit(); + // Clear cache after successful commit + self::$configCache = null; + return ['success' => true, 'updated' => $updated, 'failed' => []]; + } else { + $this->pdo->rollBack(); + return ['success' => false, 'updated' => [], 'failed' => array_keys($updates)]; + } + } catch (PDOException $e) { + $this->pdo->rollBack(); + error_log("Failed to update multiple config values: " . $e->getMessage()); + return ['success' => false, 'updated' => [], 'failed' => array_keys($updates)]; + } + } + + /** + * Convert string value to appropriate type + * + * @param string $value String value from database + * @param string $type Type specification + * @return mixed Converted value + */ + private function convertValue(?string $value, string $type) + { + if ($value === null) { + return null; + } + + switch ($type) { + case 'boolean': + return $value === 'true' || $value === '1' || $value === 'yes'; + case 'integer': + return (int)$value; + case 'color': + case 'url': + case 'string': + default: + return $value; + } + } + + /** + * Get category display name + * + * @param string $category Category key + * @return string Display name + */ + public function getCategoryDisplayName(string $category): string + { + $names = [ + 'basis' => 'Basis Daten', + 'server' => 'Server Daten', + 'rp' => 'RP Daten', + 'funktionen' => 'Funktionen', + ]; + + return $names[$category] ?? ucfirst($category); + } +} diff --git a/src/Notifications/NotificationManager.php b/src/Notifications/NotificationManager.php index 875ea5f9..5c41f502 100644 --- a/src/Notifications/NotificationManager.php +++ b/src/Notifications/NotificationManager.php @@ -17,7 +17,7 @@ public function __construct(PDO $pdo) * Create a new notification for a user * * @param int $userId User ID to notify - * @param string $type Type of notification (antrag, protokoll, dokument) + * @param string $type Type of notification (antrag, protokoll, dokument, system) * @param string $title Notification title * @param string|null $message Optional notification message * @param string|null $link Optional link to related item @@ -26,7 +26,7 @@ public function __construct(PDO $pdo) public function create(int $userId, string $type, string $title, ?string $message = null, ?string $link = null): bool { // Validate notification type - $validTypes = ['antrag', 'protokoll', 'dokument']; + $validTypes = ['antrag', 'protokoll', 'dokument', 'system']; if (!in_array($type, $validTypes)) { error_log("Invalid notification type: {$type}"); return false;