M3 Tenant-Aufloesung per App-Session ergaenzen

- Mandantenauswahl fuer Benutzer mit mehreren Tenants bauen
- feste Tenant-Domains ohne Wildcard-Abhaengigkeit vorbereiten
- Login, Backfill, Smoke-Checks und M3-Dokumentation aktualisieren
This commit is contained in:
2026-07-13 20:04:20 +02:00
parent 8ab4a8a9bb
commit 9f2e1bdfb1
10 changed files with 475 additions and 28 deletions
+196 -20
View File
@@ -34,6 +34,70 @@ function saas_token_hash(string $token): string
return hash('sha256', $token);
}
function saas_normalize_host(?string $host): string
{
$host = strtolower(trim((string)$host));
if ($host === '') {
return '';
}
if (str_contains($host, '://')) {
$parsedHost = parse_url($host, PHP_URL_HOST);
$host = is_string($parsedHost) ? $parsedHost : $host;
}
$host = preg_replace('/:\d+$/', '', $host) ?? $host;
return trim($host, " \t\n\r\0\x0B.");
}
function saas_primary_app_host(): string
{
return saas_normalize_host(app_env('APP_PRIMARY_HOST', ''));
}
function saas_is_primary_app_host(?string $host = null): bool
{
$primaryHost = saas_primary_app_host();
if ($primaryHost === '') {
return false;
}
$host = $host ?? ($_SERVER['HTTP_HOST'] ?? '');
return saas_normalize_host($host) === $primaryHost;
}
function saas_resolve_tenant_domain(PDO $pdo, ?string $host = null): ?array
{
$host = saas_normalize_host($host ?? ($_SERVER['HTTP_HOST'] ?? ''));
if ($host === '' || saas_is_primary_app_host($host)) {
return null;
}
$stmt = $pdo->prepare(
'SELECT
td.id AS tenant_domain_id,
td.domain,
td.domain_norm,
td.domain_type,
t.id AS tenant_id,
t.slug AS tenant_slug,
t.name AS tenant_name,
t.status AS tenant_status
FROM tenant_domains td
JOIN tenants t ON t.id = td.tenant_id
WHERE td.domain_norm = ?
AND td.status = ?
AND t.status = ?
LIMIT 1'
);
$stmt->execute([$host, 'active', 'active']);
$row = $stmt->fetch();
return $row !== false ? $row : null;
}
function saas_parse_money_cents(mixed $value): ?int
{
$normalized = trim((string)$value);
@@ -137,12 +201,19 @@ function saas_session_login(array $identity): void
$_SESSION['saas_user_id'] = (int)$identity['user_id'];
$_SESSION['saas_tenant_id'] = (int)$identity['tenant_id'];
$_SESSION['saas_role'] = (string)$identity['role'];
saas_clear_pending_tenant_selection();
}
function saas_logout(): void
{
app_start_session();
unset($_SESSION['saas_user_id'], $_SESSION['saas_tenant_id'], $_SESSION['saas_role']);
unset(
$_SESSION['saas_user_id'],
$_SESSION['saas_tenant_id'],
$_SESSION['saas_role'],
$_SESSION['saas_pending_user_id'],
$_SESSION['saas_pending_started_at']
);
}
function saas_require_login(): array
@@ -380,6 +451,107 @@ function saas_find_auth_identity(PDO $pdo, string $email, string $tenantSlug = '
return $identity !== false ? $identity : null;
}
function saas_list_user_memberships(PDO $pdo, int $userId): array
{
$stmt = $pdo->prepare(
'SELECT
u.id AS user_id,
u.email,
u.email_norm,
u.display_name,
u.status AS user_status,
u.email_verified_at,
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 u.status = ?
AND tm.status = ?
AND t.status = ?
ORDER BY
CASE tm.role
WHEN \'owner\' THEN 1
WHEN \'admin\' THEN 2
ELSE 3
END,
t.name'
);
$stmt->execute([$userId, 'active', 'active', 'active']);
return $stmt->fetchAll();
}
function saas_identity_for_user_tenant(PDO $pdo, int $userId, int $tenantId): ?array
{
$stmt = $pdo->prepare(
'SELECT
u.id AS user_id,
u.email,
u.email_norm,
u.display_name,
u.status AS user_status,
u.email_verified_at,
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']);
$identity = $stmt->fetch();
return $identity !== false ? $identity : null;
}
function saas_start_pending_tenant_selection(int $userId): void
{
app_start_session();
session_regenerate_id(true);
$_SESSION['saas_pending_user_id'] = $userId;
$_SESSION['saas_pending_started_at'] = time();
unset($_SESSION['saas_user_id'], $_SESSION['saas_tenant_id'], $_SESSION['saas_role']);
}
function saas_pending_tenant_user_id(): ?int
{
app_start_session();
$userId = isset($_SESSION['saas_pending_user_id']) ? (int)$_SESSION['saas_pending_user_id'] : 0;
$startedAt = isset($_SESSION['saas_pending_started_at']) ? (int)$_SESSION['saas_pending_started_at'] : 0;
if ($userId <= 0 || $startedAt <= 0 || $startedAt < time() - 900) {
saas_clear_pending_tenant_selection();
return null;
}
return $userId;
}
function saas_clear_pending_tenant_selection(): void
{
if (PHP_SAPI !== 'cli') {
app_start_session();
}
unset($_SESSION['saas_pending_user_id'], $_SESSION['saas_pending_started_at']);
}
function saas_create_auth_token(PDO $pdo, int $userId, string $tokenType, ?int $tenantId, int $ttlMinutes): array
{
$token = bin2hex(random_bytes(32));
@@ -625,11 +797,7 @@ function saas_authenticate(PDO $pdo, string $email, string $password, string $te
}
$stmt = $pdo->prepare(
'SELECT
t.id AS tenant_id,
t.slug AS tenant_slug,
t.name AS tenant_name,
tm.role
'SELECT t.id AS tenant_id
FROM tenant_memberships tm
JOIN tenants t ON t.id = tm.tenant_id
WHERE tm.user_id = ?
@@ -641,13 +809,30 @@ function saas_authenticate(PDO $pdo, string $email, string $password, string $te
WHEN \'admin\' THEN 2
ELSE 3
END,
t.name
LIMIT 1'
t.name'
);
$stmt->execute($params);
$membership = $stmt->fetch();
$tenantIds = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
if ($membership === false) {
if ($tenantIds === []) {
return [
'ok' => false,
'errors' => ['Fuer dieses Konto ist kein aktiver Mandant verfuegbar.'],
];
}
if (count($tenantIds) > 1 && trim($tenantSlug) === '') {
return [
'ok' => true,
'needs_tenant_selection' => true,
'user_id' => (int)$user['id'],
'memberships' => saas_list_user_memberships($pdo, (int)$user['id']),
'errors' => [],
];
}
$identity = saas_identity_for_user_tenant($pdo, (int)$user['id'], $tenantIds[0]);
if ($identity === null) {
return [
'ok' => false,
'errors' => ['Fuer dieses Konto ist kein aktiver Mandant verfuegbar.'],
@@ -658,16 +843,7 @@ function saas_authenticate(PDO $pdo, string $email, string $password, string $te
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'],
],
'identity' => $identity,
'errors' => [],
];
}
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS tenant_domains (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
domain VARCHAR(255) NOT NULL,
domain_norm VARCHAR(255) NOT NULL,
domain_type VARCHAR(30) NOT NULL DEFAULT 'custom',
status VARCHAR(30) NOT NULL DEFAULT 'active',
verified_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_tenant_domains_domain_norm (domain_norm),
KEY idx_tenant_domains_tenant_status (tenant_id, status),
CONSTRAINT fk_tenant_domains_tenant
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+2 -2
View File
@@ -140,9 +140,9 @@ 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 20 sicheren Seiten inklusive Login,
- HTTP-Smoke weiterhin gruen mit 21 sicheren Seiten inklusive Login,
Registrierung, Passwort-Reset, E-Mail-Verifikation und geschuetzter
Mandant-Einstellungen.
Mandant-Einstellungen sowie geschuetzter Mandantenauswahl.
## Bewusste Grenzen
+37 -4
View File
@@ -32,6 +32,7 @@ Umgesetzte Migrationen:
database/migrations/0002_saas_identity_tenants.sql
database/migrations/0003_saas_auth_account_fields.sql
database/migrations/0004_saas_auth_tokens.sql
database/migrations/0005_saas_tenant_resolution.sql
```
Tabellen:
@@ -41,6 +42,7 @@ Tabellen:
- `users`
- `tenant_memberships`
- `participants`
- `tenant_domains`
Ergaenzende Skripte:
@@ -50,6 +52,7 @@ scripts/check-m3-saas-basis.php
scripts/check-m3-auth-flow.php
scripts/check-m3-settings-flow.php
scripts/check-m3-password-email-flow.php
scripts/check-m3-tenant-resolution-flow.php
```
Wichtige Regeln:
@@ -97,9 +100,12 @@ Vorgang:
erzeugen.
5. `admin = 1` als Rolle `admin` abbilden; ein konfigurierter erster Benutzer
kann Rolle `owner` erhalten.
6. Verwaiste `participants` mit nicht mehr vorhandener `legacy_mitarbeiter_id`
aus dem Default-Tenant entfernen.
Der Backfill muss idempotent sein und darf keine bestehenden Legacy-Daten
loeschen.
loeschen. Geloescht werden nur SaaS-Spiegelungen, deren Legacy-Mitarbeiter in
`kl_Mitarbeiter` nicht mehr existiert.
Ausgefuehrter Dev-Stand:
@@ -120,6 +126,7 @@ login.php
register.php
konto.php
logout.php
mandant-auswahl.php
passwort-vergessen.php
passwort-zuruecksetzen.php
email-verifikation-senden.php
@@ -131,6 +138,10 @@ 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.
- Hat ein User genau einen aktiven Mandanten, wird dieser direkt in die Session
gelegt.
- Hat ein User mehrere aktive Mandanten, fuehrt der Login zur
Mandantenauswahl.
- 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,
@@ -145,10 +156,28 @@ Umgesetzter Umfang:
Noch offen im M3-Auth-Scope:
- Tenant-Aufloesung ueber Subdomain oder Custom Domain.
- Weitergehende Rollenmatrix fuer spaetere SaaS-Seiten.
- Produktiver Mailversand fuer Reset- und Verifikationslinks.
## Tenant-Aufloesung
Entscheidung fuer den Webspace-Betrieb:
- Keine Wildcard-Subdomains im ersten Schritt.
- Primaere App-Adresse ist eine zentrale App-Domain wie `app.kaffeeliste.de`.
- Der aktive Mandant wird nach Login ueber `tenant_memberships` und die PHP-
Session gesetzt.
- Bei genau einem Mandanten wird automatisch weitergeleitet.
- Bei mehreren Mandanten nutzt der User `mandant-auswahl.php`.
- `tenant_domains` ist vorbereitet fuer spaeter gezielt eingerichtete feste
Domains oder Subdomains, aber nicht Voraussetzung fuer den Start.
Noch offen:
- `APP_PRIMARY_HOST` in der Zielumgebung setzen.
- Produktive Domain-/Zertifikatspruefung fuer einzelne feste Domains
definieren.
## Rollen und Grundeinstellungen
Umgesetzte Dateien:
@@ -183,6 +212,7 @@ Umgesetzter Umfang:
- Owner-/Admin-Grundeinstellungen koennen aktualisiert werden: erfuellt.
- Passwort-Reset und E-Mail-Verifikation funktionieren mit Single-Use-Tokens:
erfuellt.
- Mandantenauswahl funktioniert fuer User mit mehreren Mandanten: erfuellt.
- Golden-Master und HTTP-Smoke bleiben gruen: erfuellt.
## Risiken
@@ -209,6 +239,9 @@ Umgesetzter Umfang:
10. Migration `0004_saas_auth_tokens.sql` erstellen: erledigt.
11. Passwort-Reset und E-Mail-Verifikation bauen: erledigt.
12. Password-/E-Mail-Flow-Kontrollskript schreiben: erledigt.
13. Golden-Master und HTTP-Smoke ausfuehren: erledigt.
14. Tenant-Aufloesung planen:
13. Migration `0005_saas_tenant_resolution.sql` erstellen: erledigt.
14. Mandantenauswahl und zentrale Session-Aufloesung bauen: erledigt.
15. Tenant-Resolution-Kontrollskript schreiben: erledigt.
16. Golden-Master und HTTP-Smoke ausfuehren: erledigt.
17. Produktiven Mailversand oder Landingpage vorbereiten:
naechster Schritt.
+2 -1
View File
@@ -422,7 +422,8 @@ Schritte:
- Login und Logout bauen: erster Flow erledigt.
- Passwort-Reset und E-Mail-Verifikation bauen: Dev-Flow mit Single-Use-Tokens
erledigt.
- Tenant-Aufloesung definieren.
- Tenant-Aufloesung definieren: zentrale App-Domain mit Session-Kontext und
Mandantenauswahl erledigt; feste Domains vorbereitet, keine Wildcards.
- Rollenpruefung zentralisieren: erster Owner/Admin-Check erledigt.
- Erste Admin-/Owner-Seite fuer Grundeinstellungen: erledigt.
+6
View File
@@ -17,6 +17,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$tenantSlug
);
if ($result['ok'] && !empty($result['needs_tenant_selection'])) {
saas_start_pending_tenant_selection((int)$result['user_id']);
header('Location: mandant-auswahl.php');
exit;
}
if ($result['ok']) {
saas_session_login($result['identity']);
header('Location: konto.php');
+74
View File
@@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/functions.php';
$pdo = app_db_pdo();
$pendingUserId = saas_pending_tenant_user_id();
if ($pendingUserId === null) {
header('Location: login.php');
exit;
}
$errors = [];
$memberships = saas_list_user_memberships($pdo, $pendingUserId);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
app_require_csrf();
$tenantId = isset($_POST['tenant_id']) ? (int)$_POST['tenant_id'] : 0;
$identity = $tenantId > 0 ? saas_identity_for_user_tenant($pdo, $pendingUserId, $tenantId) : null;
if ($identity === null) {
$errors[] = 'Dieser Mandant ist fuer dein Konto nicht verfuegbar.';
} else {
saas_session_login($identity);
header('Location: konto.php');
exit;
}
}
include 'header.php';
include 'headerline.php';
include 'nav.php';
?>
<section id="banner">
<div class="content">
<h2>Mandant auswaehlen</h2>
<?php if ($errors !== []): ?>
<div class="hint-box error">
<?php foreach ($errors as $error): ?>
<p><?php echo saas_html($error); ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($memberships === []): ?>
<div class="hint-box error"><p>Fuer dieses Konto ist kein aktiver Mandant verfuegbar.</p></div>
<?php else: ?>
<form method="post" action="mandant-auswahl.php">
<?php echo app_csrf_field(); ?>
<div class="row">
<div class="col-12">
<label for="tenant_id">Mandant</label>
<select name="tenant_id" id="tenant_id" required>
<?php foreach ($memberships as $membership): ?>
<option value="<?php echo saas_html($membership['tenant_id']); ?>">
<?php echo saas_html($membership['tenant_name']); ?>
(<?php echo saas_html($membership['role']); ?>)
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<ul class="actions">
<li><button type="submit">Weiter</button></li>
<li><a href="logout.php" class="button alt">Abbrechen</a></li>
</ul>
</form>
<?php endif; ?>
</div>
</section>
<?php include 'footer.php'; ?>
+16 -1
View File
@@ -89,6 +89,7 @@ $ownerEmailNorm = m3_email_norm(getenv('M3_DEFAULT_TENANT_OWNER_EMAIL') ?: null)
$stats = [
'applied_migrations' => count($applied),
'participants' => 0,
'stale_participants_removed' => 0,
'users' => 0,
'memberships' => 0,
];
@@ -154,7 +155,8 @@ try {
email = VALUES(email),
email_norm = VALUES(email_norm),
paypal_name = VALUES(paypal_name),
active = VALUES(active)'
active = VALUES(active),
legacy_mitarbeiter_id = VALUES(legacy_mitarbeiter_id)'
);
$upsertMembership = $pdo->prepare(
'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at)
@@ -206,6 +208,18 @@ try {
$stats['memberships']++;
}
$removeStaleParticipants = $pdo->prepare(
'DELETE p
FROM participants p
LEFT JOIN kl_Mitarbeiter m
ON m.MitarbeiterID = p.legacy_mitarbeiter_id
WHERE p.tenant_id = ?
AND p.legacy_mitarbeiter_id IS NOT NULL
AND m.MitarbeiterID IS NULL'
);
$removeStaleParticipants->execute([$tenantId]);
$stats['stale_participants_removed'] = $removeStaleParticipants->rowCount();
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
@@ -217,5 +231,6 @@ echo "M3 default tenant backfill complete.\n";
echo "Tenant: {$tenantSlug}\n";
echo "Applied migrations: {$stats['applied_migrations']}\n";
echo "Participants mirrored: {$stats['participants']}\n";
echo "Stale participants removed: {$stats['stale_participants_removed']}\n";
echo "Users upserted: {$stats['users']}\n";
echo "Memberships upserted: {$stats['memberships']}\n";
+121
View File
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
require __DIR__ . '/dev-db.php';
require __DIR__ . '/../app/saas-auth.php';
function tenant_resolution_assert(string $label, bool $condition, array &$failures, int &$passes): void
{
if ($condition) {
$passes++;
echo "PASS {$label}\n";
return;
}
$failures[] = $label;
echo "FAIL {$label}\n";
}
function tenant_resolution_cleanup(PDO $pdo, array $slugs, string $emailNorm): void
{
foreach ($slugs as $slug) {
$stmt = $pdo->prepare('SELECT id FROM tenants WHERE slug = ?');
$stmt->execute([$slug]);
$tenantId = $stmt->fetchColumn();
if ($tenantId === false) {
continue;
}
$tenantId = (int)$tenantId;
$pdo->prepare('DELETE FROM tenant_domains WHERE tenant_id = ?')->execute([$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);
$slugs = ['m3-tenant-choice-a', 'm3-tenant-choice-b'];
$email = 'm3-tenant-choice@test.local';
$emailNorm = saas_email_norm($email);
$password = 'M3-tenant-choice-123';
$failures = [];
$passes = 0;
tenant_resolution_cleanup($pdo, $slugs, $emailNorm);
$first = saas_register_tenant_owner($pdo, [
'tenant_name' => 'M3 Tenant Choice A',
'tenant_slug' => $slugs[0],
'display_name' => 'M3 Tenant Choice',
'email' => $email,
'password' => $password,
'password_confirm' => $password,
]);
$second = saas_register_tenant_owner($pdo, [
'tenant_name' => 'M3 Tenant Choice B',
'tenant_slug' => $slugs[1],
'display_name' => 'M3 Tenant Choice',
'email' => $email,
'password' => $password,
'password_confirm' => $password,
]);
tenant_resolution_assert('first registration succeeds', $first['ok'] === true, $failures, $passes);
tenant_resolution_assert('second registration succeeds', $second['ok'] === true, $failures, $passes);
if ($first['ok'] && $second['ok']) {
$userId = (int)$first['identity']['user_id'];
$memberships = saas_list_user_memberships($pdo, $userId);
tenant_resolution_assert('user has two memberships', count($memberships) === 2, $failures, $passes);
$genericLogin = saas_authenticate($pdo, $email, $password);
tenant_resolution_assert('generic login succeeds', $genericLogin['ok'] === true, $failures, $passes);
tenant_resolution_assert('generic login needs tenant selection', !empty($genericLogin['needs_tenant_selection']), $failures, $passes);
tenant_resolution_assert('generic login exposes memberships', count($genericLogin['memberships'] ?? []) === 2, $failures, $passes);
$specificLogin = saas_authenticate($pdo, $email, $password, $slugs[1]);
tenant_resolution_assert('specific login succeeds', $specificLogin['ok'] === true, $failures, $passes);
tenant_resolution_assert(
'specific login selects requested tenant',
($specificLogin['identity']['tenant_slug'] ?? null) === $slugs[1],
$failures,
$passes
);
$identity = saas_identity_for_user_tenant($pdo, $userId, (int)$second['identity']['tenant_id']);
tenant_resolution_assert('tenant selection identity exists', $identity !== null, $failures, $passes);
if ($identity !== null) {
tenant_resolution_assert('tenant selection returns requested tenant', $identity['tenant_slug'] === $slugs[1], $failures, $passes);
}
$pdo->prepare(
'INSERT INTO tenant_domains (tenant_id, domain, domain_norm, domain_type, status, verified_at)
VALUES (?, ?, ?, ?, ?, NOW())'
)->execute([(int)$second['identity']['tenant_id'], 'kaffee.example.test', 'kaffee.example.test', 'custom', 'active']);
$domainTenant = saas_resolve_tenant_domain($pdo, 'kaffee.example.test:443');
tenant_resolution_assert('fixed domain resolves tenant', $domainTenant !== null, $failures, $passes);
if ($domainTenant !== null) {
tenant_resolution_assert('fixed domain returns requested tenant', $domainTenant['tenant_slug'] === $slugs[1], $failures, $passes);
}
}
tenant_resolution_cleanup($pdo, $slugs, $emailNorm);
if ($failures !== []) {
echo "\nM3 tenant resolution check failed with " . count($failures) . " failure(s):\n";
foreach ($failures as $failure) {
echo "- {$failure}\n";
}
exit(1);
}
echo "\nM3 tenant resolution check passed with {$passes} assertions.\n";
+5
View File
@@ -66,6 +66,11 @@ $checks = [
'path' => 'passwort-vergessen.php',
'contains' => ['Passwort vergessen', 'Link vorbereiten'],
],
[
'label' => 'Mandantenauswahl Login-Schutz',
'path' => 'mandant-auswahl.php',
'contains' => ['Login'],
],
[
'label' => 'Passwort zuruecksetzen Invalid',
'path' => 'passwort-zuruecksetzen.php',