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:
co-authored by
Claude Opus 4.6
parent
2d8cdcc60a
commit
9a11c48487
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user