From 7b4260bc90d3014436effd414c7df24f5da499b4 Mon Sep 17 00:00:00 2001 From: Adam Lamers Date: Fri, 17 Apr 2026 03:10:55 -0400 Subject: [PATCH] oidc working --- src/InnerProvider.svelte | 71 ++++++++++++++++++++++ src/InnerSpacetimeProvider.svelte | 53 +++++------------ src/auth/AuthGate.svelte | 27 ++++++++- src/auth/auth.svelte.ts | 29 +++++++-- src/config.ts | 99 +++++++++++++++---------------- 5 files changed, 184 insertions(+), 95 deletions(-) create mode 100644 src/InnerProvider.svelte diff --git a/src/InnerProvider.svelte b/src/InnerProvider.svelte new file mode 100644 index 0000000..1a5403e --- /dev/null +++ b/src/InnerProvider.svelte @@ -0,0 +1,71 @@ + + +{#if $db.identity} + {@render children()} +{:else} +
+ +
+{/if} diff --git a/src/InnerSpacetimeProvider.svelte b/src/InnerSpacetimeProvider.svelte index f836aa4..a5049d4 100644 --- a/src/InnerSpacetimeProvider.svelte +++ b/src/InnerSpacetimeProvider.svelte @@ -1,57 +1,36 @@ -{#if $db.identity} - {@render children()} +{#if builder} + + {@render children()} + {:else}
{/if} diff --git a/src/auth/AuthGate.svelte b/src/auth/AuthGate.svelte index 4dd11e2..6a4617b 100644 --- a/src/auth/AuthGate.svelte +++ b/src/auth/AuthGate.svelte @@ -30,14 +30,29 @@ combinedConnection = `${stdbHost.replace(/^(https?|wss?):\/\//, "")}:${stdbDbName}`; const isChanging = localStorage.getItem("zep_changing_server") === "true"; + const isInCallback = window.location.search.includes("code=") && window.location.search.includes("state="); + if (isChanging) { userWantsToConnect = false; - } else if (TokenStore.get(stdbHost, stdbDbName)) { - // Auto-connect if we have a token and NOT changing server + } else if (TokenStore.get(stdbHost, stdbDbName) || isInCallback) { + // Auto-connect ONLY if we have a SpacetimeDB token OR are in a redirect callback. + // We don't auto-connect just because of auth.isAuthenticated to prevent logout loops. userWantsToConnect = true; } }); + // Ensure we transition to connecting if we just finished an OIDC redirect + $effect(() => { + if (auth.isAuthenticated && !auth.isLoading) { + const isInCallback = window.location.search.includes("code=") && window.location.search.includes("state="); + if (isInCallback) { + untrack(() => { + userWantsToConnect = true; + }); + } + } + }); + // Split combined connection if it changes $effect(() => { if (combinedConnection.includes(":")) { @@ -59,7 +74,13 @@ combinedConnection = `${hostPart}:${stdbDbName}`; }); - const hasStoredToken = $derived(!!TokenStore.get(stdbHost, stdbDbName)); + let hasStoredToken = $state(false); + + $effect(() => { + // Check for token on mount and when connection params change + const _ = combinedConnection; + hasStoredToken = !!TokenStore.get(stdbHost, stdbDbName); + }); $effect(() => { if (stdbHost) localStorage.setItem(HOST_KEY, stdbHost); diff --git a/src/auth/auth.svelte.ts b/src/auth/auth.svelte.ts index b90a31d..3953da1 100644 --- a/src/auth/auth.svelte.ts +++ b/src/auth/auth.svelte.ts @@ -7,8 +7,8 @@ import { getEnv } from "../env"; // OIDC Configuration - User should replace these with their own provider values export const oidcConfig: UserManagerSettings = { - authority: getEnv("VITE_OIDC_AUTHORITY", "https://accounts.google.com"), - client_id: getEnv("VITE_OIDC_CLIENT_ID", "REPLACE_ME"), + authority: getEnv("VITE_OIDC_AUTHORITY", "https://sso.adamlamers.com/application/o/truenas-zep/"), + client_id: getEnv("VITE_OIDC_CLIENT_ID", "Omb5jIeYY2rJqkxK52qZgLRGejOBCK5Km2BcA5RO"), redirect_uri: window.location.origin, scope: "openid profile email", response_type: "code", @@ -18,6 +18,7 @@ class AuthStore { #userManager: UserManager; #user = $state(null); #isLoading = $state(true); + #isProcessingCallback = false; constructor(settings: UserManagerSettings) { this.#userManager = new UserManager(settings); @@ -27,8 +28,19 @@ class AuthStore { window.location.search.includes("code=") && window.location.search.includes("state=") ) { - this.signinCallback(); + if (!this.#isProcessingCallback) { + this.signinCallback(); + } } else { + // Not in a callback, safe to clear any stale/redundant OIDC state keys + // that might have accumulated from partial attempts + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith("oidc.")) { + localStorage.removeItem(key); + } + } + this.#userManager .getUser() .then((user) => { @@ -71,10 +83,19 @@ class AuthStore { } async signinCallback() { + if (this.#isProcessingCallback) return; + this.#isProcessingCallback = true; this.#isLoading = true; try { const user = await this.#userManager.signinCallback(); this.#user = user; + + // Stage the id_token so SpacetimeDB can find it even if OIDC state is cleared + if (user.id_token) { + console.log("AuthStore: Staging OIDC id_token for SpacetimeDB handshake"); + localStorage.setItem("zep_oidc_staging_token", user.id_token); + } + window.history.replaceState({}, document.title, window.location.pathname); } catch (error) { console.error("Signin callback error:", error); @@ -101,7 +122,7 @@ class AuthStore { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); - if (key && key.includes("auth_token")) { + if (key && (key.includes("stdb_token:") || key.includes("auth_token") || key === "zep_oidc_staging_token")) { keysToRemove.push(key); } } diff --git a/src/config.ts b/src/config.ts index 16e2c04..3441d9c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -45,35 +45,20 @@ export const TokenStore = { const key = localStorage.key(i); if (!key) continue; - let connStr: string | null = null; - if (key.startsWith("stdb_token:")) { - // New format: stdb_token:protocol://host[:port]:database const parts = key.split(":"); if (parts.length >= 4) { const dbName = parts[parts.length - 1]; const hostParts = parts.slice(2, parts.length - 1); const host = hostParts.join(":").replace(/^\/\//, ""); - connStr = `${host}:${dbName}`; - } - } else if (key.endsWith("/auth_token")) { - // Legacy format: protocol://host[:port]/database/auth_token - // Example: http://localhost:3000/zep/auth_token - const parts = key.split("/"); - // parts: ["http:", "", "localhost:3000", "zep", "auth_token"] - if (parts.length >= 5) { - const dbName = parts[parts.length - 2]; - const host = parts[2]; - if (dbName && host) { - connStr = `${host}:${dbName}`; + const connStr = `${host}:${dbName}`; + + if (!seen.has(connStr)) { + connections.push(connStr); + seen.add(connStr); } } } - - if (connStr && !seen.has(connStr)) { - connections.push(connStr); - seen.add(connStr); - } } return connections; } @@ -102,6 +87,39 @@ export const stopActiveConnection = () => { } }; +export const handleConnect = (conn: DbConnection, identity: any, token: string) => { + const host = getStdbHost(); + const dbName = getStdbDbName(); + + console.log(`handleConnect: Connection established! Token received (len: ${token?.length || 0})`); + + _connection = conn; + if (token) { + console.log("handleConnect: Persisting token to TokenStore..."); + TokenStore.set(host, dbName, token); + + // Clear staging token now that we have a permanent one + localStorage.removeItem("zep_oidc_staging_token"); + } else { + console.warn("handleConnect: No token received from SpacetimeDB!"); + } + + connectionState.status = "connected"; + connectionState.hasConnectedOnce = true; + connectionState.error = null; + + // Call the auth update reducer to ensure OIDC info is synced + setTimeout(() => { + if (_connection) { + console.log("handleConnect: Requesting auth info update..."); + // Use the typed reducer call if available + if ((_connection.reducers as any).updateAuthInfo) { + (_connection.reducers as any).updateAuthInfo({}); + } + } + }, 100); +}; + class ConnectionManager { #retryCount = 0; #reconnectTimeout: any = null; @@ -197,50 +215,29 @@ export const connectionBuilder = (oidcToken?: string) => { const host = normalizeHost(rawHost); const dbName = getStdbDbName(); - console.log(`connectionBuilder: Using host: ${host} (raw: ${rawHost}), database: ${dbName}`); - - connectionState.status = "connecting"; - - if (activeManager) { - console.log("connectionBuilder: Stopping previous activeManager"); - activeManager.stop(); - } - - const manager = new ConnectionManager(host, dbName); - activeManager = manager; + console.log(`connectionBuilder: Creating builder for host: ${host}, database: ${dbName}`); const builder = DbConnection.builder() .withUri(host) .withDatabaseName(dbName); const storedToken = TokenStore.get(host, dbName); - console.log("connectionBuilder: oidcToken:", oidcToken ? "present" : "absent"); - console.log("connectionBuilder: storedToken:", storedToken ? "present" : "absent"); + const stagingToken = localStorage.getItem("zep_oidc_staging_token"); + + console.log("connectionBuilder: tokens -> oidc:", oidcToken ? "yes" : "no", "staging:", stagingToken ? "yes" : "no", "stored:", storedToken ? "yes" : "no"); if (oidcToken) { - console.log("connectionBuilder: Calling withToken with oidcToken"); builder.withToken(oidcToken); + } else if (stagingToken) { + console.log("connectionBuilder: Using staged OIDC token"); + builder.withToken(stagingToken); } else if (storedToken) { - console.log("connectionBuilder: Calling withToken with storedToken"); builder.withToken(storedToken); - } else { - console.log("connectionBuilder: No token, will connect anonymously"); } - // Register our manager's listeners directly on the connection object. - // This ensures they coexist with the Svelte provider's listeners. - builder.onConnect((c: any, id: any, token: string) => { - console.log("connectionBuilder: CONNECTION onConnect TRIGGERED!"); - manager.onConnect(c as any, id, token); - }); - builder.onDisconnect((_ctx: any, err: any) => { - console.log("connectionBuilder: CONNECTION onDisconnect TRIGGERED!", err); - manager.onDisconnect(); - }); - builder.onConnectError((ctx: any, err: any) => { - console.log("connectionBuilder: CONNECTION onConnectError TRIGGERED!", err); - manager.onConnectError(ctx, err); - }); + builder.onConnect((conn: any, identity: any, token: string) => { + handleConnect(conn, identity, token); + }); return builder; };