Add automatic onboarding system (Phase 3)
Onboarding Wizard: - register.php: User registration with validation - verify.php: Email verification (with demo mode) - stream.php: Stream URL configuration & validation - branding.php: Quick branding setup with live preview - complete.php: Success page with confetti animation Backend Classes (src/Onboarding/): - OnboardingManager.php: Orchestrates the onboarding flow - Registration with automatic subdomain generation - Email verification tokens - Step tracking in tenant_onboarding table - StreamValidator.php: Validates stream URLs - HLS (.m3u8) validation with playlist check - RTMP format validation - iframe/embed URL detection (YouTube, Vimeo, Twitch) - Generic HTTP reachability check Features: - 4-step wizard with progress indicator - Stream type auto-detection - Live branding preview - Skip options for optional steps - Trial period display
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
/**
|
||||
* Onboarding - Branding (Schritt 4)
|
||||
*/
|
||||
|
||||
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\Onboarding\OnboardingManager;
|
||||
|
||||
$settingsManager = new SettingsManager();
|
||||
$auth = new AuthManager();
|
||||
|
||||
if (!$auth->isLoggedIn()) {
|
||||
header('Location: /onboarding/register.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = $auth->getUser();
|
||||
$tenantId = $user['tenant_id'] ?? 0;
|
||||
|
||||
$error = '';
|
||||
$branding = [
|
||||
'site_name' => $user['tenant_name'] ?? '',
|
||||
'tagline' => '',
|
||||
'primary_color' => '#667eea',
|
||||
'secondary_color' => '#764ba2',
|
||||
];
|
||||
|
||||
// Formular verarbeiten
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$branding = [
|
||||
'site_name' => trim($_POST['site_name'] ?? ''),
|
||||
'site_name_full' => trim($_POST['site_name'] ?? ''),
|
||||
'tagline' => trim($_POST['tagline'] ?? ''),
|
||||
'primary_color' => $_POST['primary_color'] ?? '#667eea',
|
||||
'secondary_color' => $_POST['secondary_color'] ?? '#764ba2',
|
||||
];
|
||||
|
||||
try {
|
||||
$onboarding = new OnboardingManager();
|
||||
$result = $onboarding->saveBranding($tenantId, $branding);
|
||||
|
||||
if ($result['success']) {
|
||||
header('Location: /onboarding/complete.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['error'] ?? 'Fehler beim Speichern';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$error = 'Fehler: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Skip
|
||||
if (isset($_GET['skip'])) {
|
||||
header('Location: /onboarding/complete.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Branding - Aurora Livecam</title>
|
||||
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
|
||||
<style>
|
||||
.onboarding-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
padding: 2rem;
|
||||
}
|
||||
.onboarding-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: 600px;
|
||||
}
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.step {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--gray-300);
|
||||
}
|
||||
.step.active { background: var(--primary); }
|
||||
.step.completed { background: var(--success); }
|
||||
.onboarding-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.onboarding-header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.color-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.preview-card {
|
||||
margin-top: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
.preview-header {
|
||||
padding: 1.5rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
.preview-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.preview-header p {
|
||||
margin: 0.5rem 0 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.preview-body {
|
||||
padding: 1rem;
|
||||
background: var(--gray-100);
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
.skip-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--gray-500);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="onboarding-container">
|
||||
<div class="onboarding-box">
|
||||
<div class="progress-steps">
|
||||
<div class="step completed"></div>
|
||||
<div class="step completed"></div>
|
||||
<div class="step completed"></div>
|
||||
<div class="step active"></div>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-header">
|
||||
<h1>🎨 Branding</h1>
|
||||
<p style="color: var(--gray-500);">Personalisieren Sie Ihre Livecam</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="site_name">Name Ihrer Livecam</label>
|
||||
<input type="text" id="site_name" name="site_name" class="form-input"
|
||||
value="<?php echo htmlspecialchars($branding['site_name']); ?>"
|
||||
placeholder="z.B. Berghütte Webcam">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="tagline">Slogan / Beschreibung</label>
|
||||
<input type="text" id="tagline" name="tagline" class="form-input"
|
||||
value="<?php echo htmlspecialchars($branding['tagline']); ?>"
|
||||
placeholder="z.B. Live aus den Schweizer Alpen">
|
||||
</div>
|
||||
|
||||
<div class="color-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Primärfarbe</label>
|
||||
<div class="color-picker-wrapper">
|
||||
<input type="color" name="primary_color" id="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" id="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>
|
||||
|
||||
<!-- Live Preview -->
|
||||
<div class="preview-card">
|
||||
<div class="preview-header" id="preview-header" style="background: linear-gradient(135deg, <?php echo htmlspecialchars($branding['primary_color']); ?> 0%, <?php echo htmlspecialchars($branding['secondary_color']); ?> 100%);">
|
||||
<h3 id="preview-name"><?php echo htmlspecialchars($branding['site_name'] ?: 'Ihre Livecam'); ?></h3>
|
||||
<p id="preview-tagline"><?php echo htmlspecialchars($branding['tagline'] ?: 'Ihr Slogan hier'); ?></p>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
Live-Vorschau
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1.5rem;">
|
||||
Speichern & abschliessen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a href="?skip=1" class="skip-link">
|
||||
Später anpassen →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Live preview updates
|
||||
document.getElementById('site_name').addEventListener('input', (e) => {
|
||||
document.getElementById('preview-name').textContent = e.target.value || 'Ihre Livecam';
|
||||
});
|
||||
|
||||
document.getElementById('tagline').addEventListener('input', (e) => {
|
||||
document.getElementById('preview-tagline').textContent = e.target.value || 'Ihr Slogan hier';
|
||||
});
|
||||
|
||||
document.getElementById('primary_color').addEventListener('input', updateColors);
|
||||
document.getElementById('secondary_color').addEventListener('input', updateColors);
|
||||
|
||||
function updateColors() {
|
||||
const primary = document.getElementById('primary_color').value;
|
||||
const secondary = document.getElementById('secondary_color').value;
|
||||
document.getElementById('preview-header').style.background =
|
||||
`linear-gradient(135deg, ${primary} 0%, ${secondary} 100%)`;
|
||||
|
||||
document.querySelectorAll('.color-value')[0].textContent = primary;
|
||||
document.querySelectorAll('.color-value')[1].textContent = secondary;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
/**
|
||||
* Onboarding - Abgeschlossen
|
||||
*/
|
||||
|
||||
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\Onboarding\OnboardingManager;
|
||||
use AuroraLivecam\Core\Database;
|
||||
|
||||
$settingsManager = new SettingsManager();
|
||||
$auth = new AuthManager();
|
||||
|
||||
if (!$auth->isLoggedIn()) {
|
||||
header('Location: /onboarding/register.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = $auth->getUser();
|
||||
$tenantId = $user['tenant_id'] ?? 0;
|
||||
|
||||
// Onboarding abschliessen
|
||||
try {
|
||||
$onboarding = new OnboardingManager();
|
||||
$onboarding->complete($tenantId);
|
||||
} catch (\Exception $e) {
|
||||
// Ignorieren wenn DB nicht verfügbar
|
||||
}
|
||||
|
||||
// Tenant-Info laden
|
||||
$tenantSlug = 'demo';
|
||||
$subdomain = '';
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$tenant = $db->fetchOne("SELECT slug FROM tenants WHERE id = ?", [$tenantId]);
|
||||
if ($tenant) {
|
||||
$tenantSlug = $tenant['slug'];
|
||||
$subdomain = $tenantSlug . '.aurora-livecam.com';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Fallback
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fertig! - Aurora Livecam</title>
|
||||
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
|
||||
<style>
|
||||
.complete-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
padding: 2rem;
|
||||
}
|
||||
.complete-box {
|
||||
background: var(--white);
|
||||
padding: 3rem;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
}
|
||||
.complete-icon {
|
||||
font-size: 5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
animation: bounce 0.5s ease;
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
.complete-box h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--success);
|
||||
}
|
||||
.complete-box p {
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.url-box {
|
||||
background: var(--gray-100);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.url-box label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.url-box .url {
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
color: var(--primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.next-steps {
|
||||
margin-top: 2.5rem;
|
||||
text-align: left;
|
||||
background: var(--gray-50);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.next-steps h3 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
.next-steps ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.next-steps li {
|
||||
padding: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
.next-steps li::before {
|
||||
content: '→';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--primary);
|
||||
}
|
||||
.confetti {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
.confetti-piece {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--primary);
|
||||
animation: confetti-fall 3s ease-out forwards;
|
||||
}
|
||||
@keyframes confetti-fall {
|
||||
0% { transform: translateY(-100px) rotate(0deg); opacity: 1; }
|
||||
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="confetti" id="confetti"></div>
|
||||
|
||||
<div class="complete-container">
|
||||
<div class="complete-box">
|
||||
<div class="complete-icon">🎉</div>
|
||||
<h1>Herzlichen Glückwunsch!</h1>
|
||||
<p>Ihre Livecam ist jetzt eingerichtet und bereit.</p>
|
||||
|
||||
<?php if ($subdomain): ?>
|
||||
<div class="url-box">
|
||||
<label>Ihre Livecam-Adresse:</label>
|
||||
<div class="url">https://<?php echo htmlspecialchars($subdomain); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="action-buttons">
|
||||
<a href="/dashboard/" class="btn btn-primary">
|
||||
Zum Dashboard
|
||||
</a>
|
||||
<a href="/" class="btn btn-secondary" target="_blank">
|
||||
Livecam ansehen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="next-steps">
|
||||
<h3>Nächste Schritte</h3>
|
||||
<ul>
|
||||
<li>Stream-URL im Dashboard anpassen (falls noch nicht geschehen)</li>
|
||||
<li>Logo und Farben im Branding-Bereich hochladen</li>
|
||||
<li>Wetter-Widget konfigurieren</li>
|
||||
<li>Eigene Domain verbinden (optional)</li>
|
||||
<?php if ($settingsManager->isBillingEnabled()): ?>
|
||||
<li>Abo auswählen für mehr Funktionen</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Confetti Animation
|
||||
function createConfetti() {
|
||||
const container = document.getElementById('confetti');
|
||||
const colors = ['#667eea', '#764ba2', '#f093fb', '#48bb78', '#ed8936'];
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const piece = document.createElement('div');
|
||||
piece.className = 'confetti-piece';
|
||||
piece.style.left = Math.random() * 100 + '%';
|
||||
piece.style.background = colors[Math.floor(Math.random() * colors.length)];
|
||||
piece.style.animationDelay = Math.random() * 2 + 's';
|
||||
piece.style.width = (Math.random() * 10 + 5) + 'px';
|
||||
piece.style.height = piece.style.width;
|
||||
container.appendChild(piece);
|
||||
}
|
||||
|
||||
// Cleanup after animation
|
||||
setTimeout(() => {
|
||||
container.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
createConfetti();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
/**
|
||||
* Onboarding - Registrierung
|
||||
*/
|
||||
|
||||
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\Onboarding\OnboardingManager;
|
||||
use AuroraLivecam\Auth\AuthManager;
|
||||
|
||||
$settingsManager = new SettingsManager();
|
||||
|
||||
// Prüfe ob Self-Registration aktiviert ist
|
||||
if (!$settingsManager->isSelfRegistrationEnabled()) {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$auth = new AuthManager();
|
||||
|
||||
// Bereits eingeloggt?
|
||||
if ($auth->isLoggedIn()) {
|
||||
header('Location: /dashboard/');
|
||||
exit;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$formData = [];
|
||||
$success = false;
|
||||
|
||||
// Formular verarbeiten
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$formData = [
|
||||
'name' => trim($_POST['name'] ?? ''),
|
||||
'company_name' => trim($_POST['company_name'] ?? ''),
|
||||
'email' => trim($_POST['email'] ?? ''),
|
||||
'password' => $_POST['password'] ?? '',
|
||||
'password_confirm' => $_POST['password_confirm'] ?? '',
|
||||
'stream_url' => trim($_POST['stream_url'] ?? ''),
|
||||
'accept_terms' => isset($_POST['accept_terms']),
|
||||
];
|
||||
|
||||
try {
|
||||
$onboarding = new OnboardingManager();
|
||||
$result = $onboarding->register($formData);
|
||||
|
||||
if ($result['success']) {
|
||||
// Session starten und User einloggen
|
||||
$auth->login($formData['email'], $formData['password']);
|
||||
|
||||
// Zur nächsten Seite weiterleiten
|
||||
if ($onboarding->requiresEmailVerification()) {
|
||||
// Token für Demo-Zwecke in Session speichern
|
||||
$_SESSION['verification_token'] = $result['verification_token'];
|
||||
header('Location: /onboarding/verify.php');
|
||||
} else {
|
||||
header('Location: /onboarding/stream.php');
|
||||
}
|
||||
exit;
|
||||
} else {
|
||||
$errors = $result['errors'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors['general'] = 'Registrierung fehlgeschlagen: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$trialDays = $settingsManager->getTrialDays();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Registrierung - Aurora Livecam</title>
|
||||
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
|
||||
<style>
|
||||
.register-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
padding: 2rem;
|
||||
}
|
||||
.register-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: 500px;
|
||||
}
|
||||
.register-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.register-header h1 {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.register-header p {
|
||||
color: var(--gray-500);
|
||||
}
|
||||
.trial-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, var(--success) 0%, #38a169 100%);
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.error-text {
|
||||
color: var(--danger);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.input-error {
|
||||
border-color: var(--danger) !important;
|
||||
}
|
||||
.terms-text {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
.terms-text a {
|
||||
color: var(--primary);
|
||||
}
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 1.5rem 0;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--gray-300);
|
||||
}
|
||||
.divider span {
|
||||
padding: 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="register-container">
|
||||
<div class="register-box">
|
||||
<div class="register-header">
|
||||
<h1>Jetzt starten</h1>
|
||||
<p>Erstellen Sie Ihre eigene Live-Webcam</p>
|
||||
<span class="trial-badge"><?php echo $trialDays; ?> Tage kostenlos testen</span>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['general'])): ?>
|
||||
<div class="alert alert-error"><?php echo htmlspecialchars($errors['general']); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="" novalidate>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="name">Ihr Name *</label>
|
||||
<input type="text" id="name" name="name" class="form-input <?php echo isset($errors['name']) ? 'input-error' : ''; ?>"
|
||||
value="<?php echo htmlspecialchars($formData['name'] ?? ''); ?>" required>
|
||||
<?php if (isset($errors['name'])): ?>
|
||||
<p class="error-text"><?php echo htmlspecialchars($errors['name']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="company_name">Webcam / Firma *</label>
|
||||
<input type="text" id="company_name" name="company_name" class="form-input <?php echo isset($errors['company_name']) ? 'input-error' : ''; ?>"
|
||||
value="<?php echo htmlspecialchars($formData['company_name'] ?? ''); ?>"
|
||||
placeholder="z.B. Berghütte Webcam" required>
|
||||
<?php if (isset($errors['company_name'])): ?>
|
||||
<p class="error-text"><?php echo htmlspecialchars($errors['company_name']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">E-Mail-Adresse *</label>
|
||||
<input type="email" id="email" name="email" class="form-input <?php echo isset($errors['email']) ? 'input-error' : ''; ?>"
|
||||
value="<?php echo htmlspecialchars($formData['email'] ?? ''); ?>" required>
|
||||
<?php if (isset($errors['email'])): ?>
|
||||
<p class="error-text"><?php echo htmlspecialchars($errors['email']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Passwort *</label>
|
||||
<input type="password" id="password" name="password" class="form-input <?php echo isset($errors['password']) ? 'input-error' : ''; ?>"
|
||||
minlength="8" required>
|
||||
<?php if (isset($errors['password'])): ?>
|
||||
<p class="error-text"><?php echo htmlspecialchars($errors['password']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password_confirm">Passwort bestätigen *</label>
|
||||
<input type="password" id="password_confirm" name="password_confirm" class="form-input <?php echo isset($errors['password_confirm']) ? 'input-error' : ''; ?>"
|
||||
required>
|
||||
<?php if (isset($errors['password_confirm'])): ?>
|
||||
<p class="error-text"><?php echo htmlspecialchars($errors['password_confirm']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"><span>Optional</span></div>
|
||||
|
||||
<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 <?php echo isset($errors['stream_url']) ? 'input-error' : ''; ?>"
|
||||
value="<?php echo htmlspecialchars($formData['stream_url'] ?? ''); ?>"
|
||||
placeholder="https://example.com/stream.m3u8">
|
||||
<p class="form-help">Sie können die Stream-URL auch später im Dashboard hinzufügen</p>
|
||||
<?php if (isset($errors['stream_url'])): ?>
|
||||
<p class="error-text"><?php echo htmlspecialchars($errors['stream_url']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="toggle-wrapper">
|
||||
<input type="checkbox" name="accept_terms" <?php echo !empty($formData['accept_terms']) ? 'checked' : ''; ?> required>
|
||||
<span class="terms-text">
|
||||
Ich akzeptiere die <a href="/terms" target="_blank">AGB</a> und
|
||||
<a href="/privacy" target="_blank">Datenschutzerklärung</a> *
|
||||
</span>
|
||||
</label>
|
||||
<?php if (isset($errors['accept_terms'])): ?>
|
||||
<p class="error-text"><?php echo htmlspecialchars($errors['accept_terms']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1rem;">
|
||||
Kostenlos registrieren
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align: center; margin-top: 1.5rem; color: var(--gray-500);">
|
||||
Bereits registriert?
|
||||
<a href="/dashboard/login.php" style="color: var(--primary);">Anmelden</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
/**
|
||||
* Onboarding - Stream Konfiguration (Schritt 3)
|
||||
*/
|
||||
|
||||
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\Onboarding\OnboardingManager;
|
||||
use AuroraLivecam\Onboarding\StreamValidator;
|
||||
|
||||
$settingsManager = new SettingsManager();
|
||||
$auth = new AuthManager();
|
||||
|
||||
// Login prüfen
|
||||
if (!$auth->isLoggedIn()) {
|
||||
header('Location: /onboarding/register.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = $auth->getUser();
|
||||
$tenantId = $user['tenant_id'] ?? 0;
|
||||
|
||||
$error = '';
|
||||
$streamUrl = '';
|
||||
$streamType = 'hls';
|
||||
$validationResult = null;
|
||||
|
||||
// Formular verarbeiten
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$streamUrl = trim($_POST['stream_url'] ?? '');
|
||||
$streamType = $_POST['stream_type'] ?? 'hls';
|
||||
|
||||
if (empty($streamUrl)) {
|
||||
$error = 'Bitte geben Sie eine Stream-URL ein';
|
||||
} else {
|
||||
try {
|
||||
// Stream validieren
|
||||
$validator = new StreamValidator();
|
||||
$validationResult = $validator->validate($streamUrl);
|
||||
|
||||
if ($validationResult['valid']) {
|
||||
// Speichern
|
||||
$onboarding = new OnboardingManager();
|
||||
$result = $onboarding->saveStream($tenantId, $streamUrl, $streamType);
|
||||
|
||||
if ($result['success']) {
|
||||
header('Location: /onboarding/branding.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['error'];
|
||||
}
|
||||
} else {
|
||||
$error = $validationResult['error'] ?? 'Stream-URL konnte nicht validiert werden';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$error = 'Fehler: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip erlauben
|
||||
if (isset($_GET['skip'])) {
|
||||
header('Location: /onboarding/branding.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stream einrichten - Aurora Livecam</title>
|
||||
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
|
||||
<style>
|
||||
.onboarding-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
padding: 2rem;
|
||||
}
|
||||
.onboarding-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: 600px;
|
||||
}
|
||||
.onboarding-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.onboarding-header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.step {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--gray-300);
|
||||
}
|
||||
.step.active { background: var(--primary); }
|
||||
.step.completed { background: var(--success); }
|
||||
.validation-result {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.validation-success {
|
||||
background: #c6f6d5;
|
||||
border: 1px solid #9ae6b4;
|
||||
}
|
||||
.validation-error {
|
||||
background: #fed7d7;
|
||||
border: 1px solid #feb2b2;
|
||||
}
|
||||
.validation-details {
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
.stream-types {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.stream-type-card {
|
||||
border: 2px solid var(--gray-200);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.stream-type-card:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.stream-type-card.selected {
|
||||
border-color: var(--primary);
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
.stream-type-card input {
|
||||
display: none;
|
||||
}
|
||||
.stream-type-card h4 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.stream-type-card p {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
.skip-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--gray-500);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="onboarding-container">
|
||||
<div class="onboarding-box">
|
||||
<div class="progress-steps">
|
||||
<div class="step completed"></div>
|
||||
<div class="step completed"></div>
|
||||
<div class="step active"></div>
|
||||
<div class="step"></div>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-header">
|
||||
<h1>📹 Stream einrichten</h1>
|
||||
<p style="color: var(--gray-500);">Verbinden Sie Ihre Webcam oder Ihren Stream</p>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="" id="stream-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Stream-Typ wählen</label>
|
||||
<div class="stream-types">
|
||||
<label class="stream-type-card <?php echo $streamType === 'hls' ? 'selected' : ''; ?>">
|
||||
<input type="radio" name="stream_type" value="hls" <?php echo $streamType === 'hls' ? 'checked' : ''; ?>>
|
||||
<h4>🎬 HLS Stream</h4>
|
||||
<p>.m3u8 Playlist (empfohlen)</p>
|
||||
</label>
|
||||
<label class="stream-type-card <?php echo $streamType === 'rtmp' ? 'selected' : ''; ?>">
|
||||
<input type="radio" name="stream_type" value="rtmp" <?php echo $streamType === 'rtmp' ? 'checked' : ''; ?>>
|
||||
<h4>📡 RTMP</h4>
|
||||
<p>Real-Time Messaging Protocol</p>
|
||||
</label>
|
||||
<label class="stream-type-card <?php echo $streamType === 'iframe' ? 'selected' : ''; ?>">
|
||||
<input type="radio" name="stream_type" value="iframe" <?php echo $streamType === 'iframe' ? 'checked' : ''; ?>>
|
||||
<h4>🖼️ Embed</h4>
|
||||
<p>YouTube, Vimeo, Twitch</p>
|
||||
</label>
|
||||
<label class="stream-type-card <?php echo $streamType === 'webrtc' ? 'selected' : ''; ?>">
|
||||
<input type="radio" name="stream_type" value="webrtc" <?php echo $streamType === 'webrtc' ? 'checked' : ''; ?>>
|
||||
<h4>⚡ WebRTC</h4>
|
||||
<p>Ultra-niedrige Latenz</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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($streamUrl); ?>"
|
||||
placeholder="https://example.com/stream.m3u8" required>
|
||||
<p class="form-help">Die vollständige URL zu Ihrem Stream</p>
|
||||
</div>
|
||||
|
||||
<?php if ($validationResult): ?>
|
||||
<div class="validation-result <?php echo $validationResult['valid'] ? 'validation-success' : 'validation-error'; ?>">
|
||||
<strong><?php echo $validationResult['valid'] ? '✓ Stream erreichbar' : '✗ Stream nicht erreichbar'; ?></strong>
|
||||
<?php if (!empty($validationResult['details'])): ?>
|
||||
<div class="validation-details">
|
||||
<?php if (isset($validationResult['details']['detected_type'])): ?>
|
||||
Erkannter Typ: <?php echo htmlspecialchars($validationResult['details']['detected_type']); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1.5rem;">
|
||||
Stream testen & weiter
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a href="?skip=1" class="skip-link">
|
||||
Später einrichten →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.stream-type-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
document.querySelectorAll('.stream-type-card').forEach(c => c.classList.remove('selected'));
|
||||
card.classList.add('selected');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
/**
|
||||
* Onboarding - E-Mail Verifizierung
|
||||
*/
|
||||
|
||||
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\Onboarding\OnboardingManager;
|
||||
|
||||
$settingsManager = new SettingsManager();
|
||||
$auth = new AuthManager();
|
||||
|
||||
// Login prüfen
|
||||
if (!$auth->isLoggedIn()) {
|
||||
header('Location: /onboarding/register.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = $auth->getUser();
|
||||
$message = '';
|
||||
$error = '';
|
||||
$verified = false;
|
||||
|
||||
// Token aus URL verarbeiten
|
||||
if (isset($_GET['token'])) {
|
||||
try {
|
||||
$onboarding = new OnboardingManager();
|
||||
$result = $onboarding->verifyEmail($_GET['token']);
|
||||
|
||||
if ($result['success']) {
|
||||
$verified = true;
|
||||
$message = 'E-Mail erfolgreich verifiziert!';
|
||||
} else {
|
||||
$error = $result['error'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$error = 'Verifikation fehlgeschlagen';
|
||||
}
|
||||
}
|
||||
|
||||
// E-Mail erneut senden
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['resend'])) {
|
||||
try {
|
||||
$onboarding = new OnboardingManager();
|
||||
$result = $onboarding->resendVerification($user['id']);
|
||||
|
||||
if ($result['success']) {
|
||||
$_SESSION['verification_token'] = $result['token'];
|
||||
$message = 'Verifikations-E-Mail wurde erneut gesendet!';
|
||||
} else {
|
||||
$error = $result['error'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$error = 'Fehler beim Senden';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo: Token anzeigen (in Produktion würde eine E-Mail gesendet)
|
||||
$demoToken = $_SESSION['verification_token'] ?? null;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>E-Mail verifizieren - Aurora Livecam</title>
|
||||
<link rel="stylesheet" href="/dashboard/assets/dashboard.css">
|
||||
<style>
|
||||
.verify-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
padding: 2rem;
|
||||
}
|
||||
.verify-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: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
.verify-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.verify-box h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.verify-box p {
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.email-highlight {
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
.demo-box {
|
||||
background: var(--gray-100);
|
||||
border: 1px dashed var(--gray-300);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
text-align: left;
|
||||
}
|
||||
.demo-box h4 {
|
||||
font-size: 0.875rem;
|
||||
color: var(--warning);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.demo-link {
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.step {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--gray-300);
|
||||
}
|
||||
.step.active {
|
||||
background: var(--primary);
|
||||
}
|
||||
.step.completed {
|
||||
background: var(--success);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="verify-container">
|
||||
<div class="verify-box">
|
||||
<div class="progress-steps">
|
||||
<div class="step completed"></div>
|
||||
<div class="step active"></div>
|
||||
<div class="step"></div>
|
||||
<div class="step"></div>
|
||||
</div>
|
||||
|
||||
<?php if ($verified): ?>
|
||||
<div class="verify-icon">✅</div>
|
||||
<h1>E-Mail verifiziert!</h1>
|
||||
<p>Ihre E-Mail-Adresse wurde erfolgreich bestätigt.</p>
|
||||
<a href="/onboarding/stream.php" class="btn btn-primary" style="width: 100%;">
|
||||
Weiter zur Stream-Konfiguration
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<div class="verify-icon">📧</div>
|
||||
<h1>E-Mail bestätigen</h1>
|
||||
<p>
|
||||
Wir haben eine Bestätigungs-E-Mail an<br>
|
||||
<span class="email-highlight"><?php echo htmlspecialchars($user['email'] ?? ''); ?></span><br>
|
||||
gesendet.
|
||||
</p>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success"><?php echo htmlspecialchars($message); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($demoToken): ?>
|
||||
<div class="demo-box">
|
||||
<h4>⚠️ Demo-Modus</h4>
|
||||
<p style="font-size: 0.875rem; margin: 0;">In der Produktion würde eine E-Mail gesendet. Für Demo-Zwecke:</p>
|
||||
<a href="/onboarding/verify.php?token=<?php echo urlencode($demoToken); ?>" class="demo-link">
|
||||
Klicken Sie hier um zu verifizieren
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p style="margin-top: 1.5rem; color: var(--gray-500); font-size: 0.875rem;">
|
||||
Keine E-Mail erhalten?
|
||||
</p>
|
||||
|
||||
<form method="POST" action="" style="display: inline;">
|
||||
<button type="submit" name="resend" class="btn btn-secondary">
|
||||
Erneut senden
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<p style="margin-top: 2rem;">
|
||||
<a href="/dashboard/logout.php" style="color: var(--gray-500); font-size: 0.875rem;">
|
||||
Abmelden
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
/**
|
||||
* OnboardingManager - Verwaltet den Onboarding-Prozess
|
||||
*/
|
||||
|
||||
namespace AuroraLivecam\Onboarding;
|
||||
|
||||
use AuroraLivecam\Core\Database;
|
||||
use AuroraLivecam\Tenant\TenantManager;
|
||||
use AuroraLivecam\Auth\AuthManager;
|
||||
|
||||
class OnboardingManager
|
||||
{
|
||||
private Database $db;
|
||||
private TenantManager $tenantManager;
|
||||
private StreamValidator $streamValidator;
|
||||
|
||||
public const STEP_REGISTER = 1;
|
||||
public const STEP_VERIFY_EMAIL = 2;
|
||||
public const STEP_STREAM = 3;
|
||||
public const STEP_BRANDING = 4;
|
||||
public const STEP_COMPLETE = 5;
|
||||
|
||||
public function __construct(?Database $db = null)
|
||||
{
|
||||
$this->db = $db ?? Database::getInstance();
|
||||
$this->tenantManager = new TenantManager($this->db);
|
||||
$this->streamValidator = new StreamValidator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet den Onboarding-Prozess (Registrierung)
|
||||
*/
|
||||
public function register(array $data): array
|
||||
{
|
||||
$errors = $this->validateRegistration($data);
|
||||
|
||||
if (!empty($errors)) {
|
||||
return ['success' => false, 'errors' => $errors];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db->beginTransaction();
|
||||
|
||||
// Tenant erstellen
|
||||
$tenantId = $this->tenantManager->create([
|
||||
'name' => $data['company_name'] ?? $data['name'],
|
||||
'email' => $data['email'],
|
||||
'subdomain' => $this->generateSubdomain($data['company_name'] ?? $data['name']),
|
||||
'stream_url' => $data['stream_url'] ?? '',
|
||||
'stream_type' => $data['stream_type'] ?? 'hls',
|
||||
]);
|
||||
|
||||
// Admin-User für den Tenant erstellen
|
||||
$auth = new AuthManager($this->db);
|
||||
$userId = $auth->register([
|
||||
'tenant_id' => $tenantId,
|
||||
'email' => $data['email'],
|
||||
'password' => $data['password'],
|
||||
'name' => $data['name'],
|
||||
'role' => 'tenant_admin',
|
||||
]);
|
||||
|
||||
// Verification-Token generieren
|
||||
$verificationToken = $this->generateVerificationToken($userId);
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $userId,
|
||||
'verification_token' => $verificationToken,
|
||||
'next_step' => self::STEP_VERIFY_EMAIL,
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->db->rollback();
|
||||
return ['success' => false, 'errors' => ['general' => $e->getMessage()]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Registrierungsdaten
|
||||
*/
|
||||
private function validateRegistration(array $data): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Name
|
||||
if (empty($data['name'])) {
|
||||
$errors['name'] = 'Name ist erforderlich';
|
||||
}
|
||||
|
||||
// Company/Site Name
|
||||
if (empty($data['company_name'])) {
|
||||
$errors['company_name'] = 'Firmen-/Site-Name ist erforderlich';
|
||||
}
|
||||
|
||||
// Email
|
||||
if (empty($data['email'])) {
|
||||
$errors['email'] = 'E-Mail ist erforderlich';
|
||||
} elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$errors['email'] = 'Ungültige E-Mail-Adresse';
|
||||
} else {
|
||||
// Prüfe ob Email bereits existiert
|
||||
$existing = $this->db->fetchOne("SELECT id FROM users WHERE email = ?", [strtolower($data['email'])]);
|
||||
if ($existing) {
|
||||
$errors['email'] = 'Diese E-Mail-Adresse ist bereits registriert';
|
||||
}
|
||||
}
|
||||
|
||||
// Password
|
||||
if (empty($data['password'])) {
|
||||
$errors['password'] = 'Passwort ist erforderlich';
|
||||
} elseif (strlen($data['password']) < 8) {
|
||||
$errors['password'] = 'Passwort muss mindestens 8 Zeichen lang sein';
|
||||
}
|
||||
|
||||
// Password Confirmation
|
||||
if (($data['password'] ?? '') !== ($data['password_confirm'] ?? '')) {
|
||||
$errors['password_confirm'] = 'Passwörter stimmen nicht überein';
|
||||
}
|
||||
|
||||
// Stream URL (optional, aber wenn angegeben, validieren)
|
||||
if (!empty($data['stream_url'])) {
|
||||
$validation = $this->streamValidator->validate($data['stream_url']);
|
||||
if (!$validation['valid']) {
|
||||
$errors['stream_url'] = $validation['error'] ?? 'Stream-URL ungültig';
|
||||
}
|
||||
}
|
||||
|
||||
// Terms
|
||||
if (empty($data['accept_terms'])) {
|
||||
$errors['accept_terms'] = 'Sie müssen die AGB akzeptieren';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine Subdomain aus dem Firmennamen
|
||||
*/
|
||||
private function generateSubdomain(string $name): string
|
||||
{
|
||||
// Umlaute ersetzen
|
||||
$replacements = ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss'];
|
||||
$slug = str_replace(array_keys($replacements), array_values($replacements), strtolower($name));
|
||||
|
||||
// Nur alphanumerische Zeichen und Bindestriche
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
|
||||
$slug = trim($slug, '-');
|
||||
|
||||
// Max 30 Zeichen
|
||||
$slug = substr($slug, 0, 30);
|
||||
|
||||
// Eindeutigkeit prüfen
|
||||
$baseSlug = $slug;
|
||||
$counter = 1;
|
||||
while (!$this->tenantManager->isDomainAvailable($slug . '.aurora-livecam.com')) {
|
||||
$slug = $baseSlug . '-' . $counter;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen E-Mail-Verification-Token
|
||||
*/
|
||||
private function generateVerificationToken(int $userId): string
|
||||
{
|
||||
$token = bin2hex(random_bytes(32));
|
||||
|
||||
// Token in einer separaten Tabelle speichern (oder im User)
|
||||
// Vereinfacht: Wir nutzen remember_token temporär
|
||||
$this->db->update('users', ['remember_token' => hash('sha256', $token)], 'id = ?', [$userId]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifiziert E-Mail-Adresse
|
||||
*/
|
||||
public function verifyEmail(string $token): array
|
||||
{
|
||||
$hashedToken = hash('sha256', $token);
|
||||
|
||||
$user = $this->db->fetchOne(
|
||||
"SELECT id, tenant_id FROM users WHERE remember_token = ? AND email_verified_at IS NULL",
|
||||
[$hashedToken]
|
||||
);
|
||||
|
||||
if (!$user) {
|
||||
return ['success' => false, 'error' => 'Ungültiger oder abgelaufener Token'];
|
||||
}
|
||||
|
||||
$this->db->update('users', [
|
||||
'email_verified_at' => date('Y-m-d H:i:s'),
|
||||
'remember_token' => null,
|
||||
], 'id = ?', [$user['id']]);
|
||||
|
||||
// Onboarding-Status aktualisieren
|
||||
$this->updateOnboardingStep($user['tenant_id'], self::STEP_STREAM);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => $user['id'],
|
||||
'tenant_id' => $user['tenant_id'],
|
||||
'next_step' => self::STEP_STREAM,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert Stream-Konfiguration
|
||||
*/
|
||||
public function saveStream(int $tenantId, string $url, string $type = 'hls'): array
|
||||
{
|
||||
// Validieren
|
||||
$validation = $this->streamValidator->validate($url);
|
||||
|
||||
if (!$validation['valid']) {
|
||||
return ['success' => false, 'error' => $validation['error']];
|
||||
}
|
||||
|
||||
// Speichern
|
||||
$existing = $this->db->fetchOne(
|
||||
"SELECT id FROM tenant_streams WHERE tenant_id = ? AND is_primary = 1",
|
||||
[$tenantId]
|
||||
);
|
||||
|
||||
if ($existing) {
|
||||
$this->db->update('tenant_streams', [
|
||||
'stream_url' => $url,
|
||||
'stream_type' => $validation['type'] ?? $type,
|
||||
'last_status' => 'online',
|
||||
'last_check_at' => date('Y-m-d H:i:s'),
|
||||
], 'id = ?', [$existing['id']]);
|
||||
} else {
|
||||
$this->db->insert('tenant_streams', [
|
||||
'tenant_id' => $tenantId,
|
||||
'stream_url' => $url,
|
||||
'stream_type' => $validation['type'] ?? $type,
|
||||
'is_primary' => 1,
|
||||
'last_status' => 'online',
|
||||
'last_check_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Onboarding-Schritt aktualisieren
|
||||
$this->updateOnboardingStep($tenantId, self::STEP_BRANDING, ['stream_verified' => 1]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'stream_type' => $validation['type'],
|
||||
'next_step' => self::STEP_BRANDING,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert Basis-Branding
|
||||
*/
|
||||
public function saveBranding(int $tenantId, array $branding): array
|
||||
{
|
||||
$this->tenantManager->updateBranding($tenantId, $branding);
|
||||
|
||||
// Onboarding-Schritt aktualisieren
|
||||
$this->updateOnboardingStep($tenantId, self::STEP_COMPLETE, ['branding_configured' => 1]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'next_step' => self::STEP_COMPLETE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schliesst das Onboarding ab
|
||||
*/
|
||||
public function complete(int $tenantId): array
|
||||
{
|
||||
$this->db->update('tenant_onboarding', [
|
||||
'current_step' => self::STEP_COMPLETE,
|
||||
'completed_at' => date('Y-m-d H:i:s'),
|
||||
], 'tenant_id = ?', [$tenantId]);
|
||||
|
||||
// Tenant aktivieren
|
||||
$this->tenantManager->activate($tenantId);
|
||||
|
||||
return ['success' => true, 'completed' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Onboarding-Schritt
|
||||
*/
|
||||
private function updateOnboardingStep(int $tenantId, int $step, array $extra = []): void
|
||||
{
|
||||
$data = array_merge(['current_step' => $step], $extra);
|
||||
$this->db->update('tenant_onboarding', $data, 'tenant_id = ?', [$tenantId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Onboarding-Status zurück
|
||||
*/
|
||||
public function getStatus(int $tenantId): array
|
||||
{
|
||||
$onboarding = $this->db->fetchOne(
|
||||
"SELECT * FROM tenant_onboarding WHERE tenant_id = ?",
|
||||
[$tenantId]
|
||||
);
|
||||
|
||||
if (!$onboarding) {
|
||||
return [
|
||||
'current_step' => self::STEP_REGISTER,
|
||||
'completed' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'current_step' => (int)$onboarding['current_step'],
|
||||
'stream_verified' => (bool)$onboarding['stream_verified'],
|
||||
'branding_configured' => (bool)$onboarding['branding_configured'],
|
||||
'payment_configured' => (bool)$onboarding['payment_configured'],
|
||||
'completed' => $onboarding['completed_at'] !== null,
|
||||
'completed_at' => $onboarding['completed_at'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob E-Mail-Verification erforderlich ist
|
||||
*/
|
||||
public function requiresEmailVerification(): bool
|
||||
{
|
||||
// Aus Settings laden
|
||||
$settingsFile = dirname(__DIR__, 2) . '/SettingsManager.php';
|
||||
if (file_exists($settingsFile)) {
|
||||
require_once $settingsFile;
|
||||
$settings = new \SettingsManager();
|
||||
return $settings->get('saas_features.email_verification_required') ?? true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet Verification-E-Mail erneut
|
||||
*/
|
||||
public function resendVerification(int $userId): array
|
||||
{
|
||||
$user = $this->db->fetchOne("SELECT email, email_verified_at FROM users WHERE id = ?", [$userId]);
|
||||
|
||||
if (!$user) {
|
||||
return ['success' => false, 'error' => 'Benutzer nicht gefunden'];
|
||||
}
|
||||
|
||||
if ($user['email_verified_at']) {
|
||||
return ['success' => false, 'error' => 'E-Mail bereits verifiziert'];
|
||||
}
|
||||
|
||||
$token = $this->generateVerificationToken($userId);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'token' => $token,
|
||||
'email' => $user['email'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
/**
|
||||
* StreamValidator - Validiert Stream-URLs
|
||||
*/
|
||||
|
||||
namespace AuroraLivecam\Onboarding;
|
||||
|
||||
class StreamValidator
|
||||
{
|
||||
private array $supportedTypes = ['hls', 'rtmp', 'webrtc', 'iframe'];
|
||||
private int $timeout = 10;
|
||||
|
||||
/**
|
||||
* Validiert eine Stream-URL
|
||||
*/
|
||||
public function validate(string $url): array
|
||||
{
|
||||
$result = [
|
||||
'valid' => false,
|
||||
'type' => null,
|
||||
'error' => null,
|
||||
'details' => [],
|
||||
];
|
||||
|
||||
// URL-Format prüfen
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
$result['error'] = 'Ungültiges URL-Format';
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Stream-Typ erkennen
|
||||
$type = $this->detectStreamType($url);
|
||||
$result['type'] = $type;
|
||||
$result['details']['detected_type'] = $type;
|
||||
|
||||
// Je nach Typ validieren
|
||||
switch ($type) {
|
||||
case 'hls':
|
||||
return $this->validateHls($url, $result);
|
||||
case 'rtmp':
|
||||
return $this->validateRtmp($url, $result);
|
||||
case 'iframe':
|
||||
return $this->validateIframe($url, $result);
|
||||
default:
|
||||
// Generische HTTP-Prüfung
|
||||
return $this->validateHttp($url, $result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erkennt den Stream-Typ anhand der URL
|
||||
*/
|
||||
public function detectStreamType(string $url): string
|
||||
{
|
||||
$url = strtolower($url);
|
||||
|
||||
if (str_contains($url, '.m3u8')) {
|
||||
return 'hls';
|
||||
}
|
||||
|
||||
if (str_starts_with($url, 'rtmp://') || str_starts_with($url, 'rtmps://')) {
|
||||
return 'rtmp';
|
||||
}
|
||||
|
||||
if (str_contains($url, 'youtube.com') || str_contains($url, 'youtu.be') ||
|
||||
str_contains($url, 'vimeo.com') || str_contains($url, 'twitch.tv')) {
|
||||
return 'iframe';
|
||||
}
|
||||
|
||||
if (str_contains($url, '.mp4') || str_contains($url, '.webm')) {
|
||||
return 'video';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert HLS-Stream
|
||||
*/
|
||||
private function validateHls(string $url, array $result): array
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'User-Agent: Mozilla/5.0 (compatible; StreamValidator/1.0)'
|
||||
],
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$result['details']['http_code'] = $httpCode;
|
||||
$result['details']['content_type'] = $contentType;
|
||||
|
||||
if ($error) {
|
||||
$result['error'] = 'Verbindungsfehler: ' . $error;
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$result['error'] = "HTTP-Fehler: $httpCode";
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Prüfe ob es ein gültiges M3U8 ist
|
||||
if (!str_contains($response, '#EXTM3U')) {
|
||||
$result['error'] = 'Keine gültige HLS-Playlist gefunden';
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['valid'] = true;
|
||||
$result['details']['is_master'] = str_contains($response, '#EXT-X-STREAM-INF');
|
||||
$result['details']['segments'] = substr_count($response, '#EXTINF');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert RTMP-Stream (nur Format-Check)
|
||||
*/
|
||||
private function validateRtmp(string $url, array $result): array
|
||||
{
|
||||
// RTMP kann nicht einfach per HTTP geprüft werden
|
||||
// Wir prüfen nur das Format
|
||||
|
||||
$parsed = parse_url($url);
|
||||
|
||||
if (!isset($parsed['host']) || empty($parsed['host'])) {
|
||||
$result['error'] = 'RTMP-URL enthält keinen gültigen Host';
|
||||
return $result;
|
||||
}
|
||||
|
||||
// DNS-Check
|
||||
$ip = gethostbyname($parsed['host']);
|
||||
if ($ip === $parsed['host']) {
|
||||
$result['error'] = 'RTMP-Host nicht erreichbar (DNS-Fehler)';
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['valid'] = true;
|
||||
$result['details']['host'] = $parsed['host'];
|
||||
$result['details']['note'] = 'RTMP-Streams können erst zur Laufzeit vollständig validiert werden';
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert iFrame-Embed URL
|
||||
*/
|
||||
private function validateIframe(string $url, array $result): array
|
||||
{
|
||||
// Bekannte Embed-Plattformen
|
||||
$embedPatterns = [
|
||||
'youtube' => '/(?:youtube\.com\/(?:embed|watch)|youtu\.be)/i',
|
||||
'vimeo' => '/vimeo\.com/i',
|
||||
'twitch' => '/(?:twitch\.tv|player\.twitch\.tv)/i',
|
||||
'dailymotion' => '/dailymotion\.com/i',
|
||||
];
|
||||
|
||||
$platform = 'unknown';
|
||||
foreach ($embedPatterns as $name => $pattern) {
|
||||
if (preg_match($pattern, $url)) {
|
||||
$platform = $name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$result['details']['platform'] = $platform;
|
||||
|
||||
// HTTP-Check
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_NOBODY => true, // HEAD request
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$result['details']['http_code'] = $httpCode;
|
||||
|
||||
if ($httpCode >= 200 && $httpCode < 400) {
|
||||
$result['valid'] = true;
|
||||
} else {
|
||||
$result['error'] = "URL nicht erreichbar (HTTP $httpCode)";
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generische HTTP-Validierung
|
||||
*/
|
||||
private function validateHttp(string $url, array $result): array
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$result['details']['http_code'] = $httpCode;
|
||||
$result['details']['content_type'] = $contentType;
|
||||
|
||||
if ($error) {
|
||||
$result['error'] = 'Verbindungsfehler: ' . $error;
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($httpCode >= 200 && $httpCode < 400) {
|
||||
$result['valid'] = true;
|
||||
} else {
|
||||
$result['error'] = "URL nicht erreichbar (HTTP $httpCode)";
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schnelle Erreichbarkeitsprüfung
|
||||
*/
|
||||
public function isReachable(string $url): bool
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
return $httpCode >= 200 && $httpCode < 400;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user