select spacetime server
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/config.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+3
-2
@@ -10,10 +10,11 @@
|
||||
"lint": "eslint . && prettier . --check --ignore-path ../../.prettierignore",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"generate": "cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings",
|
||||
"spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb",
|
||||
"spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local",
|
||||
"spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud"
|
||||
"spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud",
|
||||
"deploy:local": "docker compose -f docker-compose.local.yml up --build",
|
||||
"deploy:maincloud": "docker compose -f docker-compose.maincloud.yml up --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
|
||||
+15
-8
@@ -1,21 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { AuthGate } from "./auth";
|
||||
import { ChatContainer } from "./chat";
|
||||
import SpacetimeProvider from "./SpacetimeProvider.svelte";
|
||||
import "./App.css";
|
||||
|
||||
let reconnectKey = $state(0);
|
||||
let showServerSettings = $state(false);
|
||||
|
||||
function handleReconnect() {
|
||||
console.log("App: Triggering reconnection...");
|
||||
reconnectKey++;
|
||||
showServerSettings = false;
|
||||
}
|
||||
|
||||
function handleToggleServerSettings(val: boolean) {
|
||||
console.log("App: Setting showServerSettings to", val);
|
||||
showServerSettings = val;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key reconnectKey}
|
||||
<SpacetimeProvider onReconnectTrigger={handleReconnect}>
|
||||
<AuthGate>
|
||||
<ChatContainer />
|
||||
</AuthGate>
|
||||
</SpacetimeProvider>
|
||||
{/key}
|
||||
<AuthGate
|
||||
reconnect={handleReconnect}
|
||||
{showServerSettings}
|
||||
onToggleServerSettings={handleToggleServerSettings}
|
||||
{reconnectKey}
|
||||
>
|
||||
<ChatContainer onShowServerSettings={() => handleToggleServerSettings(true)} />
|
||||
</AuthGate>
|
||||
|
||||
+141
-7
@@ -1,16 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { auth } from "./auth.svelte";
|
||||
import { TOKEN_KEY } from "../config";
|
||||
import { HOST_KEY, DB_NAME_KEY, getEnv, TokenStore, getStdbHost, getStdbDbName } from "../config";
|
||||
import SpacetimeProvider from "../SpacetimeProvider.svelte";
|
||||
|
||||
let { children } = $props<{ children: any }>();
|
||||
let { children, reconnect, showServerSettings, onToggleServerSettings, reconnectKey } = $props<{
|
||||
children: any,
|
||||
reconnect: () => void,
|
||||
showServerSettings: boolean,
|
||||
onToggleServerSettings: (val: boolean) => void,
|
||||
reconnectKey: number
|
||||
}>();
|
||||
|
||||
let authError = $state<string | null>(null);
|
||||
let hasStoredToken = $state(false);
|
||||
let isGuest = $state(false);
|
||||
|
||||
let stdbHost = $state("");
|
||||
let stdbDbName = $state("");
|
||||
|
||||
const MAINCLOUD_URI = "wss://maincloud.spacetimedb.com";
|
||||
let isMaincloud = $state(true);
|
||||
|
||||
onMount(() => {
|
||||
hasStoredToken = !!localStorage.getItem(TOKEN_KEY);
|
||||
stdbHost = getStdbHost();
|
||||
stdbDbName = getStdbDbName();
|
||||
isMaincloud = stdbHost === MAINCLOUD_URI || stdbHost === "";
|
||||
if (isMaincloud) stdbHost = MAINCLOUD_URI;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (isMaincloud) {
|
||||
stdbHost = MAINCLOUD_URI;
|
||||
}
|
||||
});
|
||||
|
||||
const hasStoredToken = $derived(!!TokenStore.get(stdbHost, stdbDbName));
|
||||
|
||||
$effect(() => {
|
||||
if (stdbHost) localStorage.setItem(HOST_KEY, stdbHost);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (stdbDbName) localStorage.setItem(DB_NAME_KEY, stdbDbName);
|
||||
});
|
||||
|
||||
function handleAuthError(error: string | null) {
|
||||
@@ -22,6 +53,7 @@
|
||||
new URLSearchParams(window.location.search).has("bypass_auth");
|
||||
|
||||
const isAuthenticated = $derived(auth.isAuthenticated || hasStoredToken || isGuest || isBypassEnabled);
|
||||
const shouldShowContent = $derived(isAuthenticated && !showServerSettings);
|
||||
|
||||
$effect(() => {
|
||||
console.log("AuthGate: auth.isLoading:", auth.isLoading);
|
||||
@@ -36,8 +68,12 @@
|
||||
<div class="login-screen">
|
||||
<h1>Loading Authentication...</h1>
|
||||
</div>
|
||||
{:else if isAuthenticated}
|
||||
{@render children()}
|
||||
{:else if shouldShowContent}
|
||||
{#key reconnectKey}
|
||||
<SpacetimeProvider onReconnectTrigger={reconnect}>
|
||||
{@render children()}
|
||||
</SpacetimeProvider>
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="login-screen">
|
||||
<div class="login-card">
|
||||
@@ -62,9 +98,107 @@
|
||||
class="btn-secondary"
|
||||
style="width: 100%"
|
||||
>
|
||||
Continue as a guest
|
||||
{hasStoredToken ? "Reconnect as last user" : "Continue as a guest"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="server-settings-section" style="margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--background-modifier-accent); width: 100%">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h3 style="font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; margin: 0;">Server Settings</h3>
|
||||
<button
|
||||
onclick={() => {
|
||||
reconnect();
|
||||
onToggleServerSettings(false);
|
||||
}}
|
||||
class="btn-ghost"
|
||||
style="padding: 2px 8px; font-size: 0.7rem; text-transform: uppercase; color: var(--brand);"
|
||||
>
|
||||
Apply & Reconnect
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<div class="form-group" style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
||||
<label for="stdb-host" style="font-size: 0.7rem; font-weight: bold; color: var(--text-muted); text-transform: uppercase;">SpacetimeDB Host</label>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; font-weight: bold;">Maincloud</span>
|
||||
<label class="ios-switch">
|
||||
<input type="checkbox" bind:checked={isMaincloud} />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
id="stdb-host"
|
||||
type="text"
|
||||
bind:value={stdbHost}
|
||||
disabled={isMaincloud}
|
||||
placeholder="wss://maincloud.spacetimedb.com"
|
||||
style="background-color: var(--background-tertiary); color: {isMaincloud ? 'var(--text-muted)' : 'var(--text-normal)'}; border: 1px solid var(--background-modifier-accent); border-radius: 4px; padding: 10px; cursor: {isMaincloud ? 'not-allowed' : 'text'}; transition: all 0.2s;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<label for="stdb-db" style="font-size: 0.7rem; font-weight: bold; color: var(--text-muted); text-transform: uppercase;">Database Name</label>
|
||||
<input
|
||||
id="stdb-db"
|
||||
type="text"
|
||||
bind:value={stdbDbName}
|
||||
placeholder="my-spacetime-app-jdhdg"
|
||||
style="background-color: var(--background-tertiary); color: var(--text-normal); border: 1px solid var(--background-modifier-accent); border-radius: 4px; padding: 10px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.ios-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 34px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.ios-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #3f4147;
|
||||
transition: .4s;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--status-positive);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
type User,
|
||||
type UserManagerSettings,
|
||||
} from "oidc-client-ts";
|
||||
import { getEnv } from "../config";
|
||||
|
||||
// OIDC Configuration - User should replace these with their own provider values
|
||||
export const oidcConfig: UserManagerSettings = {
|
||||
authority:
|
||||
import.meta.env.VITE_OIDC_AUTHORITY ?? "https://accounts.google.com",
|
||||
client_id: import.meta.env.VITE_OIDC_CLIENT_ID ?? "REPLACE_ME",
|
||||
authority: getEnv("VITE_OIDC_AUTHORITY", "https://accounts.google.com"),
|
||||
client_id: getEnv("VITE_OIDC_CLIENT_ID", "REPLACE_ME"),
|
||||
redirect_uri: window.location.origin,
|
||||
scope: "openid profile email",
|
||||
response_type: "code",
|
||||
@@ -98,12 +98,14 @@ class AuthStore {
|
||||
this.#isLoading = true;
|
||||
try {
|
||||
// Clear all potential SpacetimeDB tokens from local storage
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.includes("auth_token")) {
|
||||
localStorage.removeItem(key);
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
|
||||
if (this.#user) {
|
||||
await this.#userManager.signoutRedirect();
|
||||
@@ -113,6 +115,8 @@ class AuthStore {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error);
|
||||
// Fallback reload if signout fails
|
||||
window.location.reload();
|
||||
} finally {
|
||||
this.#isLoading = false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
import SettingsPanel from "./components/SettingsPanel.svelte";
|
||||
import ImageViewer from "./components/ImageViewer.svelte";
|
||||
|
||||
let { onShowServerSettings } = $props<{ onShowServerSettings: () => void }>();
|
||||
|
||||
const spacetime = useSpacetimeDB();
|
||||
// identity is guaranteed to be non-null here because of the guard in App.svelte
|
||||
const identity = $derived($spacetime.identity!);
|
||||
@@ -45,7 +47,7 @@
|
||||
<div class="chat-container" onclick={handleGlobalClick}>
|
||||
<div class="left-sidebar-wrapper">
|
||||
<div class="left-sidebar-top">
|
||||
<ServerList />
|
||||
<ServerList {onShowServerSettings} />
|
||||
|
||||
<div class="sidebar-container">
|
||||
<ChannelList />
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import type { ChatService } from "../services/chat.svelte";
|
||||
|
||||
const chat = getContext<ChatService>("chat");
|
||||
|
||||
let { onShowServerSettings } = $props<{ onShowServerSettings: () => void }>();
|
||||
</script>
|
||||
|
||||
<div class="server-list">
|
||||
@@ -33,6 +35,18 @@
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
|
||||
<div style="margin-top: auto; display: flex; flex-direction: column; gap: 8px; align-items: center; width: 100%;">
|
||||
<div class="sidebar-separator" style="width: 32px; margin: 0; background-color: var(--background-modifier-accent); height: 2px;"></div>
|
||||
<button
|
||||
class="server-icon"
|
||||
onclick={onShowServerSettings}
|
||||
title="Switch Server Configuration"
|
||||
style="color: var(--brand); margin-bottom: 12px;"
|
||||
>
|
||||
<i class="fas fa-network-wired"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Create Server Modal -->
|
||||
{#if chat.showCreateServerModal}
|
||||
<div
|
||||
@@ -98,6 +112,10 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.server-icon:hover {
|
||||
|
||||
+55
-11
@@ -2,11 +2,46 @@
|
||||
import { DbConnection } from "./module_bindings/index.ts";
|
||||
import { auth } from "./auth";
|
||||
|
||||
export const HOST =
|
||||
import.meta.env.VITE_SPACETIMEDB_HOST ?? "wss://maincloud.spacetimedb.com";
|
||||
export const DB_NAME =
|
||||
import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? "my-spacetime-app-jdhdg";
|
||||
export const TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`;
|
||||
export const getEnv = (key: string, defaultValue: string) => {
|
||||
const windowEnv = (window as any).__ENV__;
|
||||
if (windowEnv && windowEnv[key] && windowEnv[key] !== "") {
|
||||
return windowEnv[key];
|
||||
}
|
||||
if (import.meta.env[key] && import.meta.env[key] !== "") {
|
||||
return import.meta.env[key];
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export const HOST_KEY = "stdb_host";
|
||||
export const DB_NAME_KEY = "stdb_db_name";
|
||||
|
||||
const normalizeHost = (host: string) => {
|
||||
try {
|
||||
const url = new URL(host.includes("://") ? host : `wss://${host}`);
|
||||
return url.origin.replace(/^http/, 'ws'); // Ensure ws/wss
|
||||
} catch (e) {
|
||||
return host.trim().replace(/\/+$/, ""); // Fallback
|
||||
}
|
||||
};
|
||||
|
||||
export const TokenStore = {
|
||||
get: (host: string, dbName: string) => {
|
||||
const key = `${normalizeHost(host)}/${dbName}/auth_token`;
|
||||
return localStorage.getItem(key);
|
||||
},
|
||||
set: (host: string, dbName: string, token: string) => {
|
||||
const key = `${normalizeHost(host)}/${dbName}/auth_token`;
|
||||
localStorage.setItem(key, token);
|
||||
},
|
||||
clear: (host: string, dbName: string) => {
|
||||
const key = `${normalizeHost(host)}/${dbName}/auth_token`;
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
};
|
||||
|
||||
export const getStdbHost = () => localStorage.getItem(HOST_KEY) || getEnv("VITE_SPACETIMEDB_HOST", "wss://maincloud.spacetimedb.com");
|
||||
export const getStdbDbName = () => localStorage.getItem(DB_NAME_KEY) || getEnv("VITE_SPACETIMEDB_DB_NAME", "my-spacetime-app-jdhdg");
|
||||
|
||||
let _connection: DbConnection | null = null;
|
||||
export const getConnection = () => _connection;
|
||||
@@ -15,14 +50,18 @@ class ConnectionManager {
|
||||
#retryCount = 0;
|
||||
#reconnectTimeout: any = null;
|
||||
#onReconnectTrigger: () => void;
|
||||
#host: string;
|
||||
#dbName: string;
|
||||
|
||||
constructor(onReconnectTrigger: () => void) {
|
||||
constructor(host: string, dbName: string, onReconnectTrigger: () => void) {
|
||||
this.#host = host;
|
||||
this.#dbName = dbName;
|
||||
this.#onReconnectTrigger = onReconnectTrigger;
|
||||
}
|
||||
|
||||
onConnect = (conn: DbConnection, identity: any, token: string) => {
|
||||
_connection = conn;
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
TokenStore.set(this.#host, this.#dbName, token);
|
||||
console.log(
|
||||
"Connected to SpacetimeDB with identity:",
|
||||
identity.toHexString(),
|
||||
@@ -63,15 +102,20 @@ class ConnectionManager {
|
||||
}
|
||||
|
||||
export const connectionBuilder = (onReconnectTrigger: () => void) => {
|
||||
const manager = new ConnectionManager(onReconnectTrigger);
|
||||
const host = getStdbHost();
|
||||
const dbName = getStdbDbName();
|
||||
|
||||
console.log(`Connecting to SpacetimeDB: ${host}/${dbName}`);
|
||||
|
||||
const manager = new ConnectionManager(host, dbName, onReconnectTrigger);
|
||||
const builder = DbConnection.builder()
|
||||
.withUri(HOST)
|
||||
.withDatabaseName(DB_NAME)
|
||||
.withUri(host)
|
||||
.withDatabaseName(dbName)
|
||||
.onConnect(manager.onConnect)
|
||||
.onDisconnect(manager.onDisconnect)
|
||||
.onConnectError(manager.onConnectError);
|
||||
|
||||
const storedToken = localStorage.getItem(TOKEN_KEY);
|
||||
const storedToken = TokenStore.get(host, dbName);
|
||||
|
||||
if (auth.isAuthenticated && auth.user?.id_token) {
|
||||
console.log("SpacetimeDBWrapper: Connecting with OIDC token");
|
||||
|
||||
Reference in New Issue
Block a user