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,26 @@
|
|||||||
|
# Configuration (contains secrets)
|
||||||
|
config.php
|
||||||
|
|
||||||
|
# Cache files
|
||||||
|
weather_cache.json
|
||||||
|
active_viewers.json
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Vendor (if using composer)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Uploads (user content)
|
||||||
|
uploads/
|
||||||
@@ -83,6 +83,38 @@ class SettingsManager {
|
|||||||
'update_interval' => 5, // Minuten
|
'update_interval' => 5, // Minuten
|
||||||
'units' => 'metric' // metric (Celsius) oder imperial (Fahrenheit)
|
'units' => 'metric' // metric (Celsius) oder imperial (Fahrenheit)
|
||||||
],
|
],
|
||||||
|
// SaaS Features - alle aktivierbar/deaktivierbar
|
||||||
|
'saas_features' => [
|
||||||
|
// Multi-Tenant
|
||||||
|
'multi_tenant_enabled' => false, // Aktiviert DB-basierte Tenant-Verwaltung
|
||||||
|
'customer_management_enabled' => false,
|
||||||
|
|
||||||
|
// Onboarding
|
||||||
|
'self_registration_enabled' => false,
|
||||||
|
'email_verification_required' => true,
|
||||||
|
'trial_enabled' => true,
|
||||||
|
'trial_days' => 14,
|
||||||
|
|
||||||
|
// Billing
|
||||||
|
'billing_enabled' => false,
|
||||||
|
'stripe_enabled' => false,
|
||||||
|
'free_plan_available' => true,
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
'tenant_dashboard_enabled' => false,
|
||||||
|
'analytics_enabled' => false,
|
||||||
|
'custom_domain_enabled' => false,
|
||||||
|
'custom_branding_enabled' => false,
|
||||||
|
|
||||||
|
// Landing
|
||||||
|
'landing_page_enabled' => false,
|
||||||
|
'demo_mode_enabled' => false,
|
||||||
|
|
||||||
|
// Limits (Default für Free-Plan)
|
||||||
|
'default_max_viewers' => 50,
|
||||||
|
'default_storage_mb' => 500,
|
||||||
|
'default_retention_days' => 7
|
||||||
|
],
|
||||||
'last_updated' => null,
|
'last_updated' => null,
|
||||||
'updated_by' => null
|
'updated_by' => null
|
||||||
];
|
];
|
||||||
@@ -277,4 +309,49 @@ class SettingsManager {
|
|||||||
public function getWeatherUnits() {
|
public function getWeatherUnits() {
|
||||||
return $this->get('weather.units') ?? 'metric';
|
return $this->get('weather.units') ?? 'metric';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaaS Feature Helper
|
||||||
|
public function isMultiTenantEnabled() {
|
||||||
|
return $this->get('saas_features.multi_tenant_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSelfRegistrationEnabled() {
|
||||||
|
return $this->get('saas_features.self_registration_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isBillingEnabled() {
|
||||||
|
return $this->get('saas_features.billing_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isStripeEnabled() {
|
||||||
|
return $this->get('saas_features.stripe_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTenantDashboardEnabled() {
|
||||||
|
return $this->get('saas_features.tenant_dashboard_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAnalyticsEnabled() {
|
||||||
|
return $this->get('saas_features.analytics_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCustomDomainEnabled() {
|
||||||
|
return $this->get('saas_features.custom_domain_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCustomBrandingEnabled() {
|
||||||
|
return $this->get('saas_features.custom_branding_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLandingPageEnabled() {
|
||||||
|
return $this->get('saas_features.landing_page_enabled') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTrialDays() {
|
||||||
|
return $this->get('saas_features.trial_days') ?? 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultMaxViewers() {
|
||||||
|
return $this->get('saas_features.default_max_viewers') ?? 50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Aurora Livecam - Konfigurationsdatei
|
||||||
|
*
|
||||||
|
* Kopiere diese Datei zu config.php und passe die Werte an.
|
||||||
|
* WICHTIG: config.php niemals in Git committen!
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Datenbank-Konfiguration
|
||||||
|
'database' => [
|
||||||
|
'host' => 'localhost',
|
||||||
|
'port' => 3306,
|
||||||
|
'database' => 'aurora_livecam',
|
||||||
|
'username' => 'root',
|
||||||
|
'password' => '',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Anwendungs-Einstellungen
|
||||||
|
'app' => [
|
||||||
|
'name' => 'Aurora Livecam',
|
||||||
|
'url' => 'https://aurora-weather-livecam.com',
|
||||||
|
'debug' => false,
|
||||||
|
'timezone' => 'Europe/Zurich',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Multi-Tenant Einstellungen
|
||||||
|
'tenant' => [
|
||||||
|
'default_subdomain_suffix' => '.aurora-livecam.com',
|
||||||
|
'allow_custom_domains' => true,
|
||||||
|
'trial_days' => 14,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Stripe (für Billing)
|
||||||
|
'stripe' => [
|
||||||
|
'public_key' => '',
|
||||||
|
'secret_key' => '',
|
||||||
|
'webhook_secret' => '',
|
||||||
|
'currency' => 'chf',
|
||||||
|
],
|
||||||
|
|
||||||
|
// E-Mail Einstellungen (für Onboarding)
|
||||||
|
'mail' => [
|
||||||
|
'host' => 'smtp.example.com',
|
||||||
|
'port' => 587,
|
||||||
|
'username' => '',
|
||||||
|
'password' => '',
|
||||||
|
'from_address' => 'noreply@aurora-livecam.com',
|
||||||
|
'from_name' => 'Aurora Livecam',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Sicherheit
|
||||||
|
'security' => [
|
||||||
|
'session_lifetime' => 7200, // 2 Stunden
|
||||||
|
'remember_me_days' => 30,
|
||||||
|
'password_min_length' => 8,
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
-- Aurora Livecam - Multi-Tenant SaaS Schema
|
||||||
|
-- Version: 1.0.0
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Subscription Plans
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `plans` (
|
||||||
|
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(100) NOT NULL,
|
||||||
|
`slug` VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
`stripe_price_id` VARCHAR(100) NULL,
|
||||||
|
`price_monthly` DECIMAL(10,2) DEFAULT 0.00,
|
||||||
|
`price_yearly` DECIMAL(10,2) DEFAULT 0.00,
|
||||||
|
`features` JSON NULL COMMENT '{"max_viewers": 100, "storage_gb": 5, "custom_domain": true}',
|
||||||
|
`is_active` TINYINT(1) DEFAULT 1,
|
||||||
|
`sort_order` INT DEFAULT 0,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Default Plans
|
||||||
|
INSERT INTO `plans` (`name`, `slug`, `price_monthly`, `price_yearly`, `features`, `sort_order`) VALUES
|
||||||
|
('Free', 'free', 0.00, 0.00, '{"max_viewers": 10, "storage_gb": 0.5, "custom_domain": false, "weather_widget": true, "timelapse": false, "analytics": false, "branding": false}', 1),
|
||||||
|
('Basic', 'basic', 19.00, 190.00, '{"max_viewers": 50, "storage_gb": 5, "custom_domain": false, "weather_widget": true, "timelapse": true, "analytics": true, "branding": false}', 2),
|
||||||
|
('Professional', 'professional', 49.00, 490.00, '{"max_viewers": 200, "storage_gb": 20, "custom_domain": true, "weather_widget": true, "timelapse": true, "analytics": true, "branding": true}', 3),
|
||||||
|
('Enterprise', 'enterprise', 149.00, 1490.00, '{"max_viewers": -1, "storage_gb": 100, "custom_domain": true, "weather_widget": true, "timelapse": true, "analytics": true, "branding": true, "priority_support": true}', 4);
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Tenants (Customers)
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `tenants` (
|
||||||
|
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`uuid` VARCHAR(36) UNIQUE NOT NULL,
|
||||||
|
`name` VARCHAR(255) NOT NULL,
|
||||||
|
`slug` VARCHAR(100) UNIQUE NOT NULL COMMENT 'URL-safe identifier, e.g. aurora, seecam',
|
||||||
|
`email` VARCHAR(255) NOT NULL,
|
||||||
|
`status` ENUM('trial', 'active', 'suspended', 'cancelled') DEFAULT 'trial',
|
||||||
|
`plan_id` INT UNSIGNED NULL,
|
||||||
|
`trial_ends_at` TIMESTAMP NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`plan_id`) REFERENCES `plans`(`id`) ON DELETE SET NULL,
|
||||||
|
INDEX `idx_status` (`status`),
|
||||||
|
INDEX `idx_slug` (`slug`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Tenant Domains
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `tenant_domains` (
|
||||||
|
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`tenant_id` INT UNSIGNED NOT NULL,
|
||||||
|
`domain` VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
`is_primary` TINYINT(1) DEFAULT 0,
|
||||||
|
`ssl_status` ENUM('pending', 'active', 'failed') DEFAULT 'pending',
|
||||||
|
`verified_at` TIMESTAMP NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE,
|
||||||
|
INDEX `idx_domain` (`domain`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Tenant Settings (replaces settings.json per tenant)
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `tenant_settings` (
|
||||||
|
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`tenant_id` INT UNSIGNED NOT NULL,
|
||||||
|
`setting_key` VARCHAR(255) NOT NULL,
|
||||||
|
`setting_value` TEXT NULL,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY `uk_tenant_key` (`tenant_id`, `setting_key`),
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Tenant Branding
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `tenant_branding` (
|
||||||
|
`tenant_id` INT UNSIGNED PRIMARY KEY,
|
||||||
|
`site_name` VARCHAR(255) NULL,
|
||||||
|
`site_name_full` VARCHAR(255) NULL,
|
||||||
|
`tagline` VARCHAR(255) NULL,
|
||||||
|
`logo_path` VARCHAR(500) NULL,
|
||||||
|
`favicon_path` VARCHAR(500) NULL,
|
||||||
|
`primary_color` VARCHAR(7) DEFAULT '#667eea',
|
||||||
|
`secondary_color` VARCHAR(7) DEFAULT '#764ba2',
|
||||||
|
`accent_color` VARCHAR(7) DEFAULT '#f093fb',
|
||||||
|
`welcome_text_de` TEXT NULL,
|
||||||
|
`welcome_text_en` TEXT NULL,
|
||||||
|
`footer_text` TEXT NULL,
|
||||||
|
`custom_css` TEXT NULL,
|
||||||
|
`custom_js` TEXT NULL,
|
||||||
|
`social_facebook` VARCHAR(255) NULL,
|
||||||
|
`social_instagram` VARCHAR(255) NULL,
|
||||||
|
`social_youtube` VARCHAR(255) NULL,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Tenant Streams
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `tenant_streams` (
|
||||||
|
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`tenant_id` INT UNSIGNED NOT NULL,
|
||||||
|
`name` VARCHAR(255) DEFAULT 'Main Stream',
|
||||||
|
`stream_url` VARCHAR(500) NOT NULL,
|
||||||
|
`stream_type` ENUM('hls', 'rtmp', 'webrtc', 'iframe') DEFAULT 'hls',
|
||||||
|
`is_active` TINYINT(1) DEFAULT 1,
|
||||||
|
`is_primary` TINYINT(1) DEFAULT 1,
|
||||||
|
`last_check_at` TIMESTAMP NULL,
|
||||||
|
`last_status` ENUM('online', 'offline', 'error') NULL,
|
||||||
|
`error_message` VARCHAR(500) NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Users
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `users` (
|
||||||
|
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`tenant_id` INT UNSIGNED NULL COMMENT 'NULL = Super Admin',
|
||||||
|
`email` VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
`password_hash` VARCHAR(255) NOT NULL,
|
||||||
|
`name` VARCHAR(255) NULL,
|
||||||
|
`role` ENUM('super_admin', 'tenant_admin', 'tenant_user') NOT NULL DEFAULT 'tenant_user',
|
||||||
|
`email_verified_at` TIMESTAMP NULL,
|
||||||
|
`last_login_at` TIMESTAMP NULL,
|
||||||
|
`remember_token` VARCHAR(100) NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE,
|
||||||
|
INDEX `idx_email` (`email`),
|
||||||
|
INDEX `idx_tenant` (`tenant_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Subscriptions
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `subscriptions` (
|
||||||
|
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`tenant_id` INT UNSIGNED NOT NULL,
|
||||||
|
`plan_id` INT UNSIGNED NOT NULL,
|
||||||
|
`stripe_subscription_id` VARCHAR(100) NULL,
|
||||||
|
`stripe_customer_id` VARCHAR(100) NULL,
|
||||||
|
`status` ENUM('trialing', 'active', 'past_due', 'canceled', 'unpaid', 'incomplete') DEFAULT 'trialing',
|
||||||
|
`current_period_start` TIMESTAMP NULL,
|
||||||
|
`current_period_end` TIMESTAMP NULL,
|
||||||
|
`canceled_at` TIMESTAMP NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`plan_id`) REFERENCES `plans`(`id`),
|
||||||
|
INDEX `idx_tenant` (`tenant_id`),
|
||||||
|
INDEX `idx_stripe_sub` (`stripe_subscription_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Invoices (Stripe cache)
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `invoices` (
|
||||||
|
`id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`tenant_id` INT UNSIGNED NOT NULL,
|
||||||
|
`stripe_invoice_id` VARCHAR(100) UNIQUE NULL,
|
||||||
|
`amount` DECIMAL(10,2) NOT NULL,
|
||||||
|
`currency` VARCHAR(3) DEFAULT 'CHF',
|
||||||
|
`status` VARCHAR(50) NULL,
|
||||||
|
`paid_at` TIMESTAMP NULL,
|
||||||
|
`invoice_pdf_url` VARCHAR(500) NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Viewer Statistics
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `viewer_stats` (
|
||||||
|
`id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`tenant_id` INT UNSIGNED NOT NULL,
|
||||||
|
`recorded_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`viewer_count` INT DEFAULT 0,
|
||||||
|
`unique_sessions` INT DEFAULT 0,
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE,
|
||||||
|
INDEX `idx_tenant_time` (`tenant_id`, `recorded_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
-- Onboarding Progress
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS `tenant_onboarding` (
|
||||||
|
`tenant_id` INT UNSIGNED PRIMARY KEY,
|
||||||
|
`current_step` INT DEFAULT 1,
|
||||||
|
`stream_verified` TINYINT(1) DEFAULT 0,
|
||||||
|
`branding_configured` TINYINT(1) DEFAULT 0,
|
||||||
|
`payment_configured` TINYINT(1) DEFAULT 0,
|
||||||
|
`completed_at` TIMESTAMP NULL,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
+55
-16
@@ -6,6 +6,11 @@ require __DIR__ . '/vendor/autoload.php';
|
|||||||
require_once 'SettingsManager.php';
|
require_once 'SettingsManager.php';
|
||||||
require_once 'WeatherManager.php';
|
require_once 'WeatherManager.php';
|
||||||
|
|
||||||
|
// Multi-Tenant Bootstrap laden (falls vorhanden)
|
||||||
|
if (file_exists(__DIR__ . '/src/bootstrap.php')) {
|
||||||
|
require_once __DIR__ . '/src/bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
// SettingsManager initialisieren
|
// SettingsManager initialisieren
|
||||||
$settingsManager = new SettingsManager();
|
$settingsManager = new SettingsManager();
|
||||||
|
|
||||||
@@ -60,30 +65,55 @@ function safeRedirect($url) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hauptlogik
|
// Hauptlogik - Domain Redirects werden jetzt in bootstrap.php behandelt
|
||||||
|
// (Legacy-Redirect bleibt als Fallback falls Bootstrap nicht geladen)
|
||||||
$oldDomains = [
|
$oldDomains = [
|
||||||
'www.aurora-wetter-lifecam.ch',
|
'www.aurora-wetter-lifecam.ch',
|
||||||
'www.aurora-wetter-livecam.ch'
|
'www.aurora-wetter-livecam.ch'
|
||||||
];
|
];
|
||||||
$newDomain = 'www.aurora-weather-livecam.com';
|
$newDomain = 'www.aurora-weather-livecam.com';
|
||||||
|
|
||||||
if (in_array($_SERVER['HTTP_HOST'], $oldDomains)) {
|
if (in_array($_SERVER['HTTP_HOST'] ?? '', $oldDomains)) {
|
||||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||||
$newUrl = $protocol . '://' . $newDomain . $_SERVER['REQUEST_URI'];
|
$newUrl = $protocol . '://' . $newDomain . $_SERVER['REQUEST_URI'];
|
||||||
|
|
||||||
// Logging für Debugging
|
|
||||||
error_log("Umleitung von {$_SERVER['HTTP_HOST']} nach $newUrl");
|
|
||||||
|
|
||||||
if (!headers_sent()) {
|
if (!headers_sent()) {
|
||||||
header("HTTP/1.1 301 Moved Permanently");
|
header("HTTP/1.1 301 Moved Permanently");
|
||||||
header("Location: " . $newUrl);
|
header("Location: " . $newUrl);
|
||||||
} else {
|
|
||||||
echo '<script>window.location.href="' . $newUrl . '";</script>';
|
|
||||||
}
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Site-Konfiguration basierend auf Domain
|
// Site-Konfiguration: Nutze Multi-Tenant System falls verfügbar, sonst Legacy
|
||||||
|
if (function_exists('getSiteConfig')) {
|
||||||
|
// Multi-Tenant Modus (aus bootstrap.php)
|
||||||
|
$tenantConfig = getSiteConfig();
|
||||||
|
$isSeecam = ($tenantConfig['tenant_slug'] === 'seecam');
|
||||||
|
|
||||||
|
$siteConfig = [
|
||||||
|
'domain' => $_SERVER['HTTP_HOST'] ?? 'localhost',
|
||||||
|
'domainUrl' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost'),
|
||||||
|
'logo' => $tenantConfig['logo_path'] ?? ($isSeecam ? 'seecam.jpg' : 'logo.png'),
|
||||||
|
'siteName' => $tenantConfig['site_name'],
|
||||||
|
'siteNameFull' => $tenantConfig['site_name_full'],
|
||||||
|
'siteNameFullEn' => $tenantConfig['site_name_full'],
|
||||||
|
'siteTitle' => $tenantConfig['site_name_full'] . ' - Live Webcam',
|
||||||
|
'author' => $tenantConfig['site_name_full'],
|
||||||
|
'alternateName' => $tenantConfig['site_name'] . ' Webcam Schweiz',
|
||||||
|
'welcomeDe' => $tenantConfig['welcome_de'] ?: ('Willkommen bei ' . $tenantConfig['site_name_full']),
|
||||||
|
'welcomeEn' => $tenantConfig['welcome_en'] ?: ('Welcome to ' . $tenantConfig['site_name_full']),
|
||||||
|
'aboutDe' => $tenantConfig['site_name_full'] . ' ist ein Herzensprojekt von Wetterbegeisterten.',
|
||||||
|
'aboutEn' => $tenantConfig['site_name_full'] . ' is a passion project by weather enthusiasts.',
|
||||||
|
'blogTitle' => $tenantConfig['site_name'] . ' Wetter Blog',
|
||||||
|
'footerName' => $tenantConfig['site_name_full'],
|
||||||
|
'copyright' => '© ' . date('Y') . ' ' . $tenantConfig['site_name_full'],
|
||||||
|
// Zusätzliche Multi-Tenant Felder
|
||||||
|
'tenant_id' => $tenantConfig['tenant_id'] ?? 0,
|
||||||
|
'primary_color' => $tenantConfig['primary_color'] ?? '#667eea',
|
||||||
|
'secondary_color' => $tenantConfig['secondary_color'] ?? '#764ba2',
|
||||||
|
'custom_css' => $tenantConfig['custom_css'] ?? '',
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Legacy-Modus (hardcoded)
|
||||||
$isSeecam = ($_SERVER['HTTP_HOST'] === 'www.seecam.ch' || $_SERVER['HTTP_HOST'] === 'seecam.ch');
|
$isSeecam = ($_SERVER['HTTP_HOST'] === 'www.seecam.ch' || $_SERVER['HTTP_HOST'] === 'seecam.ch');
|
||||||
|
|
||||||
if ($isSeecam) {
|
if ($isSeecam) {
|
||||||
@@ -99,11 +129,15 @@ if ($isSeecam) {
|
|||||||
'alternateName' => 'Seecam Webcam Schweiz',
|
'alternateName' => 'Seecam Webcam Schweiz',
|
||||||
'welcomeDe' => 'Willkommen bei Seecam Wetter Livecam',
|
'welcomeDe' => 'Willkommen bei Seecam Wetter Livecam',
|
||||||
'welcomeEn' => 'Welcome to Seecam Weather Livecam',
|
'welcomeEn' => 'Welcome to Seecam Weather Livecam',
|
||||||
'aboutDe' => 'Seecam Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten. Wir möchten Ihnen die Schönheit der Natur und Faszination des Wetters näher bringen.',
|
'aboutDe' => 'Seecam Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten.',
|
||||||
'aboutEn' => 'Seecam Weather Livecam is a passion project...',
|
'aboutEn' => 'Seecam Weather Livecam is a passion project.',
|
||||||
'blogTitle' => 'Seecam Wetter Blog',
|
'blogTitle' => 'Seecam Wetter Blog',
|
||||||
'footerName' => 'Seecam Wetter Livecam',
|
'footerName' => 'Seecam Wetter Livecam',
|
||||||
'copyright' => '© 2024 Seecam Wetter Livecam - Webcam Zürich Oberland'
|
'copyright' => '© 2024 Seecam Wetter Livecam - Webcam Zürich Oberland',
|
||||||
|
'tenant_id' => 0,
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
'custom_css' => '',
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$siteConfig = [
|
$siteConfig = [
|
||||||
@@ -118,13 +152,18 @@ if ($isSeecam) {
|
|||||||
'alternateName' => 'Aurora Webcam Schweiz',
|
'alternateName' => 'Aurora Webcam Schweiz',
|
||||||
'welcomeDe' => 'Willkommen bei Aurora Wetter Livecam',
|
'welcomeDe' => 'Willkommen bei Aurora Wetter Livecam',
|
||||||
'welcomeEn' => 'Welcome to Aurora Weather Livecam',
|
'welcomeEn' => 'Welcome to Aurora Weather Livecam',
|
||||||
'aboutDe' => 'Aurora Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten. Wir möchten Ihnen die Schönheit der Natur und Faszination des Wetters näher bringen.',
|
'aboutDe' => 'Aurora Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten.',
|
||||||
'aboutEn' => 'Aurora Weather Livecam is a passion project...',
|
'aboutEn' => 'Aurora Weather Livecam is a passion project.',
|
||||||
'blogTitle' => 'Aurora Wetter Blog',
|
'blogTitle' => 'Aurora Wetter Blog',
|
||||||
'footerName' => 'Aurora Wetter Livecam',
|
'footerName' => 'Aurora Wetter Livecam',
|
||||||
'copyright' => '© 2024 Aurora Wetter Lifecam - Webcam Zürich Oberland'
|
'copyright' => '© 2024 Aurora Wetter Lifecam - Webcam Zürich Oberland',
|
||||||
|
'tenant_id' => 0,
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
'custom_css' => '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Database - PDO Wrapper mit Singleton Pattern
|
||||||
|
*
|
||||||
|
* Verwendung:
|
||||||
|
* $db = Database::getInstance();
|
||||||
|
* $users = $db->fetchAll("SELECT * FROM users WHERE tenant_id = ?", [$tenantId]);
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AuroraLivecam\Core;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class Database
|
||||||
|
{
|
||||||
|
private static ?Database $instance = null;
|
||||||
|
private ?PDO $pdo = null;
|
||||||
|
private array $config;
|
||||||
|
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
$this->config = $this->loadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton: Gibt die einzige Instanz zurück
|
||||||
|
*/
|
||||||
|
public static function getInstance(): Database
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt die Datenbank-Konfiguration
|
||||||
|
*/
|
||||||
|
private function loadConfig(): array
|
||||||
|
{
|
||||||
|
// Versuche .env oder config.php zu laden
|
||||||
|
$configFile = dirname(__DIR__, 2) . '/config.php';
|
||||||
|
|
||||||
|
if (file_exists($configFile)) {
|
||||||
|
$config = require $configFile;
|
||||||
|
return $config['database'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback auf Umgebungsvariablen
|
||||||
|
return [
|
||||||
|
'host' => getenv('DB_HOST') ?: 'localhost',
|
||||||
|
'port' => getenv('DB_PORT') ?: 3306,
|
||||||
|
'database' => getenv('DB_DATABASE') ?: 'aurora_livecam',
|
||||||
|
'username' => getenv('DB_USERNAME') ?: 'root',
|
||||||
|
'password' => getenv('DB_PASSWORD') ?: '',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stellt die Datenbankverbindung her (Lazy Loading)
|
||||||
|
*/
|
||||||
|
public function connect(): PDO
|
||||||
|
{
|
||||||
|
if ($this->pdo !== null) {
|
||||||
|
return $this->pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = sprintf(
|
||||||
|
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||||
|
$this->config['host'],
|
||||||
|
$this->config['port'],
|
||||||
|
$this->config['database'],
|
||||||
|
$this->config['charset']
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pdo = new PDO($dsn, $this->config['username'], $this->config['password'], [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new Exception('Database connection failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt eine Query aus und gibt alle Ergebnisse zurück
|
||||||
|
*/
|
||||||
|
public function fetchAll(string $sql, array $params = []): array
|
||||||
|
{
|
||||||
|
$stmt = $this->connect()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt eine Query aus und gibt eine Zeile zurück
|
||||||
|
*/
|
||||||
|
public function fetchOne(string $sql, array $params = []): ?array
|
||||||
|
{
|
||||||
|
$stmt = $this->connect()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
return $result ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt eine Query aus und gibt einen einzelnen Wert zurück
|
||||||
|
*/
|
||||||
|
public function fetchColumn(string $sql, array $params = [], int $column = 0): mixed
|
||||||
|
{
|
||||||
|
$stmt = $this->connect()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
return $stmt->fetchColumn($column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt INSERT/UPDATE/DELETE aus und gibt die Anzahl betroffener Zeilen zurück
|
||||||
|
*/
|
||||||
|
public function execute(string $sql, array $params = []): int
|
||||||
|
{
|
||||||
|
$stmt = $this->connect()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
return $stmt->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INSERT und gibt die neue ID zurück
|
||||||
|
*/
|
||||||
|
public function insert(string $table, array $data): int
|
||||||
|
{
|
||||||
|
$columns = implode(', ', array_map(fn($col) => "`$col`", array_keys($data)));
|
||||||
|
$placeholders = implode(', ', array_fill(0, count($data), '?'));
|
||||||
|
|
||||||
|
$sql = "INSERT INTO `$table` ($columns) VALUES ($placeholders)";
|
||||||
|
$this->execute($sql, array_values($data));
|
||||||
|
|
||||||
|
return (int) $this->connect()->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UPDATE mit WHERE-Bedingung
|
||||||
|
*/
|
||||||
|
public function update(string $table, array $data, string $where, array $whereParams = []): int
|
||||||
|
{
|
||||||
|
$set = implode(', ', array_map(fn($col) => "`$col` = ?", array_keys($data)));
|
||||||
|
$sql = "UPDATE `$table` SET $set WHERE $where";
|
||||||
|
|
||||||
|
return $this->execute($sql, [...array_values($data), ...$whereParams]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE mit WHERE-Bedingung
|
||||||
|
*/
|
||||||
|
public function delete(string $table, string $where, array $params = []): int
|
||||||
|
{
|
||||||
|
return $this->execute("DELETE FROM `$table` WHERE $where", $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet eine Transaktion
|
||||||
|
*/
|
||||||
|
public function beginTransaction(): bool
|
||||||
|
{
|
||||||
|
return $this->connect()->beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bestätigt eine Transaktion
|
||||||
|
*/
|
||||||
|
public function commit(): bool
|
||||||
|
{
|
||||||
|
return $this->connect()->commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Macht eine Transaktion rückgängig
|
||||||
|
*/
|
||||||
|
public function rollback(): bool
|
||||||
|
{
|
||||||
|
return $this->connect()->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob eine Datenbankverbindung besteht
|
||||||
|
*/
|
||||||
|
public function isConnected(): bool
|
||||||
|
{
|
||||||
|
return $this->pdo !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die PDO-Instanz direkt zurück (für komplexe Queries)
|
||||||
|
*/
|
||||||
|
public function getPdo(): PDO
|
||||||
|
{
|
||||||
|
return $this->connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent cloning
|
||||||
|
private function __clone() {}
|
||||||
|
|
||||||
|
// Prevent unserialization
|
||||||
|
public function __wakeup()
|
||||||
|
{
|
||||||
|
throw new Exception("Cannot unserialize singleton");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* TenantResolver - Ermittelt den aktuellen Tenant basierend auf Domain
|
||||||
|
*
|
||||||
|
* Ersetzt den hardcoded Domain-Switch in index.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AuroraLivecam\Core;
|
||||||
|
|
||||||
|
class TenantResolver
|
||||||
|
{
|
||||||
|
private Database $db;
|
||||||
|
private ?array $currentTenant = null;
|
||||||
|
private ?array $currentBranding = null;
|
||||||
|
private static ?TenantResolver $instance = null;
|
||||||
|
|
||||||
|
// Cache für Domain-Lookups (vermeidet DB-Anfragen bei jedem Request)
|
||||||
|
private static array $domainCache = [];
|
||||||
|
|
||||||
|
public function __construct(?Database $db = null)
|
||||||
|
{
|
||||||
|
$this->db = $db ?? Database::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton für globalen Zugriff
|
||||||
|
*/
|
||||||
|
public static function getInstance(): TenantResolver
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst die aktuelle Domain auf und gibt den Tenant zurück
|
||||||
|
*/
|
||||||
|
public function resolve(?string $domain = null): ?array
|
||||||
|
{
|
||||||
|
$domain = $domain ?? $this->getCurrentDomain();
|
||||||
|
|
||||||
|
if ($this->currentTenant !== null && ($this->currentTenant['domain'] ?? '') === $domain) {
|
||||||
|
return $this->currentTenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache prüfen
|
||||||
|
if (isset(self::$domainCache[$domain])) {
|
||||||
|
$this->currentTenant = self::$domainCache[$domain];
|
||||||
|
return $this->currentTenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aus DB laden
|
||||||
|
$this->currentTenant = $this->loadTenantByDomain($domain);
|
||||||
|
|
||||||
|
// In Cache speichern
|
||||||
|
self::$domainCache[$domain] = $this->currentTenant;
|
||||||
|
|
||||||
|
return $this->currentTenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt einen Tenant anhand der Domain aus der Datenbank
|
||||||
|
*/
|
||||||
|
private function loadTenantByDomain(string $domain): ?array
|
||||||
|
{
|
||||||
|
// Normalisiere Domain (ohne www.)
|
||||||
|
$normalizedDomain = $this->normalizeDomain($domain);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
td.domain,
|
||||||
|
td.is_primary,
|
||||||
|
p.name as plan_name,
|
||||||
|
p.slug as plan_slug,
|
||||||
|
p.features as plan_features
|
||||||
|
FROM tenant_domains td
|
||||||
|
JOIN tenants t ON td.tenant_id = t.id
|
||||||
|
LEFT JOIN plans p ON t.plan_id = p.id
|
||||||
|
WHERE td.domain = ? OR td.domain = ?
|
||||||
|
LIMIT 1
|
||||||
|
";
|
||||||
|
|
||||||
|
$tenant = $this->db->fetchOne($sql, [$domain, $normalizedDomain]);
|
||||||
|
|
||||||
|
if ($tenant && isset($tenant['plan_features'])) {
|
||||||
|
$tenant['plan_features'] = json_decode($tenant['plan_features'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tenant;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fallback: Keine DB-Verbindung oder Tabelle existiert nicht
|
||||||
|
return $this->getFallbackTenant($domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback für Legacy-Modus (ohne Datenbank)
|
||||||
|
* Unterstützt die alten hardcoded Domains
|
||||||
|
*/
|
||||||
|
private function getFallbackTenant(string $domain): ?array
|
||||||
|
{
|
||||||
|
$normalizedDomain = $this->normalizeDomain($domain);
|
||||||
|
|
||||||
|
// Alte seecam.ch Konfiguration
|
||||||
|
if (str_contains($normalizedDomain, 'seecam.ch')) {
|
||||||
|
return [
|
||||||
|
'id' => 0,
|
||||||
|
'uuid' => 'legacy-seecam',
|
||||||
|
'name' => 'Seecam',
|
||||||
|
'slug' => 'seecam',
|
||||||
|
'status' => 'active',
|
||||||
|
'domain' => $domain,
|
||||||
|
'is_legacy' => true,
|
||||||
|
'branding' => [
|
||||||
|
'site_name' => 'Seecam',
|
||||||
|
'site_name_full' => 'Seecam.ch - Live Webcam',
|
||||||
|
'tagline' => 'Ihre Live-Webcam',
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: Aurora
|
||||||
|
if (str_contains($normalizedDomain, 'aurora') ||
|
||||||
|
str_contains($normalizedDomain, 'localhost') ||
|
||||||
|
$normalizedDomain === '127.0.0.1') {
|
||||||
|
return [
|
||||||
|
'id' => 0,
|
||||||
|
'uuid' => 'legacy-aurora',
|
||||||
|
'name' => 'Aurora Weather Livecam',
|
||||||
|
'slug' => 'aurora',
|
||||||
|
'status' => 'active',
|
||||||
|
'domain' => $domain,
|
||||||
|
'is_legacy' => true,
|
||||||
|
'branding' => [
|
||||||
|
'site_name' => 'Aurora',
|
||||||
|
'site_name_full' => 'Aurora Weather Livecam - Zürich Oberland',
|
||||||
|
'tagline' => 'Wetter Webcam Schweiz',
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unbekannte Domain - Default Tenant
|
||||||
|
return [
|
||||||
|
'id' => 0,
|
||||||
|
'uuid' => 'default',
|
||||||
|
'name' => 'Livecam',
|
||||||
|
'slug' => 'default',
|
||||||
|
'status' => 'active',
|
||||||
|
'domain' => $domain,
|
||||||
|
'is_legacy' => true,
|
||||||
|
'branding' => [
|
||||||
|
'site_name' => 'Livecam',
|
||||||
|
'site_name_full' => 'Livecam',
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt das Branding des aktuellen Tenants zurück
|
||||||
|
*/
|
||||||
|
public function getBranding(): array
|
||||||
|
{
|
||||||
|
if ($this->currentBranding !== null) {
|
||||||
|
return $this->currentBranding;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = $this->resolve();
|
||||||
|
|
||||||
|
if (!$tenant) {
|
||||||
|
return $this->getDefaultBranding();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy-Tenant hat Branding inline
|
||||||
|
if (isset($tenant['is_legacy']) && $tenant['is_legacy']) {
|
||||||
|
$this->currentBranding = $tenant['branding'] ?? $this->getDefaultBranding();
|
||||||
|
return $this->currentBranding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aus DB laden
|
||||||
|
try {
|
||||||
|
$branding = $this->db->fetchOne(
|
||||||
|
"SELECT * FROM tenant_branding WHERE tenant_id = ?",
|
||||||
|
[$tenant['id']]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->currentBranding = $branding ?: $this->getDefaultBranding();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->currentBranding = $this->getDefaultBranding();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->currentBranding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Branding
|
||||||
|
*/
|
||||||
|
private function getDefaultBranding(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'site_name' => 'Livecam',
|
||||||
|
'site_name_full' => 'Live Webcam',
|
||||||
|
'tagline' => '',
|
||||||
|
'logo_path' => null,
|
||||||
|
'favicon_path' => null,
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
'accent_color' => '#f093fb',
|
||||||
|
'welcome_text_de' => '',
|
||||||
|
'welcome_text_en' => '',
|
||||||
|
'footer_text' => '',
|
||||||
|
'custom_css' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die aktuelle Domain zurück
|
||||||
|
*/
|
||||||
|
public function getCurrentDomain(): string
|
||||||
|
{
|
||||||
|
return $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisiert eine Domain (entfernt www.)
|
||||||
|
*/
|
||||||
|
private function normalizeDomain(string $domain): string
|
||||||
|
{
|
||||||
|
return preg_replace('/^www\./i', '', strtolower($domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob der aktuelle Tenant aktiv ist
|
||||||
|
*/
|
||||||
|
public function isActive(): bool
|
||||||
|
{
|
||||||
|
$tenant = $this->resolve();
|
||||||
|
return $tenant && in_array($tenant['status'], ['active', 'trial']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob der Tenant im Trial ist
|
||||||
|
*/
|
||||||
|
public function isTrial(): bool
|
||||||
|
{
|
||||||
|
$tenant = $this->resolve();
|
||||||
|
return $tenant && $tenant['status'] === 'trial';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die Tenant-ID zurück (oder 0 für Legacy)
|
||||||
|
*/
|
||||||
|
public function getTenantId(): int
|
||||||
|
{
|
||||||
|
$tenant = $this->resolve();
|
||||||
|
return $tenant['id'] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Tenant-Slug zurück
|
||||||
|
*/
|
||||||
|
public function getTenantSlug(): string
|
||||||
|
{
|
||||||
|
$tenant = $this->resolve();
|
||||||
|
return $tenant['slug'] ?? 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob Multi-Tenant-Modus aktiv ist (DB vorhanden)
|
||||||
|
*/
|
||||||
|
public function isMultiTenantEnabled(): bool
|
||||||
|
{
|
||||||
|
$tenant = $this->resolve();
|
||||||
|
return $tenant && !isset($tenant['is_legacy']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt alle Domains eines Tenants zurück
|
||||||
|
*/
|
||||||
|
public function getTenantDomains(int $tenantId): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->db->fetchAll(
|
||||||
|
"SELECT * FROM tenant_domains WHERE tenant_id = ? ORDER BY is_primary DESC",
|
||||||
|
[$tenantId]
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt den aktuellen Tenant manuell (für Tests oder CLI)
|
||||||
|
*/
|
||||||
|
public function setTenant(array $tenant): void
|
||||||
|
{
|
||||||
|
$this->currentTenant = $tenant;
|
||||||
|
$this->currentBranding = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leert den Cache
|
||||||
|
*/
|
||||||
|
public static function clearCache(): void
|
||||||
|
{
|
||||||
|
self::$domainCache = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bootstrap - Initialisiert die Multi-Tenant Umgebung
|
||||||
|
*
|
||||||
|
* Einbinden am Anfang von index.php:
|
||||||
|
* require_once __DIR__ . '/src/bootstrap.php';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Autoloader für src/ Klassen
|
||||||
|
spl_autoload_register(function ($class) {
|
||||||
|
// Namespace-Präfix
|
||||||
|
$prefix = 'AuroraLivecam\\';
|
||||||
|
$baseDir = __DIR__ . '/';
|
||||||
|
|
||||||
|
// Prüfe ob die Klasse unseren Namespace verwendet
|
||||||
|
$len = strlen($prefix);
|
||||||
|
if (strncmp($prefix, $class, $len) !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relativer Klassenname
|
||||||
|
$relativeClass = substr($class, $len);
|
||||||
|
|
||||||
|
// Pfad zur Datei
|
||||||
|
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
|
||||||
|
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require $file;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
use AuroraLivecam\Core\TenantResolver;
|
||||||
|
use AuroraLivecam\Core\Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die Site-Konfiguration basierend auf dem aktuellen Tenant zurück
|
||||||
|
* Ersetzt den hardcoded Domain-Switch in index.php
|
||||||
|
*/
|
||||||
|
function getSiteConfig(): array
|
||||||
|
{
|
||||||
|
// Legacy SettingsManager laden
|
||||||
|
$settingsFile = dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
if (!class_exists('SettingsManager') && file_exists($settingsFile)) {
|
||||||
|
require_once $settingsFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settingsManager = new \SettingsManager();
|
||||||
|
|
||||||
|
// Wenn Multi-Tenant nicht aktiviert, nutze Legacy-Modus
|
||||||
|
if (!$settingsManager->isMultiTenantEnabled()) {
|
||||||
|
return getLegacySiteConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-Tenant Modus
|
||||||
|
try {
|
||||||
|
$resolver = TenantResolver::getInstance();
|
||||||
|
$tenant = $resolver->resolve();
|
||||||
|
$branding = $resolver->getBranding();
|
||||||
|
|
||||||
|
if (!$tenant) {
|
||||||
|
return getLegacySiteConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tenant_id' => $tenant['id'],
|
||||||
|
'tenant_slug' => $tenant['slug'],
|
||||||
|
'is_multi_tenant' => true,
|
||||||
|
'site_name' => $branding['site_name'] ?? $tenant['name'],
|
||||||
|
'site_name_full' => $branding['site_name_full'] ?? $tenant['name'],
|
||||||
|
'tagline' => $branding['tagline'] ?? '',
|
||||||
|
'logo_path' => $branding['logo_path'] ?? null,
|
||||||
|
'favicon_path' => $branding['favicon_path'] ?? null,
|
||||||
|
'primary_color' => $branding['primary_color'] ?? '#667eea',
|
||||||
|
'secondary_color' => $branding['secondary_color'] ?? '#764ba2',
|
||||||
|
'accent_color' => $branding['accent_color'] ?? '#f093fb',
|
||||||
|
'welcome_de' => $branding['welcome_text_de'] ?? '',
|
||||||
|
'welcome_en' => $branding['welcome_text_en'] ?? '',
|
||||||
|
'footer_text' => $branding['footer_text'] ?? '',
|
||||||
|
'custom_css' => $branding['custom_css'] ?? '',
|
||||||
|
'social' => [
|
||||||
|
'facebook' => $branding['social_facebook'] ?? '',
|
||||||
|
'instagram' => $branding['social_instagram'] ?? '',
|
||||||
|
'youtube' => $branding['social_youtube'] ?? '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fallback auf Legacy bei Fehlern
|
||||||
|
return getLegacySiteConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy Site-Konfiguration (hardcoded Domains)
|
||||||
|
* Kompatibilität mit bestehendem Code
|
||||||
|
*/
|
||||||
|
function getLegacySiteConfig(): array
|
||||||
|
{
|
||||||
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
|
$isSeecam = (stripos($host, 'seecam.ch') !== false);
|
||||||
|
|
||||||
|
if ($isSeecam) {
|
||||||
|
return [
|
||||||
|
'tenant_id' => 0,
|
||||||
|
'tenant_slug' => 'seecam',
|
||||||
|
'is_multi_tenant' => false,
|
||||||
|
'site_name' => 'Seecam',
|
||||||
|
'site_name_full' => 'Seecam.ch - Live Webcam am See',
|
||||||
|
'tagline' => 'Ihre Live-Webcam am See',
|
||||||
|
'logo_path' => null,
|
||||||
|
'favicon_path' => null,
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
'accent_color' => '#f093fb',
|
||||||
|
'welcome_de' => 'Willkommen bei Seecam - Ihrer Live-Webcam am See!',
|
||||||
|
'welcome_en' => 'Welcome to Seecam - Your Live Webcam at the Lake!',
|
||||||
|
'footer_text' => '',
|
||||||
|
'custom_css' => '',
|
||||||
|
'social' => [
|
||||||
|
'facebook' => '',
|
||||||
|
'instagram' => '',
|
||||||
|
'youtube' => '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: Aurora
|
||||||
|
return [
|
||||||
|
'tenant_id' => 0,
|
||||||
|
'tenant_slug' => 'aurora',
|
||||||
|
'is_multi_tenant' => false,
|
||||||
|
'site_name' => 'Aurora',
|
||||||
|
'site_name_full' => 'Aurora Weather Livecam - Zürich Oberland',
|
||||||
|
'tagline' => 'Wetter Webcam Schweiz - Zürich Oberland',
|
||||||
|
'logo_path' => null,
|
||||||
|
'favicon_path' => null,
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
'accent_color' => '#f093fb',
|
||||||
|
'welcome_de' => 'Willkommen bei Aurora Weather Livecam - Ihre Wetter-Webcam im Zürcher Oberland mit AI-Erkennung für Aurora, Starlink und mehr!',
|
||||||
|
'welcome_en' => 'Welcome to Aurora Weather Livecam - Your weather webcam in the Zurich Oberland with AI detection for Aurora, Starlink and more!',
|
||||||
|
'footer_text' => '',
|
||||||
|
'custom_css' => '',
|
||||||
|
'social' => [
|
||||||
|
'facebook' => '',
|
||||||
|
'instagram' => '',
|
||||||
|
'youtube' => '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect Handler für alte Domains
|
||||||
|
*/
|
||||||
|
function handleDomainRedirects(): void
|
||||||
|
{
|
||||||
|
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||||
|
|
||||||
|
// Alte Aurora-Domains auf neue Domain umleiten
|
||||||
|
$oldDomains = [
|
||||||
|
'www.aurora-wetter-lifecam.ch',
|
||||||
|
'aurora-wetter-lifecam.ch',
|
||||||
|
'www.aurora-wetter-livecam.ch',
|
||||||
|
'aurora-wetter-livecam.ch'
|
||||||
|
];
|
||||||
|
|
||||||
|
$newDomain = 'www.aurora-weather-livecam.com';
|
||||||
|
|
||||||
|
if (in_array($host, $oldDomains)) {
|
||||||
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
header("HTTP/1.1 301 Moved Permanently");
|
||||||
|
header("Location: {$protocol}://{$newDomain}{$requestUri}");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain-Redirects automatisch ausführen
|
||||||
|
handleDomainRedirects();
|
||||||
Reference in New Issue
Block a user