From cef9f4188fd1c8f363df3cc798aa62df23cb142b Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Mar 2026 13:35:48 +0000 Subject: [PATCH] feat: fix setup guide download with embedded credentials + profile page regenerate secret - downloadSetupGuide() now triggers a real file download with bot_id/secret injected - Add POST /api/auth/regenerate-secret endpoint (JWT auth, 3/hour rate limit) - Add "Download Setup Guide" section to BotProfilePage with secret regeneration flow - Old secret immediately invalidated on regeneration Co-Authored-By: Claude Opus 4.6 --- frontend/src/composables/useNostr.ts | 17 ++++ frontend/src/pages/BotProfilePage.vue | 116 +++++++++++++++++++++++++- frontend/src/pages/JoinBoutPage.vue | 20 ++++- server/src/routes/auth.ts | 38 +++++++++ 4 files changed, 188 insertions(+), 3 deletions(-) diff --git a/frontend/src/composables/useNostr.ts b/frontend/src/composables/useNostr.ts index 668aae7..2b29c70 100644 --- a/frontend/src/composables/useNostr.ts +++ b/frontend/src/composables/useNostr.ts @@ -388,6 +388,22 @@ export function useNostr() { return { latencyMs: data.latencyMs || 0 } } + async function regenerateSecret(): Promise<{ botId: string; secret: string }> { + if (!pubkey.value) throw new Error('Not logged in') + + const res = await authFetch('/api/auth/regenerate-secret', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }) + + const data = await res.json() + if (!res.ok) { + throw new Error(data.error || 'Failed to regenerate secret') + } + + return { botId: data.botId, secret: data.secret } + } + async function registerHuman(name: string, avatarSeed?: string): Promise { if (!pubkey.value) throw new Error('Not logged in') @@ -547,6 +563,7 @@ export function useNostr() { registerHuman, updateCustomization, updateWebhook, + regenerateSecret, getStoredNsec, persistKey, logout, diff --git a/frontend/src/pages/BotProfilePage.vue b/frontend/src/pages/BotProfilePage.vue index f8cd149..f6d2a13 100644 --- a/frontend/src/pages/BotProfilePage.vue +++ b/frontend/src/pages/BotProfilePage.vue @@ -11,7 +11,7 @@ import { ensureAudioContext } from '../game/audio' const route = useRoute() const router = useRouter() -const { bot: nostrBot, pubkey, isLoggedIn, logout, updateCustomization, updateWebhook, fetchNostrProfile } = useNostr() +const { bot: nostrBot, pubkey, isLoggedIn, logout, updateCustomization, updateWebhook, regenerateSecret, fetchNostrProfile } = useNostr() const botName = route.params.name as string interface BotCustomization { @@ -106,6 +106,13 @@ const webhookTestResult = ref<{ reachable: boolean; validResponse: boolean; late const webhookError = ref('') const webhookSuccess = ref('') +// Setup guide download +const showSetupGuide = ref(false) +const isRegenerating = ref(false) +const regeneratedSecret = ref('') +const regeneratedBotId = ref('') +const regenError = ref('') + const ARCHETYPES = [ 'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat', 'cactus', 'pizza', 'mushroom', 'shark', 'penguin', 'octopus', 'skeleton', @@ -268,6 +275,43 @@ async function saveWebhook() { isUpdatingWebhook.value = false } +async function handleRegenerateSecret() { + if (isRegenerating.value) return + isRegenerating.value = true + regenError.value = '' + regeneratedSecret.value = '' + try { + const result = await regenerateSecret() + regeneratedBotId.value = result.botId + regeneratedSecret.value = result.secret + } catch (err) { + regenError.value = err instanceof Error ? err.message : 'Failed to regenerate secret' + } + isRegenerating.value = false +} + +async function downloadGuide(mode: 'webhook' | 'polling') { + const path = mode === 'polling' ? '/docs/BOTFIGHTS-POLLING.md' : '/docs/BOTFIGHTS-WEBHOOK.md' + const fileName = mode === 'polling' ? 'BOTFIGHTS-POLLING.md' : 'BOTFIGHTS-WEBHOOK.md' + try { + const res = await fetch(path) + let content = await res.text() + content = content.replace(/YOUR_BOT_ID/g, regeneratedBotId.value) + content = content.replace(/YOUR_BOT_SECRET/g, regeneratedSecret.value) + const blob = new Blob([content], { type: 'text/markdown' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch { + window.open(path, '_blank') + } +} + onMounted(async () => { try { const pk = pubkey.value || '' @@ -779,6 +823,76 @@ const tierClass = (t: number) => `tier-${t}` + +
+ + +
+

+ Download the setup guide with your credentials embedded. + This requires regenerating your bot secret (your old secret will stop working). +

+ + + + + + + +

{{ regenError }}

+
+
+