1
0

feat: typed api client and auth context with setup gate

This commit is contained in:
2026-08-29 19:34:02 +02:00
parent 63e8c5f054
commit fdf200a960
4 changed files with 308 additions and 0 deletions

70
web/src/auth.tsx Normal file
View File

@@ -0,0 +1,70 @@
import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react'
import { api } from './api'
import type { User } from './types'
export type AuthStatus = 'loading' | 'setup' | 'unauthenticated' | 'authenticated'
interface AuthContextValue {
status: AuthStatus
user: User | null
setupNeeded: boolean
refresh: () => Promise<void>
onSetupComplete: (user: User) => void
onLogin: (user: User) => void
onLogout: () => void
}
const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [status, setStatus] = useState<AuthStatus>('loading')
const [user, setUser] = useState<User | null>(null)
const [setupNeeded, setSetupNeeded] = useState(false)
const refresh = useCallback(async () => {
try {
const { needed } = await api.setupStatus()
if (needed) {
setSetupNeeded(true)
setStatus('setup')
return
}
setSetupNeeded(false)
const { user: me } = await api.me()
setUser(me)
setStatus('authenticated')
} catch {
setStatus('unauthenticated')
}
}, [])
useEffect(() => {
void refresh()
}, [refresh])
const onSetupComplete = useCallback((u: User) => {
setSetupNeeded(false)
setUser(u)
setStatus('authenticated')
}, [])
const onLogin = useCallback((u: User) => {
setUser(u)
setStatus('authenticated')
}, [])
const onLogout = useCallback(() => {
setUser(null)
setStatus('unauthenticated')
}, [])
return (
<AuthContext.Provider value={{ status, user, setupNeeded, refresh, onSetupComplete, onLogin, onLogout }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth outside AuthProvider')
return ctx
}