Add Multi-Tenant SaaS foundation for customer management
Phase 1 implementation includes: Database: - schema.sql with tables for tenants, domains, settings, branding, streams, users, subscriptions, plans, invoices, viewer_stats Core Classes (src/Core/): - Database.php: PDO wrapper with singleton pattern - TenantResolver.php: Domain-to-tenant resolution with fallback Tenant Classes (src/Tenant/): - TenantManager.php: CRUD operations for tenants - TenantSettingsManager.php: DB-based settings per tenant Configuration: - config.example.php: Template for database/stripe/mail config - bootstrap.php: Initializes multi-tenant environment - .gitignore: Excludes config.php and cache files Integration: - SettingsManager.php: Added saas_features toggles (all off by default) - index.php: Uses getSiteConfig() from bootstrap when multi-tenant enabled, falls back to legacy hardcoded domains when disabled All SaaS features are disabled by default (saas_features.multi_tenant_enabled = false), ensuring zero breaking changes to existing installations.
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
/**
|
||||
* TenantManager - CRUD-Operationen für Tenants
|
||||
*/
|
||||
|
||||
namespace AuroraLivecam\Tenant;
|
||||
|
||||
use AuroraLivecam\Core\Database;
|
||||
use Exception;
|
||||
|
||||
class TenantManager
|
||||
{
|
||||
private Database $db;
|
||||
|
||||
public function __construct(?Database $db = null)
|
||||
{
|
||||
$this->db = $db ?? Database::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Tenant
|
||||
*/
|
||||
public function create(array $data): int
|
||||
{
|
||||
$this->db->beginTransaction();
|
||||
|
||||
try {
|
||||
// UUID generieren
|
||||
$uuid = $this->generateUuid();
|
||||
|
||||
// Slug generieren falls nicht vorhanden
|
||||
$slug = $data['slug'] ?? $this->generateSlug($data['name']);
|
||||
|
||||
// Tenant erstellen
|
||||
$tenantId = $this->db->insert('tenants', [
|
||||
'uuid' => $uuid,
|
||||
'name' => $data['name'],
|
||||
'slug' => $slug,
|
||||
'email' => $data['email'],
|
||||
'status' => $data['status'] ?? 'trial',
|
||||
'plan_id' => $data['plan_id'] ?? $this->getDefaultPlanId(),
|
||||
'trial_ends_at' => $data['trial_ends_at'] ?? $this->calculateTrialEnd(),
|
||||
]);
|
||||
|
||||
// Domain hinzufügen
|
||||
if (!empty($data['domain'])) {
|
||||
$this->addDomain($tenantId, $data['domain'], true);
|
||||
}
|
||||
|
||||
// Default-Subdomain erstellen
|
||||
if (!empty($data['subdomain'])) {
|
||||
$subdomain = $data['subdomain'] . '.aurora-livecam.com';
|
||||
$this->addDomain($tenantId, $subdomain, empty($data['domain']));
|
||||
}
|
||||
|
||||
// Branding mit Defaults initialisieren
|
||||
$this->db->insert('tenant_branding', [
|
||||
'tenant_id' => $tenantId,
|
||||
'site_name' => $data['name'],
|
||||
'site_name_full' => $data['name'],
|
||||
]);
|
||||
|
||||
// Onboarding initialisieren
|
||||
$this->db->insert('tenant_onboarding', [
|
||||
'tenant_id' => $tenantId,
|
||||
'current_step' => 1,
|
||||
]);
|
||||
|
||||
// Stream hinzufügen falls vorhanden
|
||||
if (!empty($data['stream_url'])) {
|
||||
$this->db->insert('tenant_streams', [
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => 'Main Stream',
|
||||
'stream_url' => $data['stream_url'],
|
||||
'stream_type' => $data['stream_type'] ?? 'hls',
|
||||
'is_primary' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
return $tenantId;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert einen Tenant
|
||||
*/
|
||||
public function update(int $tenantId, array $data): bool
|
||||
{
|
||||
$allowedFields = ['name', 'email', 'status', 'plan_id'];
|
||||
$updateData = array_intersect_key($data, array_flip($allowedFields));
|
||||
|
||||
if (empty($updateData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->db->update('tenants', $updateData, 'id = ?', [$tenantId]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht einen Tenant (Soft-Delete durch Status-Änderung)
|
||||
*/
|
||||
public function delete(int $tenantId): bool
|
||||
{
|
||||
return $this->db->update('tenants', ['status' => 'cancelled'], 'id = ?', [$tenantId]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard-Delete (wirklich löschen - Vorsicht!)
|
||||
*/
|
||||
public function hardDelete(int $tenantId): bool
|
||||
{
|
||||
return $this->db->delete('tenants', 'id = ?', [$tenantId]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen Tenant anhand der ID zurück
|
||||
*/
|
||||
public function getById(int $id): ?array
|
||||
{
|
||||
return $this->db->fetchOne(
|
||||
"SELECT t.*, p.name as plan_name, p.features as plan_features
|
||||
FROM tenants t
|
||||
LEFT JOIN plans p ON t.plan_id = p.id
|
||||
WHERE t.id = ?",
|
||||
[$id]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen Tenant anhand des Slugs zurück
|
||||
*/
|
||||
public function getBySlug(string $slug): ?array
|
||||
{
|
||||
return $this->db->fetchOne(
|
||||
"SELECT t.*, p.name as plan_name, p.features as plan_features
|
||||
FROM tenants t
|
||||
LEFT JOIN plans p ON t.plan_id = p.id
|
||||
WHERE t.slug = ?",
|
||||
[$slug]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen Tenant anhand der UUID zurück
|
||||
*/
|
||||
public function getByUuid(string $uuid): ?array
|
||||
{
|
||||
return $this->db->fetchOne(
|
||||
"SELECT t.*, p.name as plan_name, p.features as plan_features
|
||||
FROM tenants t
|
||||
LEFT JOIN plans p ON t.plan_id = p.id
|
||||
WHERE t.uuid = ?",
|
||||
[$uuid]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listet alle Tenants auf
|
||||
*/
|
||||
public function getAll(array $filters = []): array
|
||||
{
|
||||
$sql = "SELECT t.*, p.name as plan_name, p.features as plan_features
|
||||
FROM tenants t
|
||||
LEFT JOIN plans p ON t.plan_id = p.id
|
||||
WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if (!empty($filters['status'])) {
|
||||
$sql .= " AND t.status = ?";
|
||||
$params[] = $filters['status'];
|
||||
}
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
$sql .= " AND (t.name LIKE ? OR t.email LIKE ?)";
|
||||
$params[] = '%' . $filters['search'] . '%';
|
||||
$params[] = '%' . $filters['search'] . '%';
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY t.created_at DESC";
|
||||
|
||||
if (!empty($filters['limit'])) {
|
||||
$sql .= " LIMIT " . (int)$filters['limit'];
|
||||
if (!empty($filters['offset'])) {
|
||||
$sql .= " OFFSET " . (int)$filters['offset'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->db->fetchAll($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zählt Tenants
|
||||
*/
|
||||
public function count(array $filters = []): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*) FROM tenants WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if (!empty($filters['status'])) {
|
||||
$sql .= " AND status = ?";
|
||||
$params[] = $filters['status'];
|
||||
}
|
||||
|
||||
return (int) $this->db->fetchColumn($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine Domain zu einem Tenant hinzu
|
||||
*/
|
||||
public function addDomain(int $tenantId, string $domain, bool $isPrimary = false): int
|
||||
{
|
||||
// Normalisiere Domain
|
||||
$domain = strtolower(trim($domain));
|
||||
|
||||
// Prüfe ob Domain bereits existiert
|
||||
$existing = $this->db->fetchOne(
|
||||
"SELECT id FROM tenant_domains WHERE domain = ?",
|
||||
[$domain]
|
||||
);
|
||||
|
||||
if ($existing) {
|
||||
throw new Exception("Domain '$domain' is already in use");
|
||||
}
|
||||
|
||||
// Wenn primary, setze alle anderen auf non-primary
|
||||
if ($isPrimary) {
|
||||
$this->db->execute(
|
||||
"UPDATE tenant_domains SET is_primary = 0 WHERE tenant_id = ?",
|
||||
[$tenantId]
|
||||
);
|
||||
}
|
||||
|
||||
return $this->db->insert('tenant_domains', [
|
||||
'tenant_id' => $tenantId,
|
||||
'domain' => $domain,
|
||||
'is_primary' => $isPrimary ? 1 : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt eine Domain von einem Tenant
|
||||
*/
|
||||
public function removeDomain(int $tenantId, string $domain): bool
|
||||
{
|
||||
return $this->db->delete('tenant_domains', 'tenant_id = ? AND domain = ?', [$tenantId, $domain]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Domains eines Tenants zurück
|
||||
*/
|
||||
public function getDomains(int $tenantId): array
|
||||
{
|
||||
return $this->db->fetchAll(
|
||||
"SELECT * FROM tenant_domains WHERE tenant_id = ? ORDER BY is_primary DESC",
|
||||
[$tenantId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Branding eines Tenants
|
||||
*/
|
||||
public function updateBranding(int $tenantId, array $data): bool
|
||||
{
|
||||
$allowedFields = [
|
||||
'site_name', 'site_name_full', 'tagline', 'logo_path', 'favicon_path',
|
||||
'primary_color', 'secondary_color', 'accent_color',
|
||||
'welcome_text_de', 'welcome_text_en', 'footer_text',
|
||||
'custom_css', 'custom_js',
|
||||
'social_facebook', 'social_instagram', 'social_youtube'
|
||||
];
|
||||
|
||||
$updateData = array_intersect_key($data, array_flip($allowedFields));
|
||||
|
||||
if (empty($updateData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfe ob Branding existiert
|
||||
$exists = $this->db->fetchColumn(
|
||||
"SELECT tenant_id FROM tenant_branding WHERE tenant_id = ?",
|
||||
[$tenantId]
|
||||
);
|
||||
|
||||
if ($exists) {
|
||||
return $this->db->update('tenant_branding', $updateData, 'tenant_id = ?', [$tenantId]) > 0;
|
||||
} else {
|
||||
$updateData['tenant_id'] = $tenantId;
|
||||
return $this->db->insert('tenant_branding', $updateData) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das Branding eines Tenants zurück
|
||||
*/
|
||||
public function getBranding(int $tenantId): ?array
|
||||
{
|
||||
return $this->db->fetchOne(
|
||||
"SELECT * FROM tenant_branding WHERE tenant_id = ?",
|
||||
[$tenantId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Slug verfügbar ist
|
||||
*/
|
||||
public function isSlugAvailable(string $slug, ?int $excludeTenantId = null): bool
|
||||
{
|
||||
$sql = "SELECT id FROM tenants WHERE slug = ?";
|
||||
$params = [$slug];
|
||||
|
||||
if ($excludeTenantId) {
|
||||
$sql .= " AND id != ?";
|
||||
$params[] = $excludeTenantId;
|
||||
}
|
||||
|
||||
return $this->db->fetchOne($sql, $params) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Domain verfügbar ist
|
||||
*/
|
||||
public function isDomainAvailable(string $domain, ?int $excludeTenantId = null): bool
|
||||
{
|
||||
$sql = "SELECT td.id FROM tenant_domains td WHERE td.domain = ?";
|
||||
$params = [$domain];
|
||||
|
||||
if ($excludeTenantId) {
|
||||
$sql .= " AND td.tenant_id != ?";
|
||||
$params[] = $excludeTenantId;
|
||||
}
|
||||
|
||||
return $this->db->fetchOne($sql, $params) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen URL-sicheren Slug aus einem Namen
|
||||
*/
|
||||
private function generateSlug(string $name): string
|
||||
{
|
||||
$slug = strtolower($name);
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
|
||||
$slug = trim($slug, '-');
|
||||
|
||||
// Sicherstellen dass Slug einzigartig ist
|
||||
$baseSlug = $slug;
|
||||
$counter = 1;
|
||||
while (!$this->isSlugAvailable($slug)) {
|
||||
$slug = $baseSlug . '-' . $counter;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine UUID v4
|
||||
*/
|
||||
private function generateUuid(): string
|
||||
{
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet das Trial-Ende (14 Tage)
|
||||
*/
|
||||
private function calculateTrialEnd(): string
|
||||
{
|
||||
return date('Y-m-d H:i:s', strtotime('+14 days'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die ID des Default-Plans (Free) zurück
|
||||
*/
|
||||
private function getDefaultPlanId(): int
|
||||
{
|
||||
$plan = $this->db->fetchOne("SELECT id FROM plans WHERE slug = 'free' LIMIT 1");
|
||||
return $plan ? (int)$plan['id'] : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktiviert einen Tenant (z.B. nach Zahlung)
|
||||
*/
|
||||
public function activate(int $tenantId): bool
|
||||
{
|
||||
return $this->db->update('tenants', ['status' => 'active'], 'id = ?', [$tenantId]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspendiert einen Tenant (z.B. bei Zahlungsausfall)
|
||||
*/
|
||||
public function suspend(int $tenantId): bool
|
||||
{
|
||||
return $this->db->update('tenants', ['status' => 'suspended'], 'id = ?', [$tenantId]) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
/**
|
||||
* TenantSettingsManager - DB-basierte Settings pro Tenant
|
||||
*
|
||||
* Erweitert/ersetzt SettingsManager für Multi-Tenant Betrieb
|
||||
* Fällt auf den alten SettingsManager zurück wenn DB nicht verfügbar
|
||||
*/
|
||||
|
||||
namespace AuroraLivecam\Tenant;
|
||||
|
||||
use AuroraLivecam\Core\Database;
|
||||
use AuroraLivecam\Core\TenantResolver;
|
||||
|
||||
class TenantSettingsManager
|
||||
{
|
||||
private Database $db;
|
||||
private TenantResolver $resolver;
|
||||
private int $tenantId;
|
||||
private array $settings = [];
|
||||
private bool $loaded = false;
|
||||
private bool $dbAvailable = false;
|
||||
|
||||
// Fallback auf Legacy-SettingsManager
|
||||
private ?\SettingsManager $legacyManager = null;
|
||||
|
||||
public function __construct(?int $tenantId = null, ?Database $db = null, ?TenantResolver $resolver = null)
|
||||
{
|
||||
$this->db = $db ?? Database::getInstance();
|
||||
$this->resolver = $resolver ?? TenantResolver::getInstance();
|
||||
$this->tenantId = $tenantId ?? $this->resolver->getTenantId();
|
||||
|
||||
$this->checkDbAvailability();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob die DB verfügbar ist
|
||||
*/
|
||||
private function checkDbAvailability(): void
|
||||
{
|
||||
try {
|
||||
$this->db->fetchOne("SELECT 1 FROM tenant_settings LIMIT 1");
|
||||
$this->dbAvailable = true;
|
||||
} catch (\Exception $e) {
|
||||
$this->dbAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Settings für den Tenant
|
||||
*/
|
||||
private function load(): void
|
||||
{
|
||||
if ($this->loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn keine DB, nutze Legacy
|
||||
if (!$this->dbAvailable || $this->tenantId === 0) {
|
||||
$this->loadFromLegacy();
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = $this->db->fetchAll(
|
||||
"SELECT setting_key, setting_value FROM tenant_settings WHERE tenant_id = ?",
|
||||
[$this->tenantId]
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$value = $row['setting_value'];
|
||||
// JSON-Werte parsen
|
||||
if ($value !== null && ($value[0] === '{' || $value[0] === '[')) {
|
||||
$decoded = json_decode($value, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$value = $decoded;
|
||||
}
|
||||
}
|
||||
// Booleans und Zahlen konvertieren
|
||||
elseif ($value === 'true') $value = true;
|
||||
elseif ($value === 'false') $value = false;
|
||||
elseif (is_numeric($value)) $value = strpos($value, '.') !== false ? (float)$value : (int)$value;
|
||||
|
||||
$this->settings[$row['setting_key']] = $value;
|
||||
}
|
||||
|
||||
// Defaults für fehlende Keys
|
||||
$this->settings = array_merge($this->getDefaults(), $this->settings);
|
||||
$this->loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback auf Legacy SettingsManager
|
||||
*/
|
||||
private function loadFromLegacy(): void
|
||||
{
|
||||
if ($this->legacyManager === null) {
|
||||
// Legacy-Manager einbinden
|
||||
$legacyFile = dirname(__DIR__, 2) . '/SettingsManager.php';
|
||||
if (file_exists($legacyFile) && !class_exists('\SettingsManager')) {
|
||||
require_once $legacyFile;
|
||||
}
|
||||
|
||||
if (class_exists('\SettingsManager')) {
|
||||
$this->legacyManager = new \SettingsManager();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->legacyManager) {
|
||||
// Konvertiere Legacy-Settings in unser Format
|
||||
$this->settings = $this->convertLegacySettings($this->legacyManager);
|
||||
} else {
|
||||
$this->settings = $this->getDefaults();
|
||||
}
|
||||
|
||||
$this->loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert Legacy-Settings
|
||||
*/
|
||||
private function convertLegacySettings(\SettingsManager $legacy): array
|
||||
{
|
||||
$settings = $this->getDefaults();
|
||||
|
||||
// Mappe Legacy-Werte
|
||||
$mappings = [
|
||||
'viewer_display.enabled' => 'viewer_display.enabled',
|
||||
'viewer_display.min_viewers' => 'viewer_display.min_viewers',
|
||||
'video_mode.play_in_player' => 'video_mode.play_in_player',
|
||||
'video_mode.allow_download' => 'video_mode.allow_download',
|
||||
'timelapse.default_speed' => 'timelapse.default_speed',
|
||||
'ui_display.show_recommendation_banner' => 'ui_display.show_recommendation_banner',
|
||||
'ui_display.show_qr_code' => 'ui_display.show_qr_code',
|
||||
'ui_display.show_social_media' => 'ui_display.show_social_media',
|
||||
'content.guestbook_enabled' => 'content.guestbook_enabled',
|
||||
'content.gallery_enabled' => 'content.gallery_enabled',
|
||||
'weather.enabled' => 'weather.enabled',
|
||||
'weather.location' => 'weather.location',
|
||||
'weather.lat' => 'weather.lat',
|
||||
'weather.lon' => 'weather.lon',
|
||||
'seo.custom_title' => 'seo.custom_title',
|
||||
'seo.meta_description' => 'seo.meta_description',
|
||||
];
|
||||
|
||||
foreach ($mappings as $legacyKey => $newKey) {
|
||||
$value = $legacy->get($legacyKey);
|
||||
if ($value !== null) {
|
||||
$settings[$newKey] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen Setting-Wert zurück (mit Dot-Notation)
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$this->load();
|
||||
|
||||
// Direkte Keys
|
||||
if (isset($this->settings[$key])) {
|
||||
return $this->settings[$key];
|
||||
}
|
||||
|
||||
// Dot-Notation auflösen
|
||||
$keys = explode('.', $key);
|
||||
$value = $this->settings;
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (!is_array($value) || !isset($value[$k])) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$k];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt einen Setting-Wert
|
||||
*/
|
||||
public function set(string $key, mixed $value): bool
|
||||
{
|
||||
$this->load();
|
||||
|
||||
// Wenn keine DB, nutze Legacy
|
||||
if (!$this->dbAvailable || $this->tenantId === 0) {
|
||||
return $this->setLegacy($key, $value);
|
||||
}
|
||||
|
||||
// Wert für DB vorbereiten
|
||||
$dbValue = $this->prepareValueForDb($value);
|
||||
|
||||
// UPSERT
|
||||
$sql = "INSERT INTO tenant_settings (tenant_id, setting_key, setting_value)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)";
|
||||
|
||||
$result = $this->db->execute($sql, [$this->tenantId, $key, $dbValue]) > 0;
|
||||
|
||||
if ($result) {
|
||||
$this->settings[$key] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt Legacy-Setting
|
||||
*/
|
||||
private function setLegacy(string $key, mixed $value): bool
|
||||
{
|
||||
if ($this->legacyManager) {
|
||||
return $this->legacyManager->set($key, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereitet einen Wert für die DB vor
|
||||
*/
|
||||
private function prepareValueForDb(mixed $value): string
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
if (is_array($value) || is_object($value)) {
|
||||
return json_encode($value);
|
||||
}
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht ein Setting
|
||||
*/
|
||||
public function delete(string $key): bool
|
||||
{
|
||||
if (!$this->dbAvailable || $this->tenantId === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->db->delete('tenant_settings', 'tenant_id = ? AND setting_key = ?', [$this->tenantId, $key]) > 0;
|
||||
|
||||
if ($result) {
|
||||
unset($this->settings[$key]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Settings zurück
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
$this->load();
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt mehrere Settings auf einmal
|
||||
*/
|
||||
public function setMany(array $settings): bool
|
||||
{
|
||||
foreach ($settings as $key => $value) {
|
||||
$this->set($key, $value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default-Settings
|
||||
*/
|
||||
private function getDefaults(): array
|
||||
{
|
||||
return [
|
||||
// Viewer Display
|
||||
'viewer_display.enabled' => true,
|
||||
'viewer_display.min_viewers' => 1,
|
||||
'viewer_display.update_interval' => 5,
|
||||
|
||||
// Video Mode
|
||||
'video_mode.play_in_player' => true,
|
||||
'video_mode.allow_download' => true,
|
||||
|
||||
// Timelapse
|
||||
'timelapse.default_speed' => 1,
|
||||
'timelapse.available_speeds' => [1, 10, 100],
|
||||
'timelapse.reverse_enabled' => true,
|
||||
|
||||
// UI Display
|
||||
'ui_display.show_recommendation_banner' => true,
|
||||
'ui_display.show_qr_code' => true,
|
||||
'ui_display.show_social_media' => true,
|
||||
|
||||
// Zoom
|
||||
'zoom.show_controls' => true,
|
||||
'zoom.max_level' => 4.0,
|
||||
|
||||
// Content
|
||||
'content.guestbook_enabled' => true,
|
||||
'content.gallery_enabled' => true,
|
||||
'content.ai_events_enabled' => true,
|
||||
|
||||
// Weather
|
||||
'weather.enabled' => true,
|
||||
'weather.location' => 'Zürich,CH',
|
||||
'weather.lat' => '47.3769',
|
||||
'weather.lon' => '8.5417',
|
||||
'weather.update_interval' => 5,
|
||||
'weather.units' => 'metric',
|
||||
|
||||
// SEO
|
||||
'seo.custom_title' => '',
|
||||
'seo.meta_description' => '',
|
||||
'seo.meta_keywords' => '',
|
||||
|
||||
// Theme
|
||||
'theme.default' => 'theme-legacy',
|
||||
'theme.show_switcher' => false,
|
||||
];
|
||||
}
|
||||
|
||||
// === Helper-Methoden (kompatibel mit altem SettingsManager) ===
|
||||
|
||||
public function isWeatherEnabled(): bool
|
||||
{
|
||||
return $this->get('weather.enabled', true) === true;
|
||||
}
|
||||
|
||||
public function getWeatherLocation(): string
|
||||
{
|
||||
return $this->get('weather.location', 'Zürich,CH');
|
||||
}
|
||||
|
||||
public function getWeatherCoords(): array
|
||||
{
|
||||
return [
|
||||
'lat' => $this->get('weather.lat', '47.3769'),
|
||||
'lon' => $this->get('weather.lon', '8.5417'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getWeatherUpdateInterval(): int
|
||||
{
|
||||
return (int)$this->get('weather.update_interval', 5);
|
||||
}
|
||||
|
||||
public function shouldShowViewers(): bool
|
||||
{
|
||||
return $this->get('viewer_display.enabled', true) === true;
|
||||
}
|
||||
|
||||
public function getMinViewers(): int
|
||||
{
|
||||
return (int)$this->get('viewer_display.min_viewers', 1);
|
||||
}
|
||||
|
||||
public function isGuestbookEnabled(): bool
|
||||
{
|
||||
return $this->get('content.guestbook_enabled', true) === true;
|
||||
}
|
||||
|
||||
public function isGalleryEnabled(): bool
|
||||
{
|
||||
return $this->get('content.gallery_enabled', true) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX-Handler (kompatibel mit altem SettingsManager)
|
||||
*/
|
||||
public function handleAjax(): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||||
if (!isset($_POST['settings_action'])) return;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Auth prüfen
|
||||
if (!$this->isAdmin()) {
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $_POST['settings_action'];
|
||||
|
||||
if ($action === 'update' && isset($_POST['key'], $_POST['value'])) {
|
||||
$key = $_POST['key'];
|
||||
$value = $_POST['value'];
|
||||
|
||||
// Booleans konvertieren
|
||||
if ($value === 'true') $value = true;
|
||||
elseif ($value === 'false') $value = false;
|
||||
|
||||
$success = $this->set($key, $value);
|
||||
echo json_encode(['success' => $success]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'get') {
|
||||
echo json_encode(['success' => true, 'data' => $this->all()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => false, 'error' => 'Unknown action']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob der User Admin ist
|
||||
*/
|
||||
private function isAdmin(): bool
|
||||
{
|
||||
return isset($_SESSION['admin']) && $_SESSION['admin'] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Settings neu aus der DB
|
||||
*/
|
||||
public function reload(): void
|
||||
{
|
||||
$this->loaded = false;
|
||||
$this->settings = [];
|
||||
$this->load();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user