Files
morsequest/client/src/hooks/useAuth.ts
T
kitadmin fe5cbaec2b feat(auth): guest login with random telegraph operator name
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>
2026-04-30 04:22:05 +00:00

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 }
}