fix(dashboard): use synthetic internal email for SSO owner row

client_users.email has a UNIQUE constraint. SSO users who already have a
CaveauAI account share the same email address, causing provision to fail
with "already belongs to another workspace".

Fix: SSO-provisioned client_users rows use dbn-sso-{client_id}@dbn.tools.internal
as the owner email so they never collide with real account rows. The real
email stays on clients.contact_email for display purposes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 17:51:35 +02:00
parent 06d01a3bce
commit b6212b8729
+18 -9
View File
@@ -145,14 +145,16 @@ final class CorpusProvision
private static function createOwnerUser(PDO $db, int $clientId, string $email, string $displayName): int
{
$stmt = $db->prepare('SELECT id, client_id FROM client_users WHERE email = ? LIMIT 1');
$stmt->execute([$email]);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existing && (int)$existing['client_id'] === $clientId) {
return (int)$existing['id'];
}
if ($existing) {
throw new RuntimeException("Email {$email} already belongs to another workspace.");
// client_users.email has a UNIQUE constraint, so an SSO-provisioned workspace
// uses a synthetic internal email to avoid conflicting with any real CaveauAI
// account that may share the same address. The real email lives on clients.contact_email.
$ssoEmail = self::ssoInternalEmail($db, $clientId);
$stmt = $db->prepare('SELECT id FROM client_users WHERE email = ? AND client_id = ? LIMIT 1');
$stmt->execute([$ssoEmail, $clientId]);
$existingId = (int)($stmt->fetchColumn() ?: 0);
if ($existingId > 0) {
return $existingId;
}
$usernameBase = preg_replace('/[^a-z0-9._-]+/', '', strtolower(strstr($email, '@', true) ?: 'owner'));
@@ -167,13 +169,20 @@ final class CorpusProvision
$stmt->execute([
$clientId,
$username,
$email,
$ssoEmail,
$displayName !== '' ? $displayName : null,
password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT),
]);
return (int)$db->lastInsertId();
}
private static function ssoInternalEmail(PDO $db, int $clientId): string
{
// Derive a stable internal-only email from the client_id so it is unique
// even when the same real address already exists for another workspace.
return 'dbn-sso-' . $clientId . '@dbn.tools.internal';
}
private static function ensureDefaultCorpus(PDO $db, int $clientId, string $email): int
{
$stmt = $db->prepare(