oidc working
This commit is contained in:
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user