diff --git a/src/App.svelte b/src/App.svelte index d2149d1..41a186a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -3,6 +3,15 @@ let connectionState = 'OFFLINE'; let serverVersion = '—'; let activeView = 'Live dashboard'; + let sessionToken = ''; + let authenticated = false; + let mustReplaceCredentials = false; + let username = ''; + let password = ''; + let newUsername = ''; + let newPassword = ''; + let authError = ''; + let authBusy = false; async function checkServer() { connectionState = 'CONNECTING'; @@ -16,6 +25,64 @@ connectionState = 'SERVER UNAVAILABLE'; } } + + async function login() { + authBusy = true; + authError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/auth/login`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + if (!response.ok) throw new Error('Your username or password may be incorrect.'); + const result: { session_token: string; must_replace_credentials: boolean } = await response.json(); + sessionToken = result.session_token; + authenticated = true; + mustReplaceCredentials = result.must_replace_credentials; + connectionState = 'CONNECTED'; + if (!mustReplaceCredentials) password = ''; + if (mustReplaceCredentials) newUsername = username; + } catch (error) { + authError = error instanceof Error ? error.message : 'The server could not process the login.'; + authenticated = false; + } finally { + authBusy = false; + } + } + + async function replaceCredentials() { + authBusy = true; + authError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/auth/credentials`, { + method: 'POST', + headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` }, + body: JSON.stringify({ current_password: password, username: newUsername, password: newPassword }), + }); + if (!response.ok) throw new Error(response.status === 400 ? 'Use a username and a password of at least 10 characters.' : 'Credential replacement failed.'); + sessionToken = ''; + authenticated = false; + mustReplaceCredentials = false; + username = newUsername; + password = ''; + newPassword = ''; + authError = 'Credentials updated. Sign in again.'; + } catch (error) { + authError = error instanceof Error ? error.message : 'Credential replacement failed.'; + } finally { + authBusy = false; + } + } + + async function logout() { + if (sessionToken) { + await fetch(`${serverAddress}/api/v1/auth/logout`, { method: 'POST', headers: { authorization: `Bearer ${sessionToken}` } }).catch(() => undefined); + } + sessionToken = ''; + authenticated = false; + mustReplaceCredentials = false; + } Trixie @@ -41,17 +108,49 @@ +
+
ADMINISTRATOR ACCESS
+ {#if authenticated} +
AUTHENTICATED
+ + {:else} + + + + + + {#if authError}
{authError}
{/if} + {/if} +
CHANNEL
No Twitch account connected
+ {#if !authenticated} +
+
AUTHENTICATION REQUIRED
+
⌁
Sign in to TrixieThe server connection is ready, but the dashboard requires administrator authentication.
+
+ {:else if mustReplaceCredentials} +
+
FIRST-RUN SETUP
+
+ Replace the generated administrator credentials + + + + {#if authError}
{authError}
{/if} +
+
+ {:else}
STREAM PREVIEW⋮
STREAM OFFLINE
CONNECT TWITCH TO BEGIN
ACTIVITY FEED⋮
◌
Nothing here yetFollows, subscriptions, raids, and server events will appear here.
MY CHAT⋮
⌁
Chat is waitingConnect a streamer or bot account to begin.
QUICK ACTIONS⋮
+ {/if}
diff --git a/src/styles.css b/src/styles.css index c5e334c..12385b1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -232,6 +232,42 @@ label { width: 100%; margin-top: 8px; } +.auth-card { + display: block; +} +.auth-card label { + margin-top: 9px; +} +.auth-error { + margin-top: 8px; + color: var(--ed-warn); + font: 10px/1.4 var(--font-data); +} +.auth-gate { + min-height: 300px; +} +.auth-gate-body { + display: grid; + justify-items: center; + align-content: start; + gap: 8px; + max-width: 360px; + margin: 0 auto; + padding: 48px 16px; + color: var(--ed-dim); + text-align: center; +} +.auth-gate-body strong { + color: var(--ed-fg); + font: 600 16px var(--font-display); +} +.credential-form { + justify-items: stretch; + text-align: left; +} +.credential-form strong { + text-align: center; +} .primary { padding: 7px 10px; border-color: var(--ui-line-focus);