From c1df6938b5691477f0334e54b89f8f404bdf42ac Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Sun, 25 Oct 2026 05:12:15 +0100 Subject: [PATCH] add bot secret manager view --- src/App.svelte | 44 +++++++++++++++++++++++++++++++++++++++++++- src/styles.css | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/App.svelte b/src/App.svelte index f5debb7..d681688 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -63,6 +63,11 @@ let oauthError = ''; let botCommands: Array<{ id: string; name: string; description: string; permission_level: string; aliases: string[]; tags: string[]; cooldown_seconds: number; cooldown_scope: string; response_template: string; error_template: string | null; enabled: boolean }> = []; let botCommandError = ''; + let botSecrets: Array<{ id: string; name: string; description: string; used_by: string[] }> = []; + let secretName = ''; + let secretDescription = ''; + let secretValue = ''; + let secretError = ''; let oauthScopes: Record<'streamer' | 'bot', { connected: boolean; missing_scopes: string[]; reauthorization_required: boolean }> = { streamer: { connected: false, missing_scopes: [], reauthorization_required: false }, bot: { connected: false, missing_scopes: [], reauthorization_required: false }, @@ -116,6 +121,7 @@ await loadOAuthStatus(); await loadCollaboration(); await loadBotCommands(); + await loadBotSecrets(); await loadStreamInfo(); connectRealtime(); if (!mustReplaceCredentials) password = ''; @@ -529,6 +535,42 @@ } } + async function loadBotSecrets() { + try { + const response = await fetch(`${serverAddress}/api/v1/bot/secrets`, { headers: { authorization: `Bearer ${sessionToken}` } }); + if (!response.ok) throw new Error('Secrets could not be loaded.'); + botSecrets = await response.json(); + secretError = ''; + } catch (error) { + secretError = error instanceof Error ? error.message : 'Secrets could not be loaded.'; + } + } + + async function createSecret() { + secretError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/bot/secrets`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` }, body: JSON.stringify({ name: secretName, description: secretDescription, value: secretValue }) }); + if (!response.ok) throw new Error('The secret could not be saved.'); + secretName = ''; + secretDescription = ''; + secretValue = ''; + await loadBotSecrets(); + } catch (error) { + secretError = error instanceof Error ? error.message : 'The secret could not be saved.'; + } + } + + async function deleteSecret(id: string) { + secretError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/bot/secrets/${encodeURIComponent(id)}`, { method: 'DELETE', headers: { authorization: `Bearer ${sessionToken}` } }); + if (!response.ok) throw new Error('The secret could not be deleted.'); + await loadBotSecrets(); + } catch (error) { + secretError = error instanceof Error ? error.message : 'The secret could not be deleted.'; + } + } + async function revokeOAuth(identity: 'streamer' | 'bot') { oauthBusy = true; oauthError = ''; @@ -642,7 +684,7 @@ {:else if activeView === 'Bot'} -
BOT COMMANDS
PUBLIC COMMAND CATALOG
{#if botCommandError}
{botCommandError}
{:else if botCommands.length}
{#each botCommands as command}
!{command.name}{command.description}{#if command.tags.length}
{command.tags.join(' · ')}
{/if}
{command.permission_level.toUpperCase()}{#if command.cooldown_seconds}{command.cooldown_seconds}s / {command.cooldown_scope}{/if}
{/each}
{:else}
No enabled commandsCreate commands on the server to make them available here.
{/if}
+
BOT COMMANDS
PUBLIC COMMAND CATALOG
{#if botCommandError}
{botCommandError}
{:else if botCommands.length}
{#each botCommands as command}
!{command.name}{command.description}{#if command.tags.length}
{command.tags.join(' · ')}
{/if}
{command.permission_level.toUpperCase()}{#if command.cooldown_seconds}{command.cooldown_seconds}s / {command.cooldown_scope}{/if}
{/each}
{:else}
No enabled commandsCreate commands on the server to make them available here.
{/if}
SECRETS MANAGEMENT
Values are encrypted and cannot be read after saving.
{#if botSecrets.length}
{#each botSecrets as secret}
{secret.name}{secret.description || 'No description'}{#if secret.used_by.length}
USED BY: {secret.used_by.join(', ')}
{/if}
{/each}
{:else}
There are no secrets yet.
{/if}{#if secretError}
{secretError}
{/if}
{:else}
STREAM PREVIEW⋮
{#if playerSrc}{:else}
STREAM OFFLINE
CONNECT TWITCH TO BEGIN
{/if}
diff --git a/src/styles.css b/src/styles.css index 16a296d..fe8f99c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -545,6 +545,42 @@ label { font: 10px var(--font-data); white-space: nowrap; } +.bot-secret-section { + margin-top: 24px; + padding-top: 18px; + border-top: 1px solid var(--ui-divider); +} +.bot-secret-note { + margin-top: 5px; + color: var(--ed-dim); + font-size: 11px; +} +.secret-form { + display: grid; + grid-template-columns: 1fr 1fr 1fr auto; + gap: 7px; + margin-top: 12px; +} +.bot-secret-list { + margin-top: 12px; + border-top: 1px solid var(--ui-divider); +} +.bot-secret-row { + display: flex; + justify-content: space-between; + gap: 16px; + padding: 10px 4px; + border-bottom: 1px solid var(--ui-divider); +} +.bot-secret-row strong, +.bot-secret-row span { + display: block; +} +.bot-secret-row span { + margin-top: 3px; + color: var(--ed-dim); + font-size: 11px; +} .version-label { display: block; margin-top: 13px;