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:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user