fe5cbaec2b
No email required — players pick male/female/mystery, get assigned a humorous period-appropriate operator name (e.g. "Dusty Clicksworth", "Ada Dottsworth", "The Phantom Sender"), can reroll, then hit "Begin Transmitting!" for instant play. Email/magic link moved to a collapsible section for cross-device access only. - server: POST /api/auth/guest creates guest profile + session - server/db: profiles.email now nullable, is_guest column added, migration recreates table for existing installs - client: names.ts with 15 names per gender category - client: LoginPage redesigned — gender picker → name display → play - client: useAuth gains setAuth() for direct session injection - 55 server + 44 client tests pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
|
|
export type Profile = {
|
|
id: number
|
|
email: string | null
|
|
display_name: string | null
|
|
is_guest: boolean
|
|
}
|
|
type AuthState = { profile: Profile | null; token: string | null; loading: boolean }
|
|
|
|
export function useAuth() {
|
|
const [state, setState] = useState<AuthState>({ profile: null, token: null, loading: true })
|
|
|
|
const validate = useCallback(async (token: string) => {
|
|
const r = await fetch('/api/auth/me', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
if (!r.ok) throw new Error('invalid session')
|
|
const profile: Profile = await r.json()
|
|
return profile
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
// Check URL for ?session= from magic link redirect
|
|
const params = new URLSearchParams(window.location.search)
|
|
const urlToken = params.get('session')
|
|
if (urlToken) {
|
|
localStorage.setItem('session', urlToken)
|
|
window.history.replaceState({}, '', window.location.pathname)
|
|
}
|
|
|
|
const token = urlToken || localStorage.getItem('session')
|
|
if (!token) {
|
|
setState({ profile: null, token: null, loading: false })
|
|
return
|
|
}
|
|
|
|
validate(token)
|
|
.then(profile => setState({ profile, token, loading: false }))
|
|
.catch(() => {
|
|
localStorage.removeItem('session')
|
|
setState({ profile: null, token: null, loading: false })
|
|
})
|
|
}, [validate])
|
|
|
|
const logout = useCallback(async () => {
|
|
const token = localStorage.getItem('session')
|
|
if (token) {
|
|
await fetch('/api/auth/session', {
|
|
method: 'DELETE',
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
}).catch(() => {})
|
|
localStorage.removeItem('session')
|
|
}
|
|
setState({ profile: null, token: null, loading: false })
|
|
}, [])
|
|
|
|
// Used by guest login to inject a session without a round-trip
|
|
const setAuth = useCallback((token: string, profile: Profile) => {
|
|
localStorage.setItem('session', token)
|
|
setState({ profile, token, loading: false })
|
|
}, [])
|
|
|
|
return { ...state, logout, setAuth }
|
|
}
|