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:
+196
-20
@@ -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' => [],
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user