460 lines
13 KiB
Svelte
460 lines
13 KiB
Svelte
<script lang="ts">
|
|
import { onMount, untrack } from "svelte";
|
|
import { auth } from "./auth.svelte";
|
|
import { HOST_KEY, DB_NAME_KEY } from "../env";
|
|
import { TokenStore, getStdbHost, getStdbDbName } from "../config";
|
|
import SpacetimeProvider from "../SpacetimeProvider.svelte";
|
|
import ComboBoxInput from "../chat/components/ComboBoxInput.svelte";
|
|
|
|
let { children, showServerSettings, onToggleServerSettings: _onToggleServerSettings } = $props<{
|
|
children: any,
|
|
showServerSettings: boolean,
|
|
onToggleServerSettings: (_val: boolean) => void
|
|
}>();
|
|
|
|
let authError = $state<string | null>(null);
|
|
let isGuest = $state(false);
|
|
|
|
let stdbHost = $state("");
|
|
let stdbDbName = $state("");
|
|
let storedConnections = $state<string[]>([]);
|
|
let combinedConnection = $state("");
|
|
|
|
let userWantsToConnect = $state(false);
|
|
|
|
onMount(() => {
|
|
stdbHost = getStdbHost();
|
|
stdbDbName = getStdbDbName();
|
|
|
|
storedConnections = TokenStore.listStoredConnections();
|
|
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) || 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 (Only when on login screen)
|
|
$effect(() => {
|
|
if (!userWantsToConnect && combinedConnection.includes(":")) {
|
|
const lastColon = combinedConnection.lastIndexOf(":");
|
|
const host = combinedConnection.substring(0, lastColon);
|
|
const db = combinedConnection.substring(lastColon + 1);
|
|
if (host && db && (host !== stdbHost || db !== stdbDbName)) {
|
|
untrack(() => {
|
|
stdbHost = host;
|
|
stdbDbName = db;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update combined connection if individual fields change (e.g. on mount)
|
|
$effect(() => {
|
|
const hostPart = stdbHost.replace(/^(https?|wss?):\/\//, "");
|
|
const expected = `${hostPart}:${stdbDbName}`;
|
|
if (combinedConnection !== expected) {
|
|
combinedConnection = expected;
|
|
}
|
|
});
|
|
|
|
let hasStoredToken = $state(false);
|
|
|
|
$effect(() => {
|
|
// Check for token when connection params change
|
|
if (stdbHost && stdbDbName) {
|
|
hasStoredToken = !!TokenStore.get(stdbHost, stdbDbName);
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (stdbHost && localStorage.getItem(HOST_KEY) !== stdbHost) {
|
|
localStorage.setItem(HOST_KEY, stdbHost);
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (stdbDbName && localStorage.getItem(DB_NAME_KEY) !== stdbDbName) {
|
|
localStorage.setItem(DB_NAME_KEY, stdbDbName);
|
|
}
|
|
});
|
|
|
|
const isBypassEnabled =
|
|
import.meta.env.VITE_BYPASS_AUTH === "true" ||
|
|
new URLSearchParams(window.location.search).has("bypass_auth");
|
|
|
|
const isAuthenticated = $derived(auth.isAuthenticated || hasStoredToken || isGuest || isBypassEnabled);
|
|
const shouldShowContent = $derived(isAuthenticated && !showServerSettings && userWantsToConnect);
|
|
|
|
$effect(() => {
|
|
console.log("AuthGate: auth.isLoading:", auth.isLoading);
|
|
console.log("AuthGate: auth.isAuthenticated:", auth.isAuthenticated);
|
|
console.log("AuthGate: hasStoredToken:", hasStoredToken);
|
|
console.log("AuthGate: isGuest:", isGuest);
|
|
console.log("AuthGate: isBypassEnabled:", isBypassEnabled);
|
|
});
|
|
</script>
|
|
|
|
{#if auth.isLoading && !isBypassEnabled}
|
|
<div class="login-screen">
|
|
<h1>Loading Authentication...</h1>
|
|
</div>
|
|
{:else if shouldShowContent}
|
|
<SpacetimeProvider
|
|
onCancel={() => {
|
|
userWantsToConnect = false;
|
|
isGuest = false;
|
|
}}
|
|
>
|
|
{@render children()}
|
|
</SpacetimeProvider>
|
|
{:else}
|
|
<div class="login-screen">
|
|
<div class="login-card">
|
|
<div class="hero-header">
|
|
<div class="logo-container">
|
|
<svg viewBox="0 0 100 100" class="logo-svg">
|
|
<defs>
|
|
<linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stop-color="var(--brand)" />
|
|
<stop offset="100%" stop-color="var(--brand-hover)" />
|
|
</linearGradient>
|
|
</defs>
|
|
<g class="zep-blimp">
|
|
<!-- Fins / Comic Action Lines -->
|
|
<path d="M15 40 L5 35 M15 50 L2 50 M15 60 L5 65" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.4" />
|
|
|
|
<!-- The Chat Bubble Zeppelin Sandwich -->
|
|
<!-- Top Bun -->
|
|
<path d="M25 50 C25 30 45 25 60 25 C85 25 95 35 95 50" class="blimp-top" />
|
|
<!-- Energy Filling -->
|
|
<path d="M22 50 C22 50 50 50 98 50" class="blimp-filling" />
|
|
<!-- Bottom Bun + Chat Pointer (Gondola) -->
|
|
<path d="M95 50 C95 65 85 75 60 75 C55 75 50 75 45 85 L38 75 C30 75 25 65 25 50" class="blimp-bottom" />
|
|
|
|
<!-- Internal "Z" detail -->
|
|
<path d="M55 40 L70 40 L55 60 L70 60" stroke="currentColor" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.3" />
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
<h1>Zep</h1>
|
|
<p class="tagline">Decentralized. Private. Fast.</p>
|
|
</div>
|
|
|
|
{#if authError}
|
|
<div class="login-error">{authError}</div>
|
|
{/if}
|
|
|
|
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%">
|
|
<button
|
|
onclick={() => {
|
|
userWantsToConnect = true;
|
|
auth.signinRedirect();
|
|
}}
|
|
disabled={auth.isLoading}
|
|
class="btn-primary"
|
|
style="width: 100%"
|
|
>
|
|
{auth.isLoading ? "Loading..." : "Login with OIDC"}
|
|
</button>
|
|
|
|
<button
|
|
onclick={() => {
|
|
console.log("AuthGate: Reconnect/Guest clicked. hasStoredToken:", hasStoredToken);
|
|
userWantsToConnect = true;
|
|
if (!hasStoredToken) {
|
|
isGuest = true;
|
|
}
|
|
_onToggleServerSettings(false);
|
|
}}
|
|
class="btn-secondary"
|
|
style="width: 100%"
|
|
>
|
|
{hasStoredToken ? "Reconnect as last user" : "Connect as guest"}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="server-settings-section" style="margin-top: 24px; width: 100%; border-top: 1px solid var(--background-modifier-accent);">
|
|
<div style="padding-top: 16px; 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-connection" style="font-size: 0.7rem; font-weight: bold; color: var(--text-muted); text-transform: uppercase; text-align: left; width: 100%;">
|
|
Instance hostname:database
|
|
</label>
|
|
</div>
|
|
<ComboBoxInput
|
|
id="stdb-connection"
|
|
bind:value={combinedConnection}
|
|
placeholder="connect.zep.chat:zep"
|
|
options={storedConnections}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="specs-section">
|
|
<h3>Technical Specifications</h3>
|
|
<div class="specs-grid">
|
|
<div class="spec-tag" title="Reactive state management with Svelte 5 Runes">
|
|
<i class="fab fa-js-square"></i>
|
|
<span>Svelte 5</span>
|
|
</div>
|
|
<div class="spec-tag" title="High-performance relational database backend">
|
|
<i class="fas fa-database"></i>
|
|
<span>SpacetimeDB</span>
|
|
</div>
|
|
<div class="spec-tag" title="Secure, efficient logic compiled to WebAssembly">
|
|
<i class="fab fa-rust"></i>
|
|
<span>Rust / WASM</span>
|
|
</div>
|
|
<div class="spec-tag" title="Real-time peer-to-peer voice and screen sharing">
|
|
<i class="fas fa-network-wired"></i>
|
|
<span>WebRTC Mesh</span>
|
|
</div>
|
|
<div class="spec-tag" title="End-to-end encryption via OpenPGP">
|
|
<i class="fas fa-lock"></i>
|
|
<span>GPG E2EE</span>
|
|
</div>
|
|
<div class="spec-tag" title="Cross-platform desktop app foundation">
|
|
<i class="fas fa-desktop"></i>
|
|
<span>Tauri 2.0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.login-screen {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
background: radial-gradient(
|
|
circle at center,
|
|
var(--background-secondary) 0%,
|
|
var(--background-tertiary) 100%
|
|
);
|
|
background-color: var(--background-tertiary);
|
|
}
|
|
|
|
.login-card {
|
|
background-color: var(--background-primary);
|
|
padding: 32px;
|
|
border-radius: 8px;
|
|
width: 480px;
|
|
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.2);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.login-card h1 {
|
|
color: var(--header-primary);
|
|
margin-bottom: 8px;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.login-card p {
|
|
color: var(--text-normal);
|
|
margin-bottom: 24px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.login-error {
|
|
color: var(--status-danger);
|
|
background-color: rgba(250, 119, 122, 0.1);
|
|
border: 1px solid rgba(250, 119, 122, 0.2);
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
margin-bottom: 16px;
|
|
width: 100%;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.specs-section {
|
|
margin-top: 32px;
|
|
width: 100%;
|
|
border-top: 1px solid var(--background-modifier-accent);
|
|
padding-top: 24px;
|
|
}
|
|
|
|
.specs-section h3 {
|
|
font-size: 0.75rem;
|
|
font-weight: 800;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.specs-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 12px;
|
|
}
|
|
|
|
.spec-tag {
|
|
background-color: var(--background-secondary);
|
|
border: 1px solid var(--background-modifier-accent);
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: var(--text-normal);
|
|
font-size: 0.95rem; /* Bigger text */
|
|
font-weight: 600;
|
|
transition: all 0.2s ease;
|
|
cursor: default;
|
|
}
|
|
|
|
.spec-tag i {
|
|
font-size: 1.1rem;
|
|
color: var(--brand);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.spec-tag:hover {
|
|
background-color: var(--background-modifier-hover);
|
|
border-color: var(--brand);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
color: var(--interactive-hover);
|
|
}
|
|
|
|
.spec-tag:hover i {
|
|
opacity: 1;
|
|
}
|
|
|
|
.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: var(--background-accent);
|
|
transition: .4s;
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 16px;
|
|
width: 16px;
|
|
left: 2px;
|
|
bottom: 2px;
|
|
background-color: var(--header-primary);
|
|
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);
|
|
}
|
|
|
|
.hero-header {
|
|
margin-bottom: 32px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.logo-container {
|
|
width: 100px;
|
|
height: 100px;
|
|
margin-bottom: 16px;
|
|
filter: drop-shadow(0 0 10px var(--brand));
|
|
}
|
|
|
|
.logo-svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
color: var(--brand);
|
|
}
|
|
|
|
.zep-blimp {
|
|
animation: float 4s infinite ease-in-out;
|
|
transform-origin: center;
|
|
}
|
|
|
|
.blimp-top, .blimp-bottom {
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 6;
|
|
stroke-linecap: round;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.blimp-filling {
|
|
fill: none;
|
|
stroke: url(#logoGrad);
|
|
stroke-width: 8;
|
|
stroke-linecap: round;
|
|
filter: drop-shadow(0 0 8px var(--brand));
|
|
animation: energy-pulse 2s infinite ease-in-out;
|
|
}
|
|
|
|
.tagline {
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
margin-top: 4px;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
font-weight: 700;
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0) rotate(-2deg); }
|
|
50% { transform: translateY(-10px) rotate(2deg); }
|
|
}
|
|
|
|
@keyframes energy-pulse {
|
|
0%, 100% { opacity: 0.6; stroke-width: 8; }
|
|
50% { opacity: 1; stroke-width: 10; filter: drop-shadow(0 0 12px var(--brand)); }
|
|
}
|
|
</style>
|