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
+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',