The auth /verify endpoint returns { user_id, name, email, is_admin, caps }
directly, not wrapped in a user key, so data.user was undefined and the
gate never rendered. Also: caps come back as bare slugs when ?app= is
passed, so the prefixed room-planner:cap checks always failed — replaced
with the standard can(user, cap) helper honouring is_admin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
51 lines
1.5 KiB
TypeScript
51 lines
1.5 KiB
TypeScript
import { useEffect, useState, createContext, useContext } from 'react'
|
|
import type { User } from '../types'
|
|
|
|
interface AuthCtx { user: User }
|
|
const Ctx = createContext<AuthCtx | null>(null)
|
|
|
|
export function useAuth() {
|
|
const ctx = useContext(Ctx)
|
|
if (!ctx) throw new Error('useAuth must be used inside AuthGate')
|
|
return ctx
|
|
}
|
|
|
|
export default function AuthGate({ children }: { children: React.ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
fetch('/api/auth/verify?app=room-planner', { credentials: 'include' })
|
|
.then(r => {
|
|
if (r.status === 401 || r.status === 403) {
|
|
window.location.href = `/portal?redirect=${encodeURIComponent(window.location.href)}`
|
|
return null
|
|
}
|
|
if (!r.ok) throw new Error(`Auth check failed: ${r.status}`)
|
|
return r.json()
|
|
})
|
|
.then(data => { if (data) setUser(data) })
|
|
.catch(err => setError(err.message))
|
|
}, [])
|
|
|
|
if (error) {
|
|
return (
|
|
<div style={{ padding: 32, color: '#991b1b', fontFamily: 'sans-serif' }}>
|
|
Authentication error: {error}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!user) {
|
|
return (
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
height: '100vh', fontFamily: 'sans-serif', color: '#6b7280'
|
|
}}>
|
|
Loading…
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return <Ctx.Provider value={{ user }}>{children}</Ctx.Provider>
|
|
}
|