diff --git a/app/database.php b/app/database.php new file mode 100644 index 0000000..921944c --- /dev/null +++ b/app/database.php @@ -0,0 +1,33 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => true, + ]); + + return $pdo; +} diff --git a/app/saas-auth.php b/app/saas-auth.php new file mode 100644 index 0000000..f73fb24 --- /dev/null +++ b/app/saas-auth.php @@ -0,0 +1,340 @@ +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' => [], + ]; +} diff --git a/database/migrations/0002_saas_identity_tenants.sql b/database/migrations/0002_saas_identity_tenants.sql index 87f6114..d762ede 100644 --- a/database/migrations/0002_saas_identity_tenants.sql +++ b/database/migrations/0002_saas_identity_tenants.sql @@ -34,9 +34,6 @@ CREATE TABLE IF NOT EXISTS users ( display_name VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NULL, status VARCHAR(30) NOT NULL DEFAULT 'active', - email_verified_at DATETIME NULL, - mfa_enabled TINYINT(1) NOT NULL DEFAULT 0, - disabled_at DATETIME NULL, last_login_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, @@ -79,9 +76,7 @@ CREATE TABLE IF NOT EXISTS participants ( updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_participants_tenant_legacy (tenant_id, legacy_mitarbeiter_id), UNIQUE KEY uq_participants_tenant_email_norm (tenant_id, email_norm), - UNIQUE KEY uq_participants_tenant_user (tenant_id, user_id), KEY idx_participants_tenant_active_name (tenant_id, active, display_name), - KEY idx_participants_tenant_paypal_name (tenant_id, paypal_name), KEY idx_participants_user (user_id), CONSTRAINT fk_participants_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) diff --git a/database/migrations/0003_saas_auth_account_fields.sql b/database/migrations/0003_saas_auth_account_fields.sql new file mode 100644 index 0000000..445918d --- /dev/null +++ b/database/migrations/0003_saas_auth_account_fields.sql @@ -0,0 +1,86 @@ +SET @schema_name = DATABASE(); + +SET @sql = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @schema_name + AND TABLE_NAME = 'users' + AND COLUMN_NAME = 'email_verified_at' + ), + 'SELECT 1', + 'ALTER TABLE users ADD COLUMN email_verified_at DATETIME NULL AFTER status' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @schema_name + AND TABLE_NAME = 'users' + AND COLUMN_NAME = 'mfa_enabled' + ), + 'SELECT 1', + 'ALTER TABLE users ADD COLUMN mfa_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER email_verified_at' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @schema_name + AND TABLE_NAME = 'users' + AND COLUMN_NAME = 'disabled_at' + ), + 'SELECT 1', + 'ALTER TABLE users ADD COLUMN disabled_at DATETIME NULL AFTER mfa_enabled' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @schema_name + AND TABLE_NAME = 'participants' + AND INDEX_NAME = 'uq_participants_tenant_user' + ), + 'SELECT 1', + 'ALTER TABLE participants ADD UNIQUE KEY uq_participants_tenant_user (tenant_id, user_id)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @schema_name + AND TABLE_NAME = 'participants' + AND INDEX_NAME = 'idx_participants_tenant_paypal_name' + ), + 'SELECT 1', + 'ALTER TABLE participants ADD KEY idx_participants_tenant_paypal_name (tenant_id, paypal_name)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/docs/m2-technical-foundation.md b/docs/m2-technical-foundation.md index 967e1cb..9c825ac 100644 --- a/docs/m2-technical-foundation.md +++ b/docs/m2-technical-foundation.md @@ -140,7 +140,8 @@ Die Skripte erwarten die bekannten Dev-Umgebungsvariablen `DB_HOST`, `DB_NAME`, eine PayPal-Alias-Dublette und hinterlaesst keine Datei in `var/uploads`. - CSV-Upload negative Tests: Nicht-CSV-Dateien werden abgewiesen. - Golden Master weiterhin gruen mit 104 Assertions. -- HTTP-Smoke weiterhin gruen mit 14 sicheren Seiten. +- HTTP-Smoke weiterhin gruen mit 16 sicheren Seiten inklusive Login und + Registrierung. ## Bewusste Grenzen @@ -167,7 +168,8 @@ M3 startet mit additiven SaaS-Tabellen und einer Default-Tenant-Abbildung. Die Legacy-Seiten sollen dabei weiterhin ueber die bestehenden Tabellen laufen, bis der fachliche Kern in M4/M5 schrittweise auf das Zielmodell umgestellt wird. -Naechster Startpunkt: +M3 wurde inzwischen gestartet. Umgesetzte Details stehen in +`docs/m3-saas-basis-vorbereitung.md`. Der urspruengliche Startpunkt war: 1. `tenants`, `tenant_settings`, `users`, `tenant_memberships` und `participants` als neue Migration anlegen. diff --git a/docs/m3-saas-basis-vorbereitung.md b/docs/m3-saas-basis-vorbereitung.md index b78d95c..2c8c0fe 100644 --- a/docs/m3-saas-basis-vorbereitung.md +++ b/docs/m3-saas-basis-vorbereitung.md @@ -26,10 +26,11 @@ das neue Modell umgestellt. ## Umgesetzter erster Schritt -Umgesetzte Migration: +Umgesetzte Migrationen: ```text database/migrations/0002_saas_identity_tenants.sql +database/migrations/0003_saas_auth_account_fields.sql ``` Tabellen: @@ -45,6 +46,7 @@ Ergaenzende Skripte: ```text scripts/backfill-default-tenant.php scripts/check-m3-saas-basis.php +scripts/check-m3-auth-flow.php ``` Wichtige Regeln: @@ -104,6 +106,39 @@ Ausgefuehrter Dev-Stand: `tenant_memberships` angelegt. - Ein zweiter Backfill-Lauf blieb idempotent. +## Registrierung und Login + +Umgesetzte Dateien: + +```text +app/database.php +app/saas-auth.php +login.php +register.php +konto.php +logout.php +``` + +Umgesetzter Umfang: + +- Registrierung legt Tenant, Owner-User, Default-Settings, Membership und + Owner-Participant in einer Transaktion an. +- Login prueft `users.password_hash`, aktive Membership und aktiven Tenant. +- Logout beendet die neue PHP-Login-Session. +- `konto.php` zeigt den aktuellen SaaS-Kontext fuer den angemeldeten User. +- `functions.php` akzeptiert eine SaaS-Login-Session als erste Identitaetsquelle, + laesst `DEV_AUTH_EMAIL` und `AUTH_USER` aber als Legacy-Fallback bestehen. +- `footer.php` ist fuer nicht angemeldete Public-Seiten sicher und zeigt Links + zu Login und Registrierung. + +Noch offen im M3-Auth-Scope: + +- Passwort-Reset. +- E-Mail-Verifikation. +- Tenant-Aufloesung ueber Subdomain oder Custom Domain. +- Zentrale Rollenpruefung fuer neue SaaS-Seiten. +- Owner-/Admin-Seite fuer Tenant-Grundeinstellungen. + ## Erste Akzeptanzkriterien - Migrationen laufen mehrfach ohne Fehler: erfuellt. @@ -113,6 +148,8 @@ Ausgefuehrter Dev-Stand: - Jeder bestehende `kl_Mitarbeiter` hat genau einen `participant`: erfuellt. - `participants.legacy_mitarbeiter_id` ist gesetzt: erfuellt. - Admins werden als tenant-scoped Rolle abgebildet: erfuellt. +- Registrierung und Login funktionieren fuer einen neuen Test-Tenant: + erfuellt. - Golden-Master und HTTP-Smoke bleiben gruen: erfuellt. ## Risiken @@ -128,8 +165,12 @@ Ausgefuehrter Dev-Stand: ## Empfohlene Reihenfolge 1. Migration `0002_saas_identity_tenants.sql` erstellen: erledigt. -2. `scripts/backfill-default-tenant.php` erstellen: erledigt. -3. Backfill gegen die Dev-Datenbank ausfuehren: erledigt. -4. Kontrollskript fuer Tenant/Participant/Role-Counts schreiben: erledigt. -5. Golden-Master und HTTP-Smoke ausfuehren: erledigt. -6. Danach Login-/Registrierungsrouten planen: naechster Schritt. +2. Migration `0003_saas_auth_account_fields.sql` erstellen: erledigt. +3. `scripts/backfill-default-tenant.php` erstellen: erledigt. +4. Backfill gegen die Dev-Datenbank ausfuehren: erledigt. +5. Kontrollskript fuer Tenant/Participant/Role-Counts schreiben: erledigt. +6. Login-/Registrierungsrouten bauen: erledigt. +7. Auth-Flow-Kontrollskript schreiben: erledigt. +8. Golden-Master und HTTP-Smoke ausfuehren: erledigt. +9. Passwort-Reset, E-Mail-Verifikation und zentrale Rollenpruefung planen: + naechster Schritt. diff --git a/docs/saas-umstrukturierungsplan.md b/docs/saas-umstrukturierungsplan.md index cc74dc0..7871a42 100644 --- a/docs/saas-umstrukturierungsplan.md +++ b/docs/saas-umstrukturierungsplan.md @@ -417,8 +417,10 @@ Schritte: - Default-Tenant fuer den aktuellen Bestand anlegen: erledigt. - Bestehende `kl_Mitarbeiter` idempotent in `participants` spiegeln: erledigt. -- Registrierung: Tenant + Owner-User + Default-Settings erzeugen. -- Login, Logout, Passwort-Reset und E-Mail-Verifikation bauen. +- Registrierung: Tenant + Owner-User + Default-Settings erzeugen: erster + Flow erledigt. +- Login und Logout bauen: erster Flow erledigt. +- Passwort-Reset und E-Mail-Verifikation bauen. - Tenant-Aufloesung definieren. - Rollenpruefung zentralisieren. - Erste Admin-/Owner-Seite fuer Grundeinstellungen. diff --git a/footer.php b/footer.php index 8eaef3c..f3adca5 100644 --- a/footer.php +++ b/footer.php @@ -33,6 +33,20 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){ } ?>
  • FAQ Kaffeeliste
  • + +
  • Kundenkonto
  • +
  • Logout
  • + +
  • Login
  • +
  • Registrierung
  • + diff --git a/functions.php b/functions.php index 7a54ad0..9c9251d 100644 --- a/functions.php +++ b/functions.php @@ -1,21 +1,33 @@ + + + + diff --git a/login.php b/login.php new file mode 100644 index 0000000..24b506d --- /dev/null +++ b/login.php @@ -0,0 +1,74 @@ + + + + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..db49ff6 --- /dev/null +++ b/logout.php @@ -0,0 +1,27 @@ + + + + + diff --git a/register.php b/register.php new file mode 100644 index 0000000..45fa452 --- /dev/null +++ b/register.php @@ -0,0 +1,88 @@ + trim((string)($_POST['tenant_name'] ?? '')), + 'tenant_slug' => trim((string)($_POST['tenant_slug'] ?? '')), + 'display_name' => trim((string)($_POST['display_name'] ?? '')), + 'email' => trim((string)($_POST['email'] ?? '')), +]; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + app_require_csrf(); + + $result = saas_register_tenant_owner($pdo, [ + 'tenant_name' => $values['tenant_name'], + 'tenant_slug' => $values['tenant_slug'], + 'display_name' => $values['display_name'], + 'email' => $values['email'], + 'password' => (string)($_POST['password'] ?? ''), + 'password_confirm' => (string)($_POST['password_confirm'] ?? ''), + ]); + + if ($result['ok']) { + saas_session_login($result['identity']); + header('Location: konto.php?registered=1'); + exit; + } + + $errors = $result['errors']; +} + +include 'header.php'; +include 'headerline.php'; +include 'nav.php'; +?> + + + + diff --git a/scripts/check-m3-auth-flow.php b/scripts/check-m3-auth-flow.php new file mode 100644 index 0000000..d52061f --- /dev/null +++ b/scripts/check-m3-auth-flow.php @@ -0,0 +1,101 @@ +prepare('SELECT id FROM tenants WHERE slug = ?'); + $stmt->execute([$slug]); + $tenantId = $stmt->fetchColumn(); + + if ($tenantId !== false) { + $tenantId = (int)$tenantId; + $pdo->prepare('DELETE FROM participants WHERE tenant_id = ?')->execute([$tenantId]); + $pdo->prepare('DELETE FROM tenant_memberships WHERE tenant_id = ?')->execute([$tenantId]); + $pdo->prepare('DELETE FROM tenant_settings WHERE tenant_id = ?')->execute([$tenantId]); + $pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tenantId]); + } + + $pdo->prepare('DELETE FROM users WHERE email_norm = ?')->execute([$emailNorm]); +} + +$pdo = dev_pdo(); +dev_apply_schema($pdo); + +$slug = 'm3-auth-flow-check'; +$email = 'm3-auth-flow-check@test.local'; +$emailNorm = saas_email_norm($email); +$password = 'M3-auth-flow-123'; +$failures = []; +$passes = 0; + +auth_check_cleanup($pdo, $slug, $emailNorm); + +$result = saas_register_tenant_owner($pdo, [ + 'tenant_name' => 'M3 Auth Flow Check', + 'tenant_slug' => $slug, + 'display_name' => 'M3 Auth Check', + 'email' => $email, + 'password' => $password, + 'password_confirm' => $password, +]); + +auth_check_assert('registration succeeds', $result['ok'] === true, $failures, $passes); + +if ($result['ok']) { + $tenantId = (int)$result['identity']['tenant_id']; + $userId = (int)$result['identity']['user_id']; + + $stmt = $pdo->prepare('SELECT COUNT(*) FROM tenants WHERE id = ? AND slug = ?'); + $stmt->execute([$tenantId, $slug]); + auth_check_assert('tenant is persisted', (int)$stmt->fetchColumn() === 1, $failures, $passes); + + $stmt = $pdo->prepare('SELECT COUNT(*) FROM tenant_settings WHERE tenant_id = ?'); + $stmt->execute([$tenantId]); + auth_check_assert('tenant settings are persisted', (int)$stmt->fetchColumn() === 1, $failures, $passes); + + $stmt = $pdo->prepare('SELECT COUNT(*) FROM tenant_memberships WHERE tenant_id = ? AND user_id = ? AND role = ?'); + $stmt->execute([$tenantId, $userId, 'owner']); + auth_check_assert('owner membership is persisted', (int)$stmt->fetchColumn() === 1, $failures, $passes); + + $stmt = $pdo->prepare('SELECT COUNT(*) FROM participants WHERE tenant_id = ? AND user_id = ? AND email_norm = ?'); + $stmt->execute([$tenantId, $userId, $emailNorm]); + auth_check_assert('owner participant is persisted', (int)$stmt->fetchColumn() === 1, $failures, $passes); +} + +$login = saas_authenticate($pdo, $email, $password, $slug); +auth_check_assert('login succeeds', $login['ok'] === true, $failures, $passes); +if ($login['ok']) { + auth_check_assert('login returns owner role', $login['identity']['role'] === 'owner', $failures, $passes); + auth_check_assert('login returns tenant slug', $login['identity']['tenant_slug'] === $slug, $failures, $passes); +} + +$badLogin = saas_authenticate($pdo, $email, 'wrong-password', $slug); +auth_check_assert('wrong password is rejected', $badLogin['ok'] === false, $failures, $passes); + +auth_check_cleanup($pdo, $slug, $emailNorm); + +if ($failures !== []) { + echo "\nM3 auth flow check failed with " . count($failures) . " failure(s):\n"; + foreach ($failures as $failure) { + echo "- {$failure}\n"; + } + exit(1); +} + +echo "\nM3 auth flow check passed with {$passes} assertions.\n"; diff --git a/scripts/http-smoke.php b/scripts/http-smoke.php index 49f8d14..e3451b0 100644 --- a/scripts/http-smoke.php +++ b/scripts/http-smoke.php @@ -51,6 +51,16 @@ $checks = [ 'path' => 'faq.php', 'contains' => ['FAQ - Kaffeeliste'], ], + [ + 'label' => 'Login', + 'path' => 'login.php', + 'contains' => ['Login', 'Registrieren'], + ], + [ + 'label' => 'Registrierung', + 'path' => 'register.php', + 'contains' => ['Registrierung', 'Kundenkonto anlegen'], + ], [ 'label' => 'Namensanpassung', 'path' => 'namenanpassen.php', @@ -217,4 +227,3 @@ if ($failures !== []) { } echo "\nHTTP smoke passed with {$passes} checked pages.\n"; -