fix: early name validation, fighter glow, reduced visual noise

- Add /api/auth/check-name endpoint for early name availability check
- JoinBoutPage checks name availability at naming step (not after webhook)
- FightCardPage: replace poster containers with circular glow behind sprites
- Reduce synthwave grid and CRT overlay opacity for cleaner backgrounds
- BotProfilePage: webhook management, owner-only fields
- useNostr: normalize bot data, guard auto-restore, clearAllState helper
- bots.ts: expose owner-only webhook info on stats endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 11:03:16 +00:00
co-authored by Claude Opus 4.6
parent 2d8cdcc60a
commit 9a11c48487
7 changed files with 405 additions and 120 deletions
+108 -32
View File
@@ -55,40 +55,73 @@ function store(key: string, value: unknown) {
else localStorage.setItem(key, JSON.stringify(value))
}
/** Normalize server bot data to fill missing fields */
function normalizeBotData(data: Record<string, unknown>): BotData {
return {
satsWon: 0,
satsWagered: 0,
hasWallet: false,
...data,
} as BotData
}
/** Clear all auth-related state */
function clearAllState() {
pubkey.value = null
bot.value = null
profilePicUrl.value = null
store('bf_pubkey', null)
store('bf_bot', null)
store('bf_pic', null)
}
const pubkey = ref<string | null>(loadStored('bf_pubkey'))
const bot = ref<BotData | null>(loadStored('bf_bot'))
const profilePicUrl = ref<string | null>(loadStored('bf_pic'))
const isLoading = ref(false)
// Guard: only auto-restore once across all component mounts
let autoRestoreRan = false
// Flag: skip relay pic fetch for freshly generated keys (no profile exists)
let freshlyGenerated = false
export function useNostr() {
const isLoggedIn = computed(() => !!pubkey.value && !!bot.value)
const hasExtension = computed(() => !!window.nostr)
// Restore session on first load — re-verify with server
if (pubkey.value && !bot.value) {
// Restore session on first load — re-verify with server (once only)
if (!autoRestoreRan && pubkey.value && !bot.value) {
autoRestoreRan = true
fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pubkey.value }),
}).then(r => r.json()).then(data => {
if (data.exists) {
bot.value = data.bot
store('bf_bot', data.bot)
bot.value = normalizeBotData(data.bot)
store('bf_bot', bot.value)
}
}).catch(() => {})
}
async function login(): Promise<{ pubkey: string; bot: BotData | null }> {
// Try NIP-07 extension first, fall back to stored nsec
let pk: string
if (window.nostr) {
pk = await window.nostr.getPublicKey()
try {
pk = await window.nostr.getPublicKey()
} catch {
throw new Error('Nostr extension denied access. Approve the request and try again.')
}
} else {
const storedNsec = localStorage.getItem('bf_nsec')
if (storedNsec) {
const secretKey = hexToBytes(storedNsec)
pk = getPublicKey(secretKey)
try {
const secretKey = hexToBytes(storedNsec)
pk = getPublicKey(secretKey)
} catch {
throw new Error('Stored key is corrupt. Generate a new identity.')
}
} else {
throw new Error('No Nostr extension found and no saved key. Use "Generate Login" to create one.')
}
@@ -99,10 +132,18 @@ export function useNostr() {
pubkey.value = pk
store('bf_pubkey', pk)
// Fetch Nostr profile pic from relay
const pic = await fetchNostrProfilePic(pk)
profilePicUrl.value = pic
store('bf_pic', pic)
// Fetch Nostr profile pic from relay (skip for freshly generated keys — no profile exists)
if (freshlyGenerated) {
freshlyGenerated = false
profilePicUrl.value = null
store('bf_pic', null)
} else {
// Non-blocking: start fetch but don't block login on it
fetchNostrProfilePic(pk).then(pic => {
profilePicUrl.value = pic
store('bf_pic', pic)
}).catch(() => {})
}
// Check if this pubkey has a bot
const res = await fetch('/api/auth/login', {
@@ -114,9 +155,9 @@ export function useNostr() {
if (res.ok) {
const data = await res.json()
if (data.exists) {
bot.value = data.bot
store('bf_bot', data.bot)
return { pubkey: pk, bot: data.bot }
bot.value = normalizeBotData(data.bot)
store('bf_bot', bot.value)
return { pubkey: pk, bot: bot.value }
}
}
@@ -137,13 +178,13 @@ export function useNostr() {
const nsecHex = bytesToHex(secretKey)
const nsecBech32 = nsecEncode(secretKey)
// Clear stale state from previous identity
bot.value = null
profilePicUrl.value = null
store('bf_bot', null)
store('bf_pic', null)
// Clear all stale state from previous identity
clearAllState()
// Store locally
// Mark as freshly generated so login() skips relay pic fetch
freshlyGenerated = true
// Store new key
localStorage.setItem('bf_nsec', nsecHex)
pubkey.value = pk
store('bf_pubkey', pk)
@@ -153,15 +194,32 @@ export function useNostr() {
/** Login with an existing nsec (hex) */
async function loginWithNsec(nsecHex: string): Promise<{ pubkey: string; bot: BotData | null }> {
const secretKey = hexToBytes(nsecHex)
let secretKey: Uint8Array
try {
secretKey = hexToBytes(nsecHex)
} catch {
throw new Error('Invalid secret key format.')
}
const pk = getPublicKey(secretKey)
// Clear stale state before switching identity
bot.value = null
profilePicUrl.value = null
store('bf_bot', null)
store('bf_pic', null)
localStorage.setItem('bf_nsec', nsecHex)
pubkey.value = pk
store('bf_pubkey', pk)
isLoading.value = true
try {
// Fetch Nostr profile pic (non-blocking)
fetchNostrProfilePic(pk).then(pic => {
profilePicUrl.value = pic
store('bf_pic', pic)
}).catch(() => {})
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -171,9 +229,9 @@ export function useNostr() {
if (res.ok) {
const data = await res.json()
if (data.exists) {
bot.value = data.bot
store('bf_bot', data.bot)
return { pubkey: pk, bot: data.bot }
bot.value = normalizeBotData(data.bot)
store('bf_bot', bot.value)
return { pubkey: pk, bot: bot.value }
}
}
@@ -199,7 +257,11 @@ export function useNostr() {
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Registration failed')
if (!res.ok) {
// Surface detailed error info from webhook verification failures
const msg = data.details ? `${data.error} ${data.details}` : (data.error || 'Registration failed')
throw new Error(msg)
}
bot.value = {
id: data.id,
@@ -247,6 +309,24 @@ export function useNostr() {
}
}
async function updateWebhook(newUrl: string): Promise<{ latencyMs: number }> {
if (!pubkey.value) throw new Error('Not logged in')
const res = await fetch('/api/auth/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pubkey.value, webhookUrl: newUrl }),
})
const data = await res.json()
if (!res.ok) {
const msg = data.details ? `${data.error} ${data.details}` : (data.error || 'Update failed')
throw new Error(msg)
}
return { latencyMs: data.latencyMs || 0 }
}
async function registerHuman(name: string, avatarSeed?: string): Promise<BotData> {
if (!pubkey.value) throw new Error('Not logged in')
@@ -288,12 +368,7 @@ export function useNostr() {
}
function logout() {
pubkey.value = null
bot.value = null
profilePicUrl.value = null
store('bf_pubkey', null)
store('bf_bot', null)
store('bf_pic', null)
clearAllState()
// Don't clear bf_nsec on logout — user may want to log back in
}
@@ -319,6 +394,7 @@ export function useNostr() {
registerBot,
registerHuman,
updateCustomization,
updateWebhook,
getStoredNsec,
logout,
}