diff --git a/includes/bootstrap.php b/includes/bootstrap.php index edafbe6..20668c2 100644 --- a/includes/bootstrap.php +++ b/includes/bootstrap.php @@ -110,12 +110,33 @@ dbnToolsStartSession(); function dbnToolsIsAuthenticated(): bool { + // SSO session established via dobetternorge.no signed token + if (!empty($_SESSION['dbn_tools_authenticated']) && !empty($_SESSION['dbn_tools_sso_uid'])) { + return true; + } + // Regular Caveau session return !empty($_SESSION['dbn_tools_authenticated']) && !empty($_SESSION['dbn_tools_user_id']) && !empty($_SESSION['dbn_tools_client_id']) && (string)($_SESSION['dbn_tools_client_slug'] ?? '') === dbnToolsClientSlug(); } +/** + * Validates a signed SSO token from dobetternorge.no. + * Returns the decoded payload array or null on failure. + */ +function dbnToolsValidateSsoToken(string $token, string $secret): ?array +{ + $parts = explode('.', $token, 2); + if (count($parts) !== 2) return null; + [$payload, $sig] = $parts; + if (!hash_equals(hash_hmac('sha256', $payload, $secret), $sig)) return null; + $data = json_decode(base64_decode(strtr($payload, '-_', '+/')), true); + if (!is_array($data) || ($data['exp'] ?? 0) < time()) return null; + if (empty($data['tools_approved'])) return null; + return $data; +} + function dbnToolsAuthenticatedUser(): ?array { if (!dbnToolsIsAuthenticated()) { diff --git a/index.php b/index.php index e7f1b68..f32ada6 100644 --- a/index.php +++ b/index.php @@ -3,6 +3,28 @@ declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; +// Handle SSO token from dobetternorge.no +if (isset($_GET['sso']) && !dbnToolsIsAuthenticated()) { + $ssoSecret = (string) dbnToolsEnv('DBN_SSO_SECRET', ''); + if ($ssoSecret !== '') { + $tokenData = dbnToolsValidateSsoToken((string)$_GET['sso'], $ssoSecret); + if ($tokenData !== null) { + session_regenerate_id(true); + $_SESSION['dbn_tools_authenticated'] = true; + $_SESSION['dbn_tools_authenticated_at'] = time(); + $_SESSION['dbn_tools_sso_uid'] = (int)$tokenData['uid']; + $_SESSION['dbn_tools_user_id'] = (int)$tokenData['uid']; + $_SESSION['dbn_tools_user_email'] = (string)$tokenData['email']; + $_SESSION['dbn_tools_user_role'] = 'sso'; + header('Location: ask.php'); + exit; + } + } + // Invalid/expired token — redirect back to main site to re-login + header('Location: https://dobetternorge.no/account.php?error=' . urlencode('Session expired. Please log in and try again.')); + exit; +} + if (dbnToolsIsAuthenticated()) { $return = $_GET['return'] ?? ''; $dest = ($return && str_starts_with($return, '/') && !str_contains($return, '//')) @@ -144,6 +166,15 @@ if (dbnToolsIsAuthenticated()) {

Do Better Norge

Access Legal Tools

Legal information and preparation support, not final legal advice.

+ +
+ Do Better Norge member? + Log in at dobetternorge.no →
+ Then open Tools from your account dashboard +
+ +
OR SIGN IN WITH CAVEAU ACCOUNT
+
@@ -154,6 +185,11 @@ if (dbnToolsIsAuthenticated()) {

+ +

+ No account yet? + Register free at dobetternorge.no +

diff --git a/scripts/setup-dbn-mcp.php b/scripts/setup-dbn-mcp.php new file mode 100644 index 0000000..e76e88c --- /dev/null +++ b/scripts/setup-dbn-mcp.php @@ -0,0 +1,168 @@ +prepare( + 'SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?' + ); + $stmt->execute([$table, $column]); + return (int)$stmt->fetchColumn() > 0; +} + +function dbnMcpTableExists(PDO $db, string $table): bool +{ + $stmt = $db->prepare( + 'SELECT COUNT(*) + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?' + ); + $stmt->execute([$table]); + return (int)$stmt->fetchColumn() > 0; +} + +function dbnMcpRequireTables(PDO $db, array $tables): void +{ + $missing = []; + foreach ($tables as $table) { + if (!dbnMcpTableExists($db, $table)) { + $missing[] = $table; + } + } + if ($missing !== []) { + throw new RuntimeException( + 'Missing required MCP tables: ' . implode(', ', $missing) . + '. Apply the Caveau MCP and Azure Search migrations before running this script.' + ); + } +} + +function dbnMcpUpsertFeature(PDO $db, int $clientId, string $feature, int $enabled): void +{ + $stmt = $db->prepare( + 'INSERT INTO client_feature_overrides (client_id, feature, enabled) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE enabled = VALUES(enabled)' + ); + $stmt->execute([$clientId, $feature, $enabled]); +} + +try { + $db = dbnToolsDb(); + dbnMcpRequireTables($db, [ + 'client_mcp_config', + 'client_mcp_tokens', + 'client_corpus_subscriptions', + 'client_feature_overrides', + ]); + + $client = dbnToolsFetchClient($db); + if (!$client || empty($client['is_active'])) { + throw new RuntimeException('Do Better Norge client tenant is not active or was not found.'); + } + + $clientId = (int)$client['id']; + $packageSlug = dbnToolsRequiredPackageSlug(); + $package = dbnToolsFetchPackage($packageSlug, $db); + if (!$package || empty($package['is_active'])) { + throw new RuntimeException("Package {$packageSlug} is not active or was not found."); + } + if ((int)($package['corpus_id'] ?? 0) !== 1) { + throw new RuntimeException("Package {$packageSlug} must point at corpus_id=1 for DBN MCP v1."); + } + + $db->beginTransaction(); + + $stmt = $db->prepare( + 'INSERT INTO client_corpus_subscriptions (client_id, package_id, is_active, source, subscribed_at) + VALUES (?, ?, 1, ?, NOW()) + ON DUPLICATE KEY UPDATE is_active = VALUES(is_active), source = VALUES(source), cancelled_at = NULL' + ); + $stmt->execute([$clientId, (int)$package['id'], 'dbn-mcp-v1']); + + $configColumns = [ + 'client_id' => $clientId, + 'is_enabled' => 1, + 'endpoint_slug' => 'dobetter', + ]; + if (dbnMcpColumnExists($db, 'client_mcp_config', 'inspector_enabled')) { + $configColumns['inspector_enabled'] = 1; + } + if (dbnMcpColumnExists($db, 'client_mcp_config', 'inspector_retention_days')) { + $configColumns['inspector_retention_days'] = 30; + } + $names = array_keys($configColumns); + $updates = implode(', ', array_map(static fn(string $name): string => "{$name} = VALUES({$name})", $names)); + $stmt = $db->prepare( + 'INSERT INTO client_mcp_config (' . implode(', ', $names) . ') + VALUES (' . implode(', ', array_fill(0, count($names), '?')) . ') + ON DUPLICATE KEY UPDATE ' . $updates + ); + $stmt->execute(array_values($configColumns)); + + dbnMcpUpsertFeature($db, $clientId, 'azure_search', 1); + + $existingToken = $db->prepare( + "SELECT id, token_prefix, name, created_at + FROM client_mcp_tokens + WHERE client_id = ? AND is_active = 1 AND revoked_at IS NULL + AND name = 'DBN MCP v1' + ORDER BY id DESC + LIMIT 1" + ); + $existingToken->execute([$clientId]); + $tokenRow = $existingToken->fetch(PDO::FETCH_ASSOC); + + $plaintext = null; + if (!$tokenRow) { + $plaintext = 'dbn_mcp_' . bin2hex(random_bytes(32)); + $tokenHash = hash('sha256', $plaintext); + $prefix = substr($plaintext, 0, 16); + $scopes = json_encode([ + 'tools' => ['dbn.search_legal', 'dbn.compare_retrieval', 'dbn.list_sources', 'dbn.get_document'], + 'backend' => ['azure-search', 'qdrant'], + 'corpus_id' => [1], + ], JSON_UNESCAPED_SLASHES); + + $stmt = $db->prepare( + 'INSERT INTO client_mcp_tokens (client_id, token_hash, token_prefix, name, scopes, is_active, created_at) + VALUES (?, ?, ?, ?, ?, 1, NOW())' + ); + $stmt->execute([$clientId, $tokenHash, $prefix, 'DBN MCP v1', $scopes]); + $tokenRow = [ + 'id' => (int)$db->lastInsertId(), + 'token_prefix' => $prefix, + 'name' => 'DBN MCP v1', + 'created_at' => date('Y-m-d H:i:s'), + ]; + } + + $db->commit(); + + echo "DBN MCP configured.\n"; + echo "Client: " . dbnToolsClientSlug() . " (#{$clientId})\n"; + echo "Package: {$packageSlug} (#{$package['id']}) corpus_id={$package['corpus_id']}\n"; + echo "MCP config: enabled endpoint_slug=dobetter\n"; + echo "Azure Search feature: enabled\n"; + echo "Token: {$tokenRow['name']} (#{$tokenRow['id']}) prefix={$tokenRow['token_prefix']}\n"; + if ($plaintext !== null) { + echo "Plaintext token (shown once): {$plaintext}\n"; + } else { + echo "Plaintext token: not shown; an active DBN MCP v1 token already exists.\n"; + } +} catch (Throwable $e) { + if (isset($db) && $db instanceof PDO && $db->inTransaction()) { + $db->rollBack(); + } + fwrite(STDERR, "DBN MCP setup failed: {$e->getMessage()}\n"); + exit(1); +}