Add tenant dashboard (Phase 2)
Dashboard Features: - Login page with session-based auth - Overview page with live stats (viewers, stream status) - Stream settings (URL, type configuration) - Branding editor (colors, texts, custom CSS) - Settings page (weather, content toggles, UI options) New Files: - dashboard/index.php: Main overview with stats - dashboard/login.php: Authentication page - dashboard/logout.php: Session cleanup - dashboard/stream.php: Stream configuration - dashboard/branding.php: Visual customization - dashboard/settings.php: Feature toggles - dashboard/templates/layout.php: Shared layout - dashboard/api/stats.php: Stats API endpoint - dashboard/assets/dashboard.css: Modern dashboard UI - dashboard/assets/dashboard.js: Client-side functionality - src/Auth/AuthManager.php: Secure auth with Argon2, remember-me Auth Features: - Secure password hashing (Argon2ID) - Remember-me tokens - Role-based access (super_admin, tenant_admin, tenant_user) - Legacy fallback for existing admin credentials
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard API - Stats
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__, 2) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__, 2) . '/src/bootstrap.php')) {
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
use AuroraLivecam\Auth\AuthManager;
|
||||||
|
use AuroraLivecam\Core\Database;
|
||||||
|
|
||||||
|
$auth = new AuthManager();
|
||||||
|
|
||||||
|
// Auth check
|
||||||
|
if (!$auth->isLoggedIn()) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $auth->getUser();
|
||||||
|
$tenantId = $user['tenant_id'] ?? 0;
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'viewers_current' => 0,
|
||||||
|
'viewers_today' => 0,
|
||||||
|
'viewers_peak' => 0,
|
||||||
|
'stream_status' => 'unknown',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Aktuelle Zuschauer aus Datei
|
||||||
|
$viewerFile = dirname(__DIR__, 2) . '/active_viewers.json';
|
||||||
|
if (file_exists($viewerFile)) {
|
||||||
|
$viewers = json_decode(file_get_contents($viewerFile), true);
|
||||||
|
$stats['viewers_current'] = count($viewers ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB Stats falls verfügbar
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
if ($tenantId > 0) {
|
||||||
|
$todayStats = $db->fetchOne(
|
||||||
|
"SELECT SUM(viewer_count) as total, MAX(viewer_count) as peak
|
||||||
|
FROM viewer_stats
|
||||||
|
WHERE tenant_id = ? AND DATE(recorded_at) = CURDATE()",
|
||||||
|
[$tenantId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($todayStats) {
|
||||||
|
$stats['viewers_today'] = (int)($todayStats['total'] ?? 0);
|
||||||
|
$stats['viewers_peak'] = (int)($todayStats['peak'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = $db->fetchOne(
|
||||||
|
"SELECT last_status FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1",
|
||||||
|
[$tenantId]
|
||||||
|
);
|
||||||
|
$stats['stream_status'] = $stream['last_status'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// DB nicht verfügbar - Stats bleiben auf Defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'stats' => $stats,
|
||||||
|
'timestamp' => time(),
|
||||||
|
]);
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
/* Dashboard CSS */
|
||||||
|
:root {
|
||||||
|
--primary: #667eea;
|
||||||
|
--primary-dark: #5a67d8;
|
||||||
|
--secondary: #764ba2;
|
||||||
|
--accent: #f093fb;
|
||||||
|
--success: #48bb78;
|
||||||
|
--warning: #ed8936;
|
||||||
|
--danger: #f56565;
|
||||||
|
--dark: #1a202c;
|
||||||
|
--gray-900: #1a202c;
|
||||||
|
--gray-800: #2d3748;
|
||||||
|
--gray-700: #4a5568;
|
||||||
|
--gray-600: #718096;
|
||||||
|
--gray-500: #a0aec0;
|
||||||
|
--gray-400: #cbd5e0;
|
||||||
|
--gray-300: #e2e8f0;
|
||||||
|
--gray-200: #edf2f7;
|
||||||
|
--gray-100: #f7fafc;
|
||||||
|
--white: #ffffff;
|
||||||
|
--sidebar-width: 260px;
|
||||||
|
--header-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--gray-800);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Container */
|
||||||
|
.dashboard-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
background: linear-gradient(180deg, var(--gray-900) 0%, var(--gray-800) 100%);
|
||||||
|
color: var(--white);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: fixed;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: var(--primary);
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
color: var(--gray-400);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: var(--gray-700);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--gray-700);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--gray-500);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
border-top: 1px solid var(--gray-700);
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.logout:hover {
|
||||||
|
background: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: var(--sidebar-width);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header {
|
||||||
|
height: var(--header-height);
|
||||||
|
background: var(--white);
|
||||||
|
border-bottom: 1px solid var(--gray-300);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 2rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--gray-200);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Grid */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--gray-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-change {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-change.positive { color: var(--success); }
|
||||||
|
.stat-change.negative { color: var(--danger); }
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-select,
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-select:focus,
|
||||||
|
.form-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--gray-500);
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--gray-200);
|
||||||
|
color: var(--gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: var(--success);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
.alert {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #c6f6d5;
|
||||||
|
color: #22543d;
|
||||||
|
border: 1px solid #9ae6b4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #742a2a;
|
||||||
|
border: 1px solid #feb2b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background: #feebc8;
|
||||||
|
color: #744210;
|
||||||
|
border: 1px solid #fbd38d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #bee3f8;
|
||||||
|
color: #2a4365;
|
||||||
|
border: 1px solid #90cdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background: var(--gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success { background: #c6f6d5; color: #22543d; }
|
||||||
|
.badge-warning { background: #feebc8; color: #744210; }
|
||||||
|
.badge-danger { background: #fed7d7; color: #742a2a; }
|
||||||
|
.badge-info { background: #bee3f8; color: #2a4365; }
|
||||||
|
|
||||||
|
/* Grid */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
|
||||||
|
/* Color Picker */
|
||||||
|
.color-picker-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker {
|
||||||
|
width: 50px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-value {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview Box */
|
||||||
|
.preview-box {
|
||||||
|
border: 2px dashed var(--gray-300);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch */
|
||||||
|
.toggle-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--gray-300);
|
||||||
|
border-radius: 24px;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:checked + .toggle-slider {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:checked + .toggle-slider:before {
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Page */
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background: var(--white);
|
||||||
|
padding: 2.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title p {
|
||||||
|
color: var(--gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-2,
|
||||||
|
.grid-3 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard JavaScript
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Auto-dismiss alerts after 5 seconds
|
||||||
|
const alerts = document.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(alert => {
|
||||||
|
setTimeout(() => {
|
||||||
|
alert.style.transition = 'opacity 0.3s';
|
||||||
|
alert.style.opacity = '0';
|
||||||
|
setTimeout(() => alert.remove(), 300);
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mobile sidebar toggle
|
||||||
|
const sidebar = document.querySelector('.sidebar');
|
||||||
|
const mainContent = document.querySelector('.main-content');
|
||||||
|
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
// Add menu button
|
||||||
|
const menuBtn = document.createElement('button');
|
||||||
|
menuBtn.className = 'btn btn-secondary';
|
||||||
|
menuBtn.style.cssText = 'position: fixed; top: 10px; left: 10px; z-index: 200; padding: 0.5rem;';
|
||||||
|
menuBtn.innerHTML = '☰';
|
||||||
|
menuBtn.onclick = () => sidebar.classList.toggle('open');
|
||||||
|
document.body.appendChild(menuBtn);
|
||||||
|
|
||||||
|
// Close sidebar on content click
|
||||||
|
mainContent.addEventListener('click', () => {
|
||||||
|
sidebar.classList.remove('open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color picker live preview
|
||||||
|
document.querySelectorAll('.color-picker').forEach(picker => {
|
||||||
|
picker.addEventListener('input', function() {
|
||||||
|
const wrapper = this.closest('.color-picker-wrapper');
|
||||||
|
if (wrapper) {
|
||||||
|
const valueDisplay = wrapper.querySelector('.color-value');
|
||||||
|
if (valueDisplay) {
|
||||||
|
valueDisplay.textContent = this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form unsaved changes warning
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
let formChanged = false;
|
||||||
|
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('change', () => {
|
||||||
|
formChanged = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', () => {
|
||||||
|
formChanged = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', (e) => {
|
||||||
|
if (formChanged) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stats refresh (every 30 seconds on overview page)
|
||||||
|
if (document.querySelector('.stats-grid')) {
|
||||||
|
setInterval(refreshStats, 30000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh stats via AJAX
|
||||||
|
*/
|
||||||
|
function refreshStats() {
|
||||||
|
fetch('/dashboard/api/stats.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
updateStatCard('viewers_current', data.stats.viewers_current);
|
||||||
|
updateStatCard('viewers_today', data.stats.viewers_today);
|
||||||
|
updateStatCard('viewers_peak', data.stats.viewers_peak);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.log('Stats refresh failed:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a stat card value
|
||||||
|
*/
|
||||||
|
function updateStatCard(id, value) {
|
||||||
|
const cards = document.querySelectorAll('.stat-card');
|
||||||
|
cards.forEach(card => {
|
||||||
|
const label = card.querySelector('.stat-label');
|
||||||
|
if (label) {
|
||||||
|
// Match by label text (simplified)
|
||||||
|
const valueEl = card.querySelector('.stat-value');
|
||||||
|
if (valueEl && typeof value !== 'undefined') {
|
||||||
|
valueEl.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification toast
|
||||||
|
*/
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `alert alert-${type}`;
|
||||||
|
toast.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 1000; min-width: 300px;';
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.transition = 'opacity 0.3s';
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm dangerous actions
|
||||||
|
*/
|
||||||
|
function confirmAction(message) {
|
||||||
|
return confirm(message || 'Sind Sie sicher?');
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard - Branding Einstellungen
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__) . '/src/bootstrap.php')) {
|
||||||
|
require_once dirname(__DIR__) . '/src/bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
use AuroraLivecam\Auth\AuthManager;
|
||||||
|
use AuroraLivecam\Core\Database;
|
||||||
|
use AuroraLivecam\Tenant\TenantManager;
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
$auth = new AuthManager();
|
||||||
|
$auth->requireLogin();
|
||||||
|
|
||||||
|
$user = $auth->getUser();
|
||||||
|
$tenantId = $user['tenant_id'] ?? 0;
|
||||||
|
|
||||||
|
$flashMessage = null;
|
||||||
|
$flashType = 'info';
|
||||||
|
|
||||||
|
// Branding-Daten laden
|
||||||
|
$branding = [
|
||||||
|
'site_name' => '',
|
||||||
|
'site_name_full' => '',
|
||||||
|
'tagline' => '',
|
||||||
|
'primary_color' => '#667eea',
|
||||||
|
'secondary_color' => '#764ba2',
|
||||||
|
'accent_color' => '#f093fb',
|
||||||
|
'welcome_text_de' => '',
|
||||||
|
'welcome_text_en' => '',
|
||||||
|
'footer_text' => '',
|
||||||
|
'custom_css' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
if ($tenantId > 0) {
|
||||||
|
$tenantManager = new TenantManager($db);
|
||||||
|
$dbBranding = $tenantManager->getBranding($tenantId);
|
||||||
|
if ($dbBranding) {
|
||||||
|
$branding = array_merge($branding, $dbBranding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// DB nicht verfügbar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formular verarbeiten
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$newBranding = [
|
||||||
|
'site_name' => trim($_POST['site_name'] ?? ''),
|
||||||
|
'site_name_full' => trim($_POST['site_name_full'] ?? ''),
|
||||||
|
'tagline' => trim($_POST['tagline'] ?? ''),
|
||||||
|
'primary_color' => $_POST['primary_color'] ?? '#667eea',
|
||||||
|
'secondary_color' => $_POST['secondary_color'] ?? '#764ba2',
|
||||||
|
'accent_color' => $_POST['accent_color'] ?? '#f093fb',
|
||||||
|
'welcome_text_de' => trim($_POST['welcome_text_de'] ?? ''),
|
||||||
|
'welcome_text_en' => trim($_POST['welcome_text_en'] ?? ''),
|
||||||
|
'footer_text' => trim($_POST['footer_text'] ?? ''),
|
||||||
|
'custom_css' => trim($_POST['custom_css'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
if ($tenantId > 0) {
|
||||||
|
$tenantManager = new TenantManager($db);
|
||||||
|
$tenantManager->updateBranding($tenantId, $newBranding);
|
||||||
|
|
||||||
|
$flashMessage = 'Branding gespeichert!';
|
||||||
|
$flashType = 'success';
|
||||||
|
$branding = array_merge($branding, $newBranding);
|
||||||
|
} else {
|
||||||
|
$flashMessage = 'Branding kann im Legacy-Modus nicht gespeichert werden.';
|
||||||
|
$flashType = 'warning';
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$flashMessage = 'Fehler beim Speichern: ' . $e->getMessage();
|
||||||
|
$flashType = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = 'Branding';
|
||||||
|
$currentPage = 'branding';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<form method="POST" action="">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<!-- Grundeinstellungen -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Grundeinstellungen</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="site_name">Site Name (kurz)</label>
|
||||||
|
<input type="text" id="site_name" name="site_name" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($branding['site_name']); ?>"
|
||||||
|
placeholder="MeineCam">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="site_name_full">Site Name (vollständig)</label>
|
||||||
|
<input type="text" id="site_name_full" name="site_name_full" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($branding['site_name_full']); ?>"
|
||||||
|
placeholder="Meine Wetter Livecam">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="tagline">Tagline / Slogan</label>
|
||||||
|
<input type="text" id="tagline" name="tagline" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($branding['tagline']); ?>"
|
||||||
|
placeholder="Ihre Live-Webcam 24/7">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Farben -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Farben</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Primärfarbe</label>
|
||||||
|
<div class="color-picker-wrapper">
|
||||||
|
<input type="color" name="primary_color" class="color-picker"
|
||||||
|
value="<?php echo htmlspecialchars($branding['primary_color']); ?>">
|
||||||
|
<span class="color-value"><?php echo htmlspecialchars($branding['primary_color']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Sekundärfarbe</label>
|
||||||
|
<div class="color-picker-wrapper">
|
||||||
|
<input type="color" name="secondary_color" class="color-picker"
|
||||||
|
value="<?php echo htmlspecialchars($branding['secondary_color']); ?>">
|
||||||
|
<span class="color-value"><?php echo htmlspecialchars($branding['secondary_color']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Akzentfarbe</label>
|
||||||
|
<div class="color-picker-wrapper">
|
||||||
|
<input type="color" name="accent_color" class="color-picker"
|
||||||
|
value="<?php echo htmlspecialchars($branding['accent_color']); ?>">
|
||||||
|
<span class="color-value"><?php echo htmlspecialchars($branding['accent_color']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vorschau -->
|
||||||
|
<div style="margin-top: 1rem; padding: 1rem; border-radius: 0.5rem;
|
||||||
|
background: linear-gradient(135deg, <?php echo htmlspecialchars($branding['primary_color']); ?> 0%, <?php echo htmlspecialchars($branding['secondary_color']); ?> 100%);">
|
||||||
|
<span style="color: white; font-weight: bold;">Farbvorschau</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Texte -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Willkommenstexte</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="welcome_text_de">Willkommenstext (Deutsch)</label>
|
||||||
|
<textarea id="welcome_text_de" name="welcome_text_de" class="form-textarea"
|
||||||
|
placeholder="Willkommen bei unserer Livecam..."><?php echo htmlspecialchars($branding['welcome_text_de']); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="welcome_text_en">Welcome Text (English)</label>
|
||||||
|
<textarea id="welcome_text_en" name="welcome_text_en" class="form-textarea"
|
||||||
|
placeholder="Welcome to our livecam..."><?php echo htmlspecialchars($branding['welcome_text_en']); ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="footer_text">Footer Text</label>
|
||||||
|
<input type="text" id="footer_text" name="footer_text" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($branding['footer_text']); ?>"
|
||||||
|
placeholder="© 2024 Ihre Livecam">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Eigenes CSS</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="custom_css">Custom CSS (optional)</label>
|
||||||
|
<textarea id="custom_css" name="custom_css" class="form-textarea"
|
||||||
|
style="font-family: monospace; min-height: 150px;"
|
||||||
|
placeholder="/* Eigene CSS-Regeln hier */"><?php echo htmlspecialchars($branding['custom_css']); ?></textarea>
|
||||||
|
<p class="form-help">Fortgeschrittene Benutzer können hier eigene CSS-Regeln hinzufügen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 1.5rem;">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Branding speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Color picker update
|
||||||
|
document.querySelectorAll('.color-picker').forEach(picker => {
|
||||||
|
picker.addEventListener('input', (e) => {
|
||||||
|
e.target.parentNode.querySelector('.color-value').textContent = e.target.value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
include __DIR__ . '/templates/layout.php';
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard - Übersicht
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__) . '/src/bootstrap.php')) {
|
||||||
|
require_once dirname(__DIR__) . '/src/bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
use AuroraLivecam\Auth\AuthManager;
|
||||||
|
use AuroraLivecam\Core\Database;
|
||||||
|
use AuroraLivecam\Core\TenantResolver;
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
$auth = new AuthManager();
|
||||||
|
|
||||||
|
// Login erforderlich
|
||||||
|
$auth->requireLogin();
|
||||||
|
|
||||||
|
$user = $auth->getUser();
|
||||||
|
$tenantId = $user['tenant_id'] ?? 0;
|
||||||
|
|
||||||
|
// Stats laden
|
||||||
|
$stats = [
|
||||||
|
'viewers_current' => 0,
|
||||||
|
'viewers_today' => 0,
|
||||||
|
'viewers_peak' => 0,
|
||||||
|
'stream_status' => 'unknown',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Versuche Stats aus DB zu laden
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
if ($tenantId > 0) {
|
||||||
|
// Aktuelle Zuschauer (vereinfacht)
|
||||||
|
$viewerFile = dirname(__DIR__) . '/active_viewers.json';
|
||||||
|
if (file_exists($viewerFile)) {
|
||||||
|
$viewers = json_decode(file_get_contents($viewerFile), true);
|
||||||
|
$stats['viewers_current'] = count($viewers ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heute Stats
|
||||||
|
$todayStats = $db->fetchOne(
|
||||||
|
"SELECT SUM(viewer_count) as total, MAX(viewer_count) as peak
|
||||||
|
FROM viewer_stats
|
||||||
|
WHERE tenant_id = ? AND DATE(recorded_at) = CURDATE()",
|
||||||
|
[$tenantId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($todayStats) {
|
||||||
|
$stats['viewers_today'] = $todayStats['total'] ?? 0;
|
||||||
|
$stats['viewers_peak'] = $todayStats['peak'] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream Status
|
||||||
|
$stream = $db->fetchOne(
|
||||||
|
"SELECT last_status FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1",
|
||||||
|
[$tenantId]
|
||||||
|
);
|
||||||
|
$stats['stream_status'] = $stream['last_status'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// DB nicht verfügbar - Legacy-Modus
|
||||||
|
$viewerFile = dirname(__DIR__) . '/active_viewers.json';
|
||||||
|
if (file_exists($viewerFile)) {
|
||||||
|
$viewers = json_decode(file_get_contents($viewerFile), true);
|
||||||
|
$stats['viewers_current'] = count($viewers ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page Setup
|
||||||
|
$pageTitle = 'Übersicht';
|
||||||
|
$currentPage = 'overview';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Stats Grid -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">👥</div>
|
||||||
|
<div class="stat-value"><?php echo $stats['viewers_current']; ?></div>
|
||||||
|
<div class="stat-label">Aktuelle Zuschauer</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">📊</div>
|
||||||
|
<div class="stat-value"><?php echo $stats['viewers_today']; ?></div>
|
||||||
|
<div class="stat-label">Zuschauer heute</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">🏆</div>
|
||||||
|
<div class="stat-value"><?php echo $stats['viewers_peak']; ?></div>
|
||||||
|
<div class="stat-label">Peak heute</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<?php echo $stats['stream_status'] === 'online' ? '🟢' : ($stats['stream_status'] === 'offline' ? '🔴' : '⚪'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value" style="font-size: 1.25rem; text-transform: capitalize;">
|
||||||
|
<?php echo $stats['stream_status'] === 'online' ? 'Online' : ($stats['stream_status'] === 'offline' ? 'Offline' : 'Unbekannt'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">Stream Status</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Schnellzugriff</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid grid-3">
|
||||||
|
<a href="/dashboard/stream.php" class="btn btn-secondary">
|
||||||
|
📹 Stream bearbeiten
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/branding.php" class="btn btn-secondary">
|
||||||
|
🎨 Branding anpassen
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/settings.php" class="btn btn-secondary">
|
||||||
|
⚙️ Einstellungen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity (Platzhalter) -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Letzte Aktivitäten</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p style="color: var(--gray-500); text-align: center; padding: 2rem;">
|
||||||
|
Aktivitäten werden hier angezeigt, sobald Analytics aktiviert ist.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
include __DIR__ . '/templates/layout.php';
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard Login
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__) . '/src/bootstrap.php')) {
|
||||||
|
require_once dirname(__DIR__) . '/src/bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
use AuroraLivecam\Auth\AuthManager;
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
|
||||||
|
// Prüfe ob Dashboard aktiviert ist
|
||||||
|
if (!$settingsManager->isTenantDashboardEnabled() && !$settingsManager->isMultiTenantEnabled()) {
|
||||||
|
// Fallback auf Legacy-Admin
|
||||||
|
header('Location: /?admin=1');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$auth = new AuthManager();
|
||||||
|
|
||||||
|
// Bereits eingeloggt?
|
||||||
|
if ($auth->isLoggedIn()) {
|
||||||
|
header('Location: /dashboard/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
// Login verarbeiten
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$email = $_POST['email'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$remember = isset($_POST['remember']);
|
||||||
|
|
||||||
|
if ($auth->login($email, $password, $remember)) {
|
||||||
|
header('Location: /dashboard/');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = 'Ungültige Anmeldedaten';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-title">
|
||||||
|
<h1>Dashboard Login</h1>
|
||||||
|
<p>Melden Sie sich an, um fortzufahren</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="email">E-Mail / Benutzername</label>
|
||||||
|
<input type="text" id="email" name="email" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($_POST['email'] ?? ''); ?>"
|
||||||
|
required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="password">Passwort</label>
|
||||||
|
<input type="password" id="password" name="password" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="remember">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>Angemeldet bleiben</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%;">
|
||||||
|
Anmelden
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style="text-align: center; margin-top: 1.5rem; color: var(--gray-500);">
|
||||||
|
<a href="/" style="color: var(--primary);">Zurück zur Livecam</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard Logout
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__) . '/src/bootstrap.php')) {
|
||||||
|
require_once dirname(__DIR__) . '/src/bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
use AuroraLivecam\Auth\AuthManager;
|
||||||
|
|
||||||
|
$auth = new AuthManager();
|
||||||
|
$auth->logout();
|
||||||
|
|
||||||
|
header('Location: /dashboard/login.php');
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard - Einstellungen
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__) . '/src/bootstrap.php')) {
|
||||||
|
require_once dirname(__DIR__) . '/src/bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
use AuroraLivecam\Auth\AuthManager;
|
||||||
|
use AuroraLivecam\Tenant\TenantSettingsManager;
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
$auth = new AuthManager();
|
||||||
|
$auth->requireLogin();
|
||||||
|
|
||||||
|
$user = $auth->getUser();
|
||||||
|
$tenantId = $user['tenant_id'] ?? 0;
|
||||||
|
|
||||||
|
$flashMessage = null;
|
||||||
|
$flashType = 'info';
|
||||||
|
|
||||||
|
// Tenant-Settings laden
|
||||||
|
try {
|
||||||
|
$tenantSettings = new TenantSettingsManager($tenantId);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$tenantSettings = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einstellungen für das Template
|
||||||
|
$settings = [
|
||||||
|
'viewer_display_enabled' => $settingsManager->get('viewer_display.enabled') ?? true,
|
||||||
|
'viewer_min' => $settingsManager->get('viewer_display.min_viewers') ?? 1,
|
||||||
|
'weather_enabled' => $settingsManager->get('weather.enabled') ?? true,
|
||||||
|
'weather_location' => $settingsManager->get('weather.location') ?? 'Zürich,CH',
|
||||||
|
'weather_lat' => $settingsManager->get('weather.lat') ?? '47.3769',
|
||||||
|
'weather_lon' => $settingsManager->get('weather.lon') ?? '8.5417',
|
||||||
|
'guestbook_enabled' => $settingsManager->get('content.guestbook_enabled') ?? true,
|
||||||
|
'gallery_enabled' => $settingsManager->get('content.gallery_enabled') ?? true,
|
||||||
|
'ai_events_enabled' => $settingsManager->get('content.ai_events_enabled') ?? true,
|
||||||
|
'show_qr_code' => $settingsManager->get('ui_display.show_qr_code') ?? true,
|
||||||
|
'show_social_media' => $settingsManager->get('ui_display.show_social_media') ?? true,
|
||||||
|
'timelapse_reverse' => $settingsManager->get('zoom_timelapse.timelapse_reverse_enabled') ?? true,
|
||||||
|
'max_zoom' => $settingsManager->get('zoom_timelapse.max_zoom_level') ?? 4.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Formular verarbeiten
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$updates = [
|
||||||
|
'viewer_display.enabled' => isset($_POST['viewer_display_enabled']),
|
||||||
|
'viewer_display.min_viewers' => (int)($_POST['viewer_min'] ?? 1),
|
||||||
|
'weather.enabled' => isset($_POST['weather_enabled']),
|
||||||
|
'weather.location' => trim($_POST['weather_location'] ?? ''),
|
||||||
|
'weather.lat' => trim($_POST['weather_lat'] ?? ''),
|
||||||
|
'weather.lon' => trim($_POST['weather_lon'] ?? ''),
|
||||||
|
'content.guestbook_enabled' => isset($_POST['guestbook_enabled']),
|
||||||
|
'content.gallery_enabled' => isset($_POST['gallery_enabled']),
|
||||||
|
'content.ai_events_enabled' => isset($_POST['ai_events_enabled']),
|
||||||
|
'ui_display.show_qr_code' => isset($_POST['show_qr_code']),
|
||||||
|
'ui_display.show_social_media' => isset($_POST['show_social_media']),
|
||||||
|
'zoom_timelapse.timelapse_reverse_enabled' => isset($_POST['timelapse_reverse']),
|
||||||
|
'zoom_timelapse.max_zoom_level' => (float)($_POST['max_zoom'] ?? 4.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
$success = true;
|
||||||
|
foreach ($updates as $key => $value) {
|
||||||
|
if (!$settingsManager->set($key, $value)) {
|
||||||
|
$success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$flashMessage = 'Einstellungen gespeichert!';
|
||||||
|
$flashType = 'success';
|
||||||
|
|
||||||
|
// Reload settings
|
||||||
|
$settings = [
|
||||||
|
'viewer_display_enabled' => $updates['viewer_display.enabled'],
|
||||||
|
'viewer_min' => $updates['viewer_display.min_viewers'],
|
||||||
|
'weather_enabled' => $updates['weather.enabled'],
|
||||||
|
'weather_location' => $updates['weather.location'],
|
||||||
|
'weather_lat' => $updates['weather.lat'],
|
||||||
|
'weather_lon' => $updates['weather.lon'],
|
||||||
|
'guestbook_enabled' => $updates['content.guestbook_enabled'],
|
||||||
|
'gallery_enabled' => $updates['content.gallery_enabled'],
|
||||||
|
'ai_events_enabled' => $updates['content.ai_events_enabled'],
|
||||||
|
'show_qr_code' => $updates['ui_display.show_qr_code'],
|
||||||
|
'show_social_media' => $updates['ui_display.show_social_media'],
|
||||||
|
'timelapse_reverse' => $updates['zoom_timelapse.timelapse_reverse_enabled'],
|
||||||
|
'max_zoom' => $updates['zoom_timelapse.max_zoom_level'],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$flashMessage = 'Fehler beim Speichern einiger Einstellungen.';
|
||||||
|
$flashType = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = 'Einstellungen';
|
||||||
|
$currentPage = 'settings';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<form method="POST" action="">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<!-- Viewer-Anzeige -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Zuschauer-Anzeige</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="viewer_display_enabled"
|
||||||
|
<?php echo $settings['viewer_display_enabled'] ? 'checked' : ''; ?>>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>Zuschauer-Anzahl anzeigen</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="viewer_min">Mindestanzahl für Anzeige</label>
|
||||||
|
<input type="number" id="viewer_min" name="viewer_min" class="form-input"
|
||||||
|
value="<?php echo (int)$settings['viewer_min']; ?>" min="0" max="100">
|
||||||
|
<p class="form-help">Zuschauer werden erst ab dieser Anzahl angezeigt</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wetter-Widget -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Wetter-Widget</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="weather_enabled"
|
||||||
|
<?php echo $settings['weather_enabled'] ? 'checked' : ''; ?>>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>Wetter-Widget aktivieren</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="weather_location">Standort-Name</label>
|
||||||
|
<input type="text" id="weather_location" name="weather_location" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($settings['weather_location']); ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="weather_lat">Breitengrad</label>
|
||||||
|
<input type="text" id="weather_lat" name="weather_lat" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($settings['weather_lat']); ?>">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="weather_lon">Längengrad</label>
|
||||||
|
<input type="text" id="weather_lon" name="weather_lon" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($settings['weather_lon']); ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Inhalte</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="guestbook_enabled"
|
||||||
|
<?php echo $settings['guestbook_enabled'] ? 'checked' : ''; ?>>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>Gästebuch aktivieren</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="gallery_enabled"
|
||||||
|
<?php echo $settings['gallery_enabled'] ? 'checked' : ''; ?>>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>Galerie aktivieren</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="ai_events_enabled"
|
||||||
|
<?php echo $settings['ai_events_enabled'] ? 'checked' : ''; ?>>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>AI-Events aktivieren</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UI -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Oberfläche</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="show_qr_code"
|
||||||
|
<?php echo $settings['show_qr_code'] ? 'checked' : ''; ?>>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>QR-Code anzeigen</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="show_social_media"
|
||||||
|
<?php echo $settings['show_social_media'] ? 'checked' : ''; ?>>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>Social Media Links anzeigen</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-wrapper">
|
||||||
|
<span class="toggle">
|
||||||
|
<input type="checkbox" name="timelapse_reverse"
|
||||||
|
<?php echo $settings['timelapse_reverse'] ? 'checked' : ''; ?>>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</span>
|
||||||
|
<span>Timelapse Rückwärts erlauben</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="max_zoom">Maximaler Zoom</label>
|
||||||
|
<input type="number" id="max_zoom" name="max_zoom" class="form-input"
|
||||||
|
value="<?php echo (float)$settings['max_zoom']; ?>" min="1" max="10" step="0.5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 1.5rem;">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Einstellungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
include __DIR__ . '/templates/layout.php';
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard - Stream Einstellungen
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
require_once dirname(__DIR__) . '/SettingsManager.php';
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__) . '/src/bootstrap.php')) {
|
||||||
|
require_once dirname(__DIR__) . '/src/bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
use AuroraLivecam\Auth\AuthManager;
|
||||||
|
use AuroraLivecam\Core\Database;
|
||||||
|
|
||||||
|
$settingsManager = new SettingsManager();
|
||||||
|
$auth = new AuthManager();
|
||||||
|
$auth->requireLogin();
|
||||||
|
|
||||||
|
$user = $auth->getUser();
|
||||||
|
$tenantId = $user['tenant_id'] ?? 0;
|
||||||
|
|
||||||
|
$flashMessage = null;
|
||||||
|
$flashType = 'info';
|
||||||
|
|
||||||
|
// Stream-Daten laden
|
||||||
|
$stream = [
|
||||||
|
'stream_url' => '',
|
||||||
|
'stream_type' => 'hls',
|
||||||
|
'is_active' => true,
|
||||||
|
'last_status' => 'unknown',
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
if ($tenantId > 0) {
|
||||||
|
$dbStream = $db->fetchOne(
|
||||||
|
"SELECT * FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1",
|
||||||
|
[$tenantId]
|
||||||
|
);
|
||||||
|
if ($dbStream) {
|
||||||
|
$stream = $dbStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// DB nicht verfügbar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formular verarbeiten
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$streamUrl = trim($_POST['stream_url'] ?? '');
|
||||||
|
$streamType = $_POST['stream_type'] ?? 'hls';
|
||||||
|
|
||||||
|
if (empty($streamUrl)) {
|
||||||
|
$flashMessage = 'Bitte geben Sie eine Stream-URL ein.';
|
||||||
|
$flashType = 'error';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
if ($tenantId > 0) {
|
||||||
|
// Prüfe ob Stream existiert
|
||||||
|
$existing = $db->fetchOne(
|
||||||
|
"SELECT id FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1",
|
||||||
|
[$tenantId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$db->update('tenant_streams', [
|
||||||
|
'stream_url' => $streamUrl,
|
||||||
|
'stream_type' => $streamType,
|
||||||
|
], 'id = ?', [$existing['id']]);
|
||||||
|
} else {
|
||||||
|
$db->insert('tenant_streams', [
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'stream_url' => $streamUrl,
|
||||||
|
'stream_type' => $streamType,
|
||||||
|
'is_primary' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$flashMessage = 'Stream-Einstellungen gespeichert!';
|
||||||
|
$flashType = 'success';
|
||||||
|
|
||||||
|
// Reload stream data
|
||||||
|
$stream['stream_url'] = $streamUrl;
|
||||||
|
$stream['stream_type'] = $streamType;
|
||||||
|
} else {
|
||||||
|
$flashMessage = 'Stream-Einstellungen können im Legacy-Modus nicht gespeichert werden.';
|
||||||
|
$flashType = 'warning';
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$flashMessage = 'Fehler beim Speichern: ' . $e->getMessage();
|
||||||
|
$flashType = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = 'Stream Einstellungen';
|
||||||
|
$currentPage = 'stream';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Stream Konfiguration</h3>
|
||||||
|
<span class="badge badge-<?php echo $stream['last_status'] === 'online' ? 'success' : ($stream['last_status'] === 'offline' ? 'danger' : 'info'); ?>">
|
||||||
|
<?php echo ucfirst($stream['last_status'] ?? 'Unbekannt'); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="stream_url">Stream URL</label>
|
||||||
|
<input type="url" id="stream_url" name="stream_url" class="form-input"
|
||||||
|
value="<?php echo htmlspecialchars($stream['stream_url']); ?>"
|
||||||
|
placeholder="https://example.com/stream.m3u8">
|
||||||
|
<p class="form-help">Die URL zu Ihrem HLS-Stream (.m3u8) oder RTMP-Stream</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="stream_type">Stream Typ</label>
|
||||||
|
<select id="stream_type" name="stream_type" class="form-select">
|
||||||
|
<option value="hls" <?php echo ($stream['stream_type'] ?? 'hls') === 'hls' ? 'selected' : ''; ?>>
|
||||||
|
HLS (.m3u8)
|
||||||
|
</option>
|
||||||
|
<option value="rtmp" <?php echo ($stream['stream_type'] ?? '') === 'rtmp' ? 'selected' : ''; ?>>
|
||||||
|
RTMP
|
||||||
|
</option>
|
||||||
|
<option value="webrtc" <?php echo ($stream['stream_type'] ?? '') === 'webrtc' ? 'selected' : ''; ?>>
|
||||||
|
WebRTC
|
||||||
|
</option>
|
||||||
|
<option value="iframe" <?php echo ($stream['stream_type'] ?? '') === 'iframe' ? 'selected' : ''; ?>>
|
||||||
|
iFrame Embed
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Stream Vorschau</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (!empty($stream['stream_url'])): ?>
|
||||||
|
<div style="aspect-ratio: 16/9; background: #000; border-radius: 0.5rem; overflow: hidden;">
|
||||||
|
<video id="preview-player" controls style="width: 100%; height: 100%;">
|
||||||
|
<source src="<?php echo htmlspecialchars($stream['stream_url']); ?>" type="application/x-mpegURL">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<p class="form-help" style="margin-top: 1rem;">
|
||||||
|
Hinweis: Die Vorschau funktioniert nur mit HLS-Streams und wenn Ihr Browser HLS unterstützt.
|
||||||
|
</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="preview-box">
|
||||||
|
<p>Keine Stream-URL konfiguriert</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Stream Monitoring</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p style="color: var(--gray-500);">
|
||||||
|
Stream-Monitoring zeigt automatische Verfügbarkeitsprüfungen an.
|
||||||
|
Diese Funktion wird demnächst verfügbar sein.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
include __DIR__ . '/templates/layout.php';
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard Layout Template
|
||||||
|
*
|
||||||
|
* Variablen:
|
||||||
|
* - $pageTitle: Seitentitel
|
||||||
|
* - $currentPage: Aktuelle Seite (für Navigation)
|
||||||
|
* - $content: Hauptinhalt
|
||||||
|
*/
|
||||||
|
|
||||||
|
$user = $auth->getUser();
|
||||||
|
$tenantName = $user['tenant_name'] ?? 'Dashboard';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php echo htmlspecialchars($pageTitle ?? 'Dashboard'); ?> - <?php echo htmlspecialchars($tenantName); ?></title>
|
||||||
|
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h2><?php echo htmlspecialchars($tenantName); ?></h2>
|
||||||
|
<span class="role-badge"><?php echo htmlspecialchars($user['role'] ?? 'user'); ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="/dashboard/" class="nav-item <?php echo ($currentPage ?? '') === 'overview' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">📊</span>
|
||||||
|
<span>Übersicht</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/dashboard/stream.php" class="nav-item <?php echo ($currentPage ?? '') === 'stream' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">📹</span>
|
||||||
|
<span>Stream</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/dashboard/branding.php" class="nav-item <?php echo ($currentPage ?? '') === 'branding' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">🎨</span>
|
||||||
|
<span>Branding</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/dashboard/settings.php" class="nav-item <?php echo ($currentPage ?? '') === 'settings' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">⚙️</span>
|
||||||
|
<span>Einstellungen</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php if ($settingsManager->isAnalyticsEnabled()): ?>
|
||||||
|
<a href="/dashboard/analytics.php" class="nav-item <?php echo ($currentPage ?? '') === 'analytics' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">📈</span>
|
||||||
|
<span>Analytics</span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($settingsManager->isCustomDomainEnabled()): ?>
|
||||||
|
<a href="/dashboard/domains.php" class="nav-item <?php echo ($currentPage ?? '') === 'domains' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">🌐</span>
|
||||||
|
<span>Domains</span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($settingsManager->isBillingEnabled()): ?>
|
||||||
|
<a href="/dashboard/billing.php" class="nav-item <?php echo ($currentPage ?? '') === 'billing' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">💳</span>
|
||||||
|
<span>Abrechnung</span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($auth->isSuperAdmin()): ?>
|
||||||
|
<div class="nav-divider"></div>
|
||||||
|
<span class="nav-label">Admin</span>
|
||||||
|
|
||||||
|
<a href="/dashboard/admin/tenants.php" class="nav-item <?php echo ($currentPage ?? '') === 'admin-tenants' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">👥</span>
|
||||||
|
<span>Kunden</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/dashboard/admin/plans.php" class="nav-item <?php echo ($currentPage ?? '') === 'admin-plans' ? 'active' : ''; ?>">
|
||||||
|
<span class="nav-icon">📋</span>
|
||||||
|
<span>Pläne</span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<a href="/" class="nav-item" target="_blank">
|
||||||
|
<span class="nav-icon">🔗</span>
|
||||||
|
<span>Zur Livecam</span>
|
||||||
|
</a>
|
||||||
|
<a href="/dashboard/logout.php" class="nav-item logout">
|
||||||
|
<span class="nav-icon">🚪</span>
|
||||||
|
<span>Abmelden</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<header class="main-header">
|
||||||
|
<h1><?php echo htmlspecialchars($pageTitle ?? 'Dashboard'); ?></h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<span class="user-info">
|
||||||
|
<?php echo htmlspecialchars($user['email'] ?? ''); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<?php if (isset($flashMessage)): ?>
|
||||||
|
<div class="alert alert-<?php echo $flashType ?? 'info'; ?>">
|
||||||
|
<?php echo htmlspecialchars($flashMessage); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php echo $content ?? ''; ?>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/dashboard/assets/dashboard.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AuthManager - Sichere Authentifizierung für Dashboard
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AuroraLivecam\Auth;
|
||||||
|
|
||||||
|
use AuroraLivecam\Core\Database;
|
||||||
|
|
||||||
|
class AuthManager
|
||||||
|
{
|
||||||
|
private Database $db;
|
||||||
|
private bool $dbAvailable = false;
|
||||||
|
|
||||||
|
public function __construct(?Database $db = null)
|
||||||
|
{
|
||||||
|
$this->db = $db ?? Database::getInstance();
|
||||||
|
$this->checkDbAvailability();
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkDbAvailability(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->db->fetchOne("SELECT 1 FROM users LIMIT 1");
|
||||||
|
$this->dbAvailable = true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->dbAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registriert einen neuen Benutzer
|
||||||
|
*/
|
||||||
|
public function register(array $data): int
|
||||||
|
{
|
||||||
|
if (!$this->dbAvailable) {
|
||||||
|
throw new \Exception('Database not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validierung
|
||||||
|
if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new \Exception('Invalid email address');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data['password']) || strlen($data['password']) < 8) {
|
||||||
|
throw new \Exception('Password must be at least 8 characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Email bereits existiert
|
||||||
|
$existing = $this->db->fetchOne("SELECT id FROM users WHERE email = ?", [$data['email']]);
|
||||||
|
if ($existing) {
|
||||||
|
throw new \Exception('Email already registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benutzer erstellen
|
||||||
|
return $this->db->insert('users', [
|
||||||
|
'tenant_id' => $data['tenant_id'] ?? null,
|
||||||
|
'email' => strtolower($data['email']),
|
||||||
|
'password_hash' => password_hash($data['password'], PASSWORD_ARGON2ID),
|
||||||
|
'name' => $data['name'] ?? null,
|
||||||
|
'role' => $data['role'] ?? 'tenant_user',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login mit Email und Passwort
|
||||||
|
*/
|
||||||
|
public function login(string $email, string $password, bool $remember = false): bool
|
||||||
|
{
|
||||||
|
// Legacy-Modus (hardcoded admin)
|
||||||
|
if (!$this->dbAvailable) {
|
||||||
|
return $this->legacyLogin($email, $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->db->fetchOne(
|
||||||
|
"SELECT u.*, t.name as tenant_name, t.slug as tenant_slug
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN tenants t ON u.tenant_id = t.id
|
||||||
|
WHERE u.email = ?",
|
||||||
|
[strtolower($email)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session setzen
|
||||||
|
$this->setSession($user);
|
||||||
|
|
||||||
|
// Last login aktualisieren
|
||||||
|
$this->db->update('users', ['last_login_at' => date('Y-m-d H:i:s')], 'id = ?', [$user['id']]);
|
||||||
|
|
||||||
|
// Remember-Me Cookie
|
||||||
|
if ($remember) {
|
||||||
|
$this->setRememberToken($user['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy Login (kompatibel mit altem AdminManager)
|
||||||
|
*/
|
||||||
|
private function legacyLogin(string $email, string $password): bool
|
||||||
|
{
|
||||||
|
// Alte hardcoded Credentials als Fallback
|
||||||
|
if ($email === 'admin' && $password === 'sonne4000$$$$Q') {
|
||||||
|
$_SESSION['admin'] = true;
|
||||||
|
$_SESSION['user'] = [
|
||||||
|
'id' => 0,
|
||||||
|
'email' => 'admin',
|
||||||
|
'name' => 'Administrator',
|
||||||
|
'role' => 'super_admin',
|
||||||
|
'tenant_id' => null,
|
||||||
|
];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die Session-Daten
|
||||||
|
*/
|
||||||
|
private function setSession(array $user): void
|
||||||
|
{
|
||||||
|
$_SESSION['admin'] = true; // Kompatibilität mit Legacy
|
||||||
|
$_SESSION['user'] = [
|
||||||
|
'id' => $user['id'],
|
||||||
|
'email' => $user['email'],
|
||||||
|
'name' => $user['name'],
|
||||||
|
'role' => $user['role'],
|
||||||
|
'tenant_id' => $user['tenant_id'],
|
||||||
|
'tenant_name' => $user['tenant_name'] ?? null,
|
||||||
|
'tenant_slug' => $user['tenant_slug'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt Remember-Me Token
|
||||||
|
*/
|
||||||
|
private function setRememberToken(int $userId): void
|
||||||
|
{
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$hashedToken = hash('sha256', $token);
|
||||||
|
|
||||||
|
$this->db->update('users', ['remember_token' => $hashedToken], 'id = ?', [$userId]);
|
||||||
|
|
||||||
|
setcookie('remember_token', $token, [
|
||||||
|
'expires' => time() + (86400 * 30), // 30 Tage
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => true,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft Remember-Me Cookie
|
||||||
|
*/
|
||||||
|
public function checkRememberToken(): bool
|
||||||
|
{
|
||||||
|
if (!isset($_COOKIE['remember_token']) || !$this->dbAvailable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashedToken = hash('sha256', $_COOKIE['remember_token']);
|
||||||
|
|
||||||
|
$user = $this->db->fetchOne(
|
||||||
|
"SELECT u.*, t.name as tenant_name, t.slug as tenant_slug
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN tenants t ON u.tenant_id = t.id
|
||||||
|
WHERE u.remember_token = ?",
|
||||||
|
[$hashedToken]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$this->setSession($user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout
|
||||||
|
*/
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
// Remember-Token löschen
|
||||||
|
if ($this->isLoggedIn() && $this->dbAvailable) {
|
||||||
|
$userId = $_SESSION['user']['id'] ?? 0;
|
||||||
|
if ($userId > 0) {
|
||||||
|
$this->db->update('users', ['remember_token' => null], 'id = ?', [$userId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie löschen
|
||||||
|
setcookie('remember_token', '', [
|
||||||
|
'expires' => time() - 3600,
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => true,
|
||||||
|
'httponly' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Session zerstören
|
||||||
|
$_SESSION = [];
|
||||||
|
if (ini_get("session.use_cookies")) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(session_name(), '', time() - 42000,
|
||||||
|
$params["path"], $params["domain"],
|
||||||
|
$params["secure"], $params["httponly"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob User eingeloggt ist
|
||||||
|
*/
|
||||||
|
public function isLoggedIn(): bool
|
||||||
|
{
|
||||||
|
return isset($_SESSION['admin']) && $_SESSION['admin'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den aktuellen User zurück
|
||||||
|
*/
|
||||||
|
public function getUser(): ?array
|
||||||
|
{
|
||||||
|
return $_SESSION['user'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob User eine bestimmte Rolle hat
|
||||||
|
*/
|
||||||
|
public function hasRole(string $role): bool
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
return $user && $user['role'] === $role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob User Super-Admin ist
|
||||||
|
*/
|
||||||
|
public function isSuperAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole('super_admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob User Tenant-Admin ist
|
||||||
|
*/
|
||||||
|
public function isTenantAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole('tenant_admin') || $this->hasRole('super_admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die Tenant-ID des aktuellen Users zurück
|
||||||
|
*/
|
||||||
|
public function getTenantId(): ?int
|
||||||
|
{
|
||||||
|
$user = $this->getUser();
|
||||||
|
return $user ? ($user['tenant_id'] ?? null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob User Zugriff auf einen bestimmten Tenant hat
|
||||||
|
*/
|
||||||
|
public function canAccessTenant(int $tenantId): bool
|
||||||
|
{
|
||||||
|
if ($this->isSuperAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getTenantId() === $tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ändert das Passwort
|
||||||
|
*/
|
||||||
|
public function changePassword(int $userId, string $currentPassword, string $newPassword): bool
|
||||||
|
{
|
||||||
|
if (!$this->dbAvailable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->db->fetchOne("SELECT password_hash FROM users WHERE id = ?", [$userId]);
|
||||||
|
|
||||||
|
if (!$user || !password_verify($currentPassword, $user['password_hash'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($newPassword) < 8) {
|
||||||
|
throw new \Exception('Password must be at least 8 characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->db->update('users', [
|
||||||
|
'password_hash' => password_hash($newPassword, PASSWORD_ARGON2ID)
|
||||||
|
], 'id = ?', [$userId]) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert ein Passwort-Reset-Token
|
||||||
|
*/
|
||||||
|
public function generateResetToken(string $email): ?string
|
||||||
|
{
|
||||||
|
if (!$this->dbAvailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->db->fetchOne("SELECT id FROM users WHERE email = ?", [strtolower($email)]);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return null; // Keine Info leaken ob Email existiert
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
// Token würde normalerweise in separater Tabelle mit Ablaufzeit gespeichert
|
||||||
|
// Für jetzt: vereinfachte Version
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware: Erfordert Login
|
||||||
|
*/
|
||||||
|
public function requireLogin(): void
|
||||||
|
{
|
||||||
|
if (!$this->isLoggedIn()) {
|
||||||
|
if (!$this->checkRememberToken()) {
|
||||||
|
header('Location: /dashboard/login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware: Erfordert bestimmte Rolle
|
||||||
|
*/
|
||||||
|
public function requireRole(string $role): void
|
||||||
|
{
|
||||||
|
$this->requireLogin();
|
||||||
|
|
||||||
|
if (!$this->hasRole($role) && !$this->isSuperAdmin()) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo "Access denied";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user