`, plan `sandbox`, * dbn_sso_uid = sso_uid, fresh api_key hash) * - one row in `client_users` (role owner, random password hash — login is * always via SSO bridge, never password) * - one row in `client_corpora` (slug `default`, is_default=1) * * CaveauAI sessions (which already have client_id + client_user_id set on the * tools session) are returned as-is without touching the DB. * * Output shape: * [ * 'client_id' => int, * 'client_user_id' => int, * 'corpus_id' => int, * 'created' => bool, // true the first time, false on every call after * ] */ final class CorpusProvision { public static function ensureForSsoUser(int $ssoUid, string $email, string $displayName): array { if ($ssoUid <= 0) { throw new DbnToolsHttpException('SSO user id is required.', 400, 'missing_sso_uid'); } if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new DbnToolsHttpException('A valid email is required.', 400, 'invalid_email'); } dbnToolsBootCaveau(); $db = getDb(); $existing = self::lookupBySso($db, $ssoUid); if ($existing !== null) { $corpusId = self::ensureDefaultCorpus($db, $existing['client_id'], $email); return [ 'client_id' => $existing['client_id'], 'client_user_id' => $existing['client_user_id'], 'corpus_id' => $corpusId, 'created' => false, ]; } $db->beginTransaction(); try { $clientId = self::createClient($db, $ssoUid, $email, $displayName); $clientUserId = self::createOwnerUser($db, $clientId, $email, $displayName); $corpusId = self::ensureDefaultCorpus($db, $clientId, $email); self::subscribeIncludedPackages($db, $clientId); $db->commit(); } catch (Throwable $e) { $db->rollBack(); throw new DbnToolsHttpException( 'Could not provision dashboard tenant: ' . $e->getMessage(), 500, 'provision_failed' ); } return [ 'client_id' => $clientId, 'client_user_id' => $clientUserId, 'corpus_id' => $corpusId, 'created' => true, ]; } public static function ensureForCaveauSession(int $clientId, string $email): array { if ($clientId <= 0) { throw new DbnToolsHttpException('Caveau client_id is required.', 400, 'missing_client_id'); } dbnToolsBootCaveau(); $db = getDb(); $corpusId = self::ensureDefaultCorpus($db, $clientId, $email); return [ 'client_id' => $clientId, 'client_user_id' => 0, 'corpus_id' => $corpusId, 'created' => false, ]; } private static function lookupBySso(PDO $db, int $ssoUid): ?array { $stmt = $db->prepare('SELECT id FROM clients WHERE dbn_sso_uid = ? LIMIT 1'); $stmt->execute([$ssoUid]); $clientId = (int)($stmt->fetchColumn() ?: 0); if ($clientId === 0) { return null; } $stmt = $db->prepare( "SELECT id FROM client_users WHERE client_id = ? AND role = 'owner' AND is_active = 1 ORDER BY id ASC LIMIT 1" ); $stmt->execute([$clientId]); $userId = (int)($stmt->fetchColumn() ?: 0); return ['client_id' => $clientId, 'client_user_id' => $userId]; } private static function createClient(PDO $db, int $ssoUid, string $email, string $displayName): int { require_once dbnToolsAiPortalRoot() . '/platform/includes/client_auth.php'; $apiKey = generateApiKey(); $slug = self::uniqueSlug($db, 'dbn-user-' . $ssoUid); $name = $displayName !== '' ? $displayName : ('Dashboard ' . $email); $stmt = $db->prepare(" INSERT INTO clients ( dbn_sso_uid, slug, name, contact_email, country, timezone, plan, embedding_tier, api_key_hash, api_key_prefix, monthly_query_limit, monthly_document_limit, monthly_user_limit, is_active, plan_source, subscription_status ) VALUES ( ?, ?, ?, ?, 'NO', 'Europe/Oslo', 'sandbox', 'standard', ?, ?, 500, 50, 1, 1, 'signup', 'none' ) "); $stmt->execute([ $ssoUid, $slug, $name, $email, $apiKey['hash'], $apiKey['prefix'], ]); return (int)$db->lastInsertId(); } 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."); } $usernameBase = preg_replace('/[^a-z0-9._-]+/', '', strtolower(strstr($email, '@', true) ?: 'owner')); $usernameBase = $usernameBase !== '' ? $usernameBase : 'owner'; $username = self::uniqueUsername($db, $usernameBase); $stmt = $db->prepare(" INSERT INTO client_users (client_id, username, email, display_name, password_hash, role, is_active) VALUES (?, ?, ?, ?, ?, 'owner', 1) "); $stmt->execute([ $clientId, $username, $email, $displayName !== '' ? $displayName : null, password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT), ]); return (int)$db->lastInsertId(); } private static function ensureDefaultCorpus(PDO $db, int $clientId, string $email): int { $stmt = $db->prepare( 'SELECT id FROM client_corpora WHERE client_id = ? AND is_default = 1 ORDER BY id ASC LIMIT 1' ); $stmt->execute([$clientId]); $id = (int)($stmt->fetchColumn() ?: 0); if ($id > 0) { return $id; } $stmt = $db->prepare(" INSERT INTO client_corpora (client_id, name, slug, description, is_default) VALUES (?, ?, 'default', ?, 1) "); $stmt->execute([ $clientId, 'Min korpus', 'Default personal corpus for ' . $email, ]); return (int)$db->lastInsertId(); } private static function subscribeIncludedPackages(PDO $db, int $clientId): void { $packageSlug = dbnToolsRequiredPackageSlug(); $stmt = $db->prepare('SELECT id FROM corpus_packages WHERE slug = ? AND is_active = 1 LIMIT 1'); $stmt->execute([$packageSlug]); $packageId = (int)($stmt->fetchColumn() ?: 0); if ($packageId === 0) { return; } $db->prepare( "INSERT IGNORE INTO client_corpus_subscriptions (client_id, package_id, is_active, source, subscribed_at) VALUES (?, ?, 1, 'dbn_dashboard', NOW())" )->execute([$clientId, $packageId]); } private static function uniqueSlug(PDO $db, string $base): string { $base = preg_replace('/[^a-z0-9-]+/', '-', strtolower($base)) ?: 'dbn-user'; $base = trim($base, '-'); $stmt = $db->prepare('SELECT id FROM clients WHERE slug = ? LIMIT 1'); $slug = $base; $suffix = 2; while (true) { $stmt->execute([$slug]); if (!$stmt->fetch(PDO::FETCH_ASSOC)) { return $slug; } $slug = $base . '-' . $suffix; $suffix++; } } private static function uniqueUsername(PDO $db, string $base): string { $stmt = $db->prepare('SELECT id FROM client_users WHERE username = ? LIMIT 1'); $username = $base; $suffix = 2; while (true) { $stmt->execute([$username]); if (!$stmt->fetch(PDO::FETCH_ASSOC)) { return $username; } $username = $base . $suffix; $suffix++; } } }