prepare( 'SELECT u.id AS user_id, u.email, u.email_norm, u.display_name, u.status AS user_status, t.id AS tenant_id, t.slug AS tenant_slug, t.name AS tenant_name, t.status AS tenant_status, tm.role, tm.status AS membership_status FROM users u JOIN tenant_memberships tm ON tm.user_id = u.id JOIN tenants t ON t.id = tm.tenant_id WHERE u.id = ? AND t.id = ? AND u.status = ? AND tm.status = ? AND t.status = ? LIMIT 1' ); $stmt->execute([$userId, $tenantId, 'active', 'active', 'active']); $row = $stmt->fetch(); } catch (Throwable $e) { return null; } if ($row === false) { unset($_SESSION['saas_user_id'], $_SESSION['saas_tenant_id'], $_SESSION['saas_role']); return null; } $user = $row; return $user; } function saas_session_login(array $identity): void { app_start_session(); session_regenerate_id(true); $_SESSION['saas_user_id'] = (int)$identity['user_id']; $_SESSION['saas_tenant_id'] = (int)$identity['tenant_id']; $_SESSION['saas_role'] = (string)$identity['role']; } function saas_logout(): void { app_start_session(); unset($_SESSION['saas_user_id'], $_SESSION['saas_tenant_id'], $_SESSION['saas_role']); } function saas_require_login(): array { $user = saas_current_user(); if ($user !== null) { return $user; } header('Location: login.php'); exit; } function saas_authenticate(PDO $pdo, string $email, string $password, string $tenantSlug = ''): array { $emailNorm = saas_email_norm($email); if (!filter_var($emailNorm, FILTER_VALIDATE_EMAIL) || $password === '') { return [ 'ok' => false, 'errors' => ['E-Mail oder Passwort ist ungueltig.'], ]; } $stmt = $pdo->prepare( 'SELECT id, email, email_norm, display_name, password_hash, status FROM users WHERE email_norm = ? LIMIT 1' ); $stmt->execute([$emailNorm]); $user = $stmt->fetch(); if ($user === false || (string)$user['status'] !== 'active' || empty($user['password_hash']) || !password_verify($password, (string)$user['password_hash']) ) { return [ 'ok' => false, 'errors' => ['E-Mail oder Passwort ist ungueltig.'], ]; } $params = [(int)$user['id'], 'active', 'active']; $tenantFilter = ''; if (trim($tenantSlug) !== '') { $tenantFilter = ' AND t.slug = ?'; $params[] = saas_slugify($tenantSlug); } $stmt = $pdo->prepare( 'SELECT t.id AS tenant_id, t.slug AS tenant_slug, t.name AS tenant_name, tm.role FROM tenant_memberships tm JOIN tenants t ON t.id = tm.tenant_id WHERE tm.user_id = ? AND tm.status = ? AND t.status = ?' . $tenantFilter . ' ORDER BY CASE tm.role WHEN \'owner\' THEN 1 WHEN \'admin\' THEN 2 ELSE 3 END, t.name LIMIT 1' ); $stmt->execute($params); $membership = $stmt->fetch(); if ($membership === false) { return [ 'ok' => false, 'errors' => ['Fuer dieses Konto ist kein aktiver Mandant verfuegbar.'], ]; } $pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = ?')->execute([(int)$user['id']]); return [ 'ok' => true, 'identity' => [ 'user_id' => (int)$user['id'], 'email' => (string)$user['email'], 'email_norm' => (string)$user['email_norm'], 'display_name' => (string)$user['display_name'], 'tenant_id' => (int)$membership['tenant_id'], 'tenant_slug' => (string)$membership['tenant_slug'], 'tenant_name' => (string)$membership['tenant_name'], 'role' => (string)$membership['role'], ], 'errors' => [], ]; } function saas_register_tenant_owner(PDO $pdo, array $input): array { $tenantName = trim((string)($input['tenant_name'] ?? '')); $tenantSlug = saas_slugify((string)($input['tenant_slug'] ?? '')); if ($tenantSlug === '') { $tenantSlug = saas_slugify($tenantName); } $displayName = trim((string)($input['display_name'] ?? '')); $email = trim((string)($input['email'] ?? '')); $emailNorm = saas_email_norm($email); $password = (string)($input['password'] ?? ''); $passwordConfirm = (string)($input['password_confirm'] ?? ''); $errors = []; if (strlen($tenantName) < 3 || strlen($tenantName) > 255) { $errors[] = 'Der Kundenname muss zwischen 3 und 255 Zeichen lang sein.'; } if (!preg_match('/^[a-z0-9][a-z0-9-]{1,98}[a-z0-9]$/', $tenantSlug)) { $errors[] = 'Das Kundenkuerzel darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten.'; } if (strlen($displayName) < 2 || strlen($displayName) > 255) { $errors[] = 'Der Name muss zwischen 2 und 255 Zeichen lang sein.'; } if (!filter_var($emailNorm, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Bitte eine gueltige E-Mail-Adresse eingeben.'; } if (strlen($password) < 8) { $errors[] = 'Das Passwort muss mindestens 8 Zeichen lang sein.'; } if ($password !== $passwordConfirm) { $errors[] = 'Die Passwort-Wiederholung stimmt nicht.'; } if ($errors !== []) { return ['ok' => false, 'errors' => $errors]; } $stmt = $pdo->prepare('SELECT id, password_hash, status FROM users WHERE email_norm = ? LIMIT 1'); $stmt->execute([$emailNorm]); $existingUser = $stmt->fetch(); if ($existingUser !== false && (string)$existingUser['status'] !== 'active') { return [ 'ok' => false, 'errors' => ['Dieses Benutzerkonto ist nicht aktiv.'], ]; } if ($existingUser !== false && !empty($existingUser['password_hash']) && !password_verify($password, (string)$existingUser['password_hash']) ) { return [ 'ok' => false, 'errors' => ['Diese E-Mail ist bereits vergeben. Bitte mit dem bestehenden Passwort anmelden.'], ]; } $stmt = $pdo->prepare('SELECT COUNT(*) FROM tenants WHERE slug = ?'); $stmt->execute([$tenantSlug]); if ((int)$stmt->fetchColumn() > 0) { return [ 'ok' => false, 'errors' => ['Dieses Kundenkuerzel ist bereits vergeben.'], ]; } $passwordHash = password_hash($password, PASSWORD_DEFAULT); try { $pdo->beginTransaction(); $stmt = $pdo->prepare( 'INSERT INTO tenants (slug, name, status, timezone, locale, currency_code) VALUES (?, ?, ?, ?, ?, ?)' ); $stmt->execute([$tenantSlug, $tenantName, 'active', 'Europe/Berlin', 'de-DE', 'EUR']); $tenantId = (int)$pdo->lastInsertId(); $stmt = $pdo->prepare( 'INSERT INTO tenant_settings (tenant_id, mark_price_cents, self_entry_enabled, paypal_enabled, paypal_url_template) VALUES (?, ?, ?, ?, ?)' ); $stmt->execute([$tenantId, 20, 1, 0, '']); if ($existingUser === false) { $stmt = $pdo->prepare( 'INSERT INTO users (email, email_norm, display_name, password_hash, status) VALUES (?, ?, ?, ?, ?)' ); $stmt->execute([$email, $emailNorm, $displayName, $passwordHash, 'active']); $userId = (int)$pdo->lastInsertId(); } else { $userId = (int)$existingUser['id']; $stmt = $pdo->prepare( 'UPDATE users SET email = ?, display_name = ?, password_hash = ?, status = ? WHERE id = ?' ); $stmt->execute([$email, $displayName, $passwordHash, 'active', $userId]); } $stmt = $pdo->prepare( 'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at) VALUES (?, ?, ?, ?, NOW())' ); $stmt->execute([$tenantId, $userId, 'owner', 'active']); $stmt = $pdo->prepare( 'INSERT INTO participants (tenant_id, user_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, ?, ?)' ); $stmt->execute([$tenantId, $userId, $displayName, $email, $emailNorm, 1]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } return [ 'ok' => false, 'errors' => ['Die Registrierung konnte nicht gespeichert werden.'], ]; } return [ 'ok' => true, 'identity' => [ 'user_id' => $userId, 'email' => $email, 'email_norm' => $emailNorm, 'display_name' => $displayName, 'tenant_id' => $tenantId, 'tenant_slug' => $tenantSlug, 'tenant_name' => $tenantName, 'role' => 'owner', ], 'errors' => [], ]; }