oidc working

This commit is contained in:
2026-04-17 03:10:55 -04:00
parent 49a325ebee
commit 7b4260bc90
5 changed files with 184 additions and 95 deletions
+71
View File
@@ -0,0 +1,71 @@
<script lang="ts">
import { createSpacetimeDBProvider } from "spacetimedb/svelte";
import type { ConnectionBuilder } from "spacetimedb";
import { getStdbHost, getStdbDbName, handleConnect } from "./config";
let { builder, children, onCancel }: { builder: ConnectionBuilder<any>, children: any, onCancel?: () => void } = $props();
// Initialize the provider
const db = createSpacetimeDBProvider(builder);
// Fallback: Also register direct listener on the connection if possible
$effect(() => {
if ($db.connection) {
$db.connection.onConnect((conn: any, identity: any, token: string) => {
console.log("InnerProvider: connection.onConnect triggered, syncing state...");
handleConnect(conn, identity, token);
});
}
});
// Watch for successful connection and persist state
$effect(() => {
console.log("InnerProvider State:", {
isActive: $db.isActive,
hasIdentity: !!$db.identity,
hasToken: !!$db.token,
hasConn: !!$db.connection
});
if ($db.isActive && $db.identity && $db.connection) {
console.log("InnerProvider: Connection valid, syncing state...");
handleConnect($db.connection, $db.identity, $db.token || "");
}
});
const host = getStdbHost();
const dbName = getStdbDbName();
</script>
{#if $db.identity}
{@render children()}
{:else}
<div class="login-screen">
<div class="login-card" style="text-align: center;">
<i class="fas fa-circle-notch fa-spin" style="font-size: 3rem; color: var(--brand); margin-bottom: 20px;"></i>
<h1>Connecting to SpacetimeDB...</h1>
<p style="color: var(--text-muted); margin-top: 8px; margin-bottom: 24px;">Establishing a secure connection to the chat server.</p>
<div style="background-color: var(--background-tertiary); padding: 16px; border-radius: 8px; margin-bottom: 24px; text-align: left; font-size: 0.8rem; border: 1px solid var(--background-modifier-accent); display: flex; flex-direction: column; gap: 12px; width: 100%; box-sizing: border-box;">
<div class="connection-detail">
<div style="color: var(--text-muted); font-weight: 800; text-transform: uppercase; font-size: 0.65rem; margin-bottom: 4px; letter-spacing: 0.05em;">Host</div>
<div style="color: var(--text-normal); font-family: var(--font-code); word-break: break-all; line-height: 1.4;">{host}</div>
</div>
<div class="connection-detail">
<div style="color: var(--text-muted); font-weight: 800; text-transform: uppercase; font-size: 0.65rem; margin-bottom: 4px; letter-spacing: 0.05em;">Database</div>
<div style="color: var(--text-normal); font-family: var(--font-code); word-break: break-all; line-height: 1.4;">{dbName}</div>
</div>
</div>
{#if onCancel}
<button
onclick={onCancel}
class="btn-secondary"
style="width: 100%;"
>
Cancel
</button>
{/if}
</div>
</div>
{/if}
+16 -37
View File
@@ -1,57 +1,36 @@
<script lang="ts">
import { createSpacetimeDBProvider } from "spacetimedb/svelte";
import { connectionBuilder, getStdbHost, getStdbDbName } from "./config";
import { connectionBuilder, handleConnect } from "./config";
import { auth } from "./auth/auth.svelte";
import { connectionState } from "./connection.svelte";
import InnerProvider from "./InnerProvider.svelte";
import type { ConnectionBuilder } from "spacetimedb";
let { children, onCancel } = $props<{
children: any,
onCancel?: () => void
}>();
const host = getStdbHost();
const dbName = getStdbDbName();
// Initialize SpacetimeDB provider
const db = createSpacetimeDBProvider(connectionBuilder(
auth.user?.id_token
));
let builder = $state<ConnectionBuilder<any> | null>(null);
$effect(() => {
console.log("InnerSpacetimeProvider: $db.isActive:", $db.isActive);
console.log("InnerSpacetimeProvider: $db.identity:", $db.identity?.toHexString());
console.log("InnerSpacetimeProvider: $db.token:", $db.token ? "present" : "absent");
if (!auth.isLoading) {
console.log("InnerSpacetimeProvider: Initializing connection...");
connectionState.status = "connecting";
builder = connectionBuilder(auth.user?.id_token);
}
});
</script>
{#if $db.identity}
{@render children()}
{#if builder}
<InnerProvider {builder} {onCancel}>
{@render children()}
</InnerProvider>
{:else}
<div class="login-screen">
<div class="login-card" style="text-align: center;">
<i class="fas fa-circle-notch fa-spin" style="font-size: 3rem; color: var(--brand); margin-bottom: 20px;"></i>
<h1>Connecting to SpacetimeDB...</h1>
<p style="color: var(--text-muted); margin-top: 8px; margin-bottom: 24px;">Establishing a secure connection to the chat server.</p>
<div style="background-color: var(--background-tertiary); padding: 16px; border-radius: 8px; margin-bottom: 24px; text-align: left; font-size: 0.8rem; border: 1px solid var(--background-modifier-accent); display: flex; flex-direction: column; gap: 12px; width: 100%; box-sizing: border-box;">
<div class="connection-detail">
<div style="color: var(--text-muted); font-weight: 800; text-transform: uppercase; font-size: 0.65rem; margin-bottom: 4px; letter-spacing: 0.05em;">Host</div>
<div style="color: var(--text-normal); font-family: var(--font-code); word-break: break-all; line-height: 1.4;">{host}</div>
</div>
<div class="connection-detail">
<div style="color: var(--text-muted); font-weight: 800; text-transform: uppercase; font-size: 0.65rem; margin-bottom: 4px; letter-spacing: 0.05em;">Database</div>
<div style="color: var(--text-normal); font-family: var(--font-code); word-break: break-all; line-height: 1.4;">{dbName}</div>
</div>
</div>
{#if onCancel}
<button
onclick={onCancel}
class="btn-secondary"
style="width: 100%;"
>
Cancel
</button>
{/if}
<h1>Authenticating...</h1>
<p style="color: var(--text-muted); margin-top: 8px;">Waiting for identity provider callback.</p>
</div>
</div>
{/if}
+24 -3
View File
@@ -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);
+25 -4
View File
@@ -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<User | null | undefined>(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);
}
}
+48 -51
View File
@@ -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;
};