add bot secret manager view
This commit is contained in:
+43
-1
@@ -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 @@
|
||||
</div>
|
||||
</section>
|
||||
{:else if activeView === 'Bot'}
|
||||
<section class="bot-page panel"><div class="panel-header"><span>BOT COMMANDS</span><button class="secondary" onclick={loadBotCommands}>REFRESH</button></div><div class="bot-page-body"><div class="micro">PUBLIC COMMAND CATALOG</div>{#if botCommandError}<div class="auth-error">{botCommandError}</div>{:else if botCommands.length}<div class="bot-command-list">{#each botCommands as command}<article class="bot-command-row"><div><strong>!{command.name}</strong><span class="bot-command-description">{command.description}</span>{#if command.tags.length}<div class="micro">{command.tags.join(' · ')}</div>{/if}</div><div class="bot-command-meta"><span>{command.permission_level.toUpperCase()}</span>{#if command.cooldown_seconds}<span>{command.cooldown_seconds}s / {command.cooldown_scope}</span>{/if}</div></article>{/each}</div>{:else}<div class="empty-state"><strong>No enabled commands</strong><span>Create commands on the server to make them available here.</span></div>{/if}</div></section>
|
||||
<section class="bot-page panel"><div class="panel-header"><span>BOT COMMANDS</span><button class="secondary" onclick={loadBotCommands}>REFRESH</button></div><div class="bot-page-body"><div class="micro">PUBLIC COMMAND CATALOG</div>{#if botCommandError}<div class="auth-error">{botCommandError}</div>{:else if botCommands.length}<div class="bot-command-list">{#each botCommands as command}<article class="bot-command-row"><div><strong>!{command.name}</strong><span class="bot-command-description">{command.description}</span>{#if command.tags.length}<div class="micro">{command.tags.join(' · ')}</div>{/if}</div><div class="bot-command-meta"><span>{command.permission_level.toUpperCase()}</span>{#if command.cooldown_seconds}<span>{command.cooldown_seconds}s / {command.cooldown_scope}</span>{/if}</div></article>{/each}</div>{:else}<div class="empty-state"><strong>No enabled commands</strong><span>Create commands on the server to make them available here.</span></div>{/if}<div class="bot-secret-section"><div class="micro">SECRETS MANAGEMENT</div><div class="bot-secret-note">Values are encrypted and cannot be read after saving.</div><div class="secret-form"><input class="control" bind:value={secretName} maxlength="100" placeholder="Name" /><input class="control" bind:value={secretDescription} maxlength="500" placeholder="Description" /><input class="control" type="password" bind:value={secretValue} maxlength="65536" placeholder="Value" /><button class="primary" onclick={createSecret} disabled={!secretName || !secretValue}>ADD SECRET</button></div>{#if botSecrets.length}<div class="bot-secret-list">{#each botSecrets as secret}<div class="bot-secret-row"><div><strong>{secret.name}</strong><span>{secret.description || 'No description'}</span>{#if secret.used_by.length}<div class="micro">USED BY: {secret.used_by.join(', ')}</div>{/if}</div><button class="secondary-action" onclick={() => deleteSecret(secret.id)}>DELETE</button></div>{/each}</div>{:else}<div class="empty-row">There are no secrets yet.</div>{/if}{#if secretError}<div class="auth-error">{secretError}</div>{/if}</div></div></section>
|
||||
{:else}
|
||||
<div class="dashboard-grid">
|
||||
<section class="panel preview-panel"><div class="panel-header"><span>STREAM PREVIEW</span><span class="panel-menu">⋮</span></div>{#if playerSrc}<iframe class="twitch-player" title={`Twitch player for ${streamerName}`} src={playerSrc} allowfullscreen></iframe>{:else}<div class="preview-stage"><div class="offline-mark">STREAM OFFLINE</div><div class="offline-subtitle">CONNECT TWITCH TO BEGIN</div></div>{/if}<div class="panel-footer"><span class="micro">VIDEO PLAYER</span><span class="badge">{playerSrc ? streamerName : 'OFFLINE'}</span></div></section>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user