feat: add client administrator authentication gate
This commit is contained in:
@@ -3,6 +3,15 @@
|
|||||||
let connectionState = 'OFFLINE';
|
let connectionState = 'OFFLINE';
|
||||||
let serverVersion = '—';
|
let serverVersion = '—';
|
||||||
let activeView = 'Live dashboard';
|
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() {
|
async function checkServer() {
|
||||||
connectionState = 'CONNECTING';
|
connectionState = 'CONNECTING';
|
||||||
@@ -16,6 +25,64 @@
|
|||||||
connectionState = 'SERVER UNAVAILABLE';
|
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;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><title>Trixie</title></svelte:head>
|
<svelte:head><title>Trixie</title></svelte:head>
|
||||||
@@ -41,17 +108,49 @@
|
|||||||
<input id="server-address" class="control" bind:value={serverAddress} spellcheck="false" />
|
<input id="server-address" class="control" bind:value={serverAddress} spellcheck="false" />
|
||||||
<button class="primary control-button" onclick={checkServer}>TEST CONNECTION</button>
|
<button class="primary control-button" onclick={checkServer}>TEST CONNECTION</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="server-card auth-card">
|
||||||
|
<div class="micro">ADMINISTRATOR ACCESS</div>
|
||||||
|
{#if authenticated}
|
||||||
|
<div class="server-status"><span class="status-dot good"></span>AUTHENTICATED</div>
|
||||||
|
<button class="primary control-button" onclick={logout}>LOG OUT</button>
|
||||||
|
{:else}
|
||||||
|
<label for="username" class="micro">USERNAME</label>
|
||||||
|
<input id="username" class="control" bind:value={username} autocomplete="username" />
|
||||||
|
<label for="password" class="micro">PASSWORD</label>
|
||||||
|
<input id="password" class="control" type="password" bind:value={password} autocomplete="current-password" />
|
||||||
|
<button class="primary control-button" onclick={login} disabled={authBusy}>LOG IN</button>
|
||||||
|
{#if authError}<div class="auth-error">{authError}</div>{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="side-section"><div class="micro">CHANNEL</div><div class="empty-row">No Twitch account connected</div></div>
|
<div class="side-section"><div class="micro">CHANNEL</div><div class="empty-row">No Twitch account connected</div></div>
|
||||||
<div class="side-section side-footer"><div class="micro">SERVER VERSION</div><div class="mono">{serverVersion}</div></div>
|
<div class="side-section side-footer"><div class="micro">SERVER VERSION</div><div class="mono">{serverVersion}</div></div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="content-area">
|
<section class="content-area">
|
||||||
|
{#if !authenticated}
|
||||||
|
<section class="auth-gate panel">
|
||||||
|
<div class="panel-header"><span>AUTHENTICATION REQUIRED</span></div>
|
||||||
|
<div class="auth-gate-body"><div class="empty-glyph">⌁</div><strong>Sign in to Trixie</strong><span>The server connection is ready, but the dashboard requires administrator authentication.</span></div>
|
||||||
|
</section>
|
||||||
|
{:else if mustReplaceCredentials}
|
||||||
|
<section class="auth-gate panel">
|
||||||
|
<div class="panel-header"><span>FIRST-RUN SETUP</span></div>
|
||||||
|
<div class="auth-gate-body credential-form">
|
||||||
|
<strong>Replace the generated administrator credentials</strong>
|
||||||
|
<label for="new-username" class="micro">NEW USERNAME</label><input id="new-username" class="control" bind:value={newUsername} autocomplete="username" />
|
||||||
|
<label for="new-password" class="micro">NEW PASSWORD</label><input id="new-password" class="control" type="password" bind:value={newPassword} autocomplete="new-password" />
|
||||||
|
<button class="primary" onclick={replaceCredentials} disabled={authBusy}>SAVE AND CONTINUE</button>
|
||||||
|
{#if authError}<div class="auth-error">{authError}</div>{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{:else}
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<section class="panel preview-panel"><div class="panel-header"><span>STREAM PREVIEW</span><span class="panel-menu">⋮</span></div><div class="preview-stage"><div class="offline-mark">STREAM OFFLINE</div><div class="offline-subtitle">CONNECT TWITCH TO BEGIN</div></div><div class="panel-footer"><span class="micro">VIDEO PLAYER</span><span class="badge">OFFLINE</span></div></section>
|
<section class="panel preview-panel"><div class="panel-header"><span>STREAM PREVIEW</span><span class="panel-menu">⋮</span></div><div class="preview-stage"><div class="offline-mark">STREAM OFFLINE</div><div class="offline-subtitle">CONNECT TWITCH TO BEGIN</div></div><div class="panel-footer"><span class="micro">VIDEO PLAYER</span><span class="badge">OFFLINE</span></div></section>
|
||||||
<section class="panel activity-panel"><div class="panel-header"><span>ACTIVITY FEED</span><span class="panel-menu">⋮</span></div><div class="panel-tabs"><button class="tab active">ALL ACTIVITY</button><button class="tab">MODERATION</button></div><div class="empty-state"><div class="empty-glyph">◌</div><strong>Nothing here yet</strong><span>Follows, subscriptions, raids, and server events will appear here.</span></div></section>
|
<section class="panel activity-panel"><div class="panel-header"><span>ACTIVITY FEED</span><span class="panel-menu">⋮</span></div><div class="panel-tabs"><button class="tab active">ALL ACTIVITY</button><button class="tab">MODERATION</button></div><div class="empty-state"><div class="empty-glyph">◌</div><strong>Nothing here yet</strong><span>Follows, subscriptions, raids, and server events will appear here.</span></div></section>
|
||||||
<section class="panel chat-panel"><div class="panel-header"><span>MY CHAT</span><span class="panel-menu">⋮</span></div><div class="chat-empty"><div class="empty-glyph">⌁</div><strong>Chat is waiting</strong><span>Connect a streamer or bot account to begin.</span></div><div class="chat-composer"><input class="control" placeholder="Send a message" disabled /><button class="primary" disabled>SEND</button></div></section>
|
<section class="panel chat-panel"><div class="panel-header"><span>MY CHAT</span><span class="panel-menu">⋮</span></div><div class="chat-empty"><div class="empty-glyph">⌁</div><strong>Chat is waiting</strong><span>Connect a streamer or bot account to begin.</span></div><div class="chat-composer"><input class="control" placeholder="Send a message" disabled /><button class="primary" disabled>SEND</button></div></section>
|
||||||
<section class="panel quick-panel"><div class="panel-header"><span>QUICK ACTIONS</span><span class="panel-menu">⋮</span></div><div class="quick-grid"><button class="quick-action accent">EDIT STREAM INFO</button><button class="quick-action">MODERATION</button><button class="quick-action">BOT COMMANDS</button><button class="quick-action">OVERLAYS</button></div></section>
|
<section class="panel quick-panel"><div class="panel-header"><span>QUICK ACTIONS</span><span class="panel-menu">⋮</span></div><div class="quick-grid"><button class="quick-action accent">EDIT STREAM INFO</button><button class="quick-action">MODERATION</button><button class="quick-action">BOT COMMANDS</button><button class="quick-action">OVERLAYS</button></div></section>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -232,6 +232,42 @@ label {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 8px;
|
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 {
|
.primary {
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
border-color: var(--ui-line-focus);
|
border-color: var(--ui-line-focus);
|
||||||
|
|||||||
Reference in New Issue
Block a user