FastAPI backend (Python 3.11, httpx for SignalR/GraphQL — no MSSQL ODBC), shares kitchen_db directly. React/TS/Vite fullscreen board frontend. Backend: auth.py (APP_SLUG=kds, SimpleNamespace), main.py (4 KDS migrations, SignalR start/stop), kds.py router, models (kds/settings/resos — read from kitchen_db), signalr_listener.py (backoff pre-existing), kds_graphql.py, database.py. Requirements stripped to ~9 packages; image ~400 MB lighter than kitchen (no MSSQL ODBC layer). Frontend: AuthGate (app=kds), single fullscreen route, dark board theme. KDS.tsx URL prefix patched (/api/kds/ → /kds/api/kds/), recipe images cross-app (/kitchen/api/recipes/). nginx: 5 blocks with SSE proxy headers on /kds/api/ block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
67 lines
1.7 KiB
TypeScript
67 lines
1.7 KiB
TypeScript
import { createContext, useContext, useEffect, useState } from 'react'
|
|
import type { ReactNode } from 'react'
|
|
import type { User } from '../types'
|
|
|
|
interface AuthCtx {
|
|
user: User
|
|
token: string
|
|
restrictedPages: string[]
|
|
login: (t: string) => void
|
|
logout: () => void
|
|
}
|
|
const Ctx = createContext<AuthCtx | null>(null)
|
|
|
|
export function useAuth() {
|
|
const ctx = useContext(Ctx)
|
|
if (!ctx) throw new Error('useAuth outside AuthGate')
|
|
return ctx
|
|
}
|
|
|
|
export default function AuthGate({ children }: { children: ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null)
|
|
const [checking, setChecking] = useState(true)
|
|
|
|
useEffect(() => {
|
|
fetch('/kds/api/auth/verify?app=kds', { credentials: 'include' })
|
|
.then((r) => {
|
|
if (!r.ok) throw new Error('unauth')
|
|
return r.json()
|
|
})
|
|
.then((data) =>
|
|
setUser({
|
|
email: data.email || data.sub || '',
|
|
name: data.name || data.display_name || '',
|
|
is_admin: data.is_admin ?? false,
|
|
caps: data.caps ?? [],
|
|
})
|
|
)
|
|
.catch(() => {
|
|
;(window.top ?? window).location.href = '/login'
|
|
})
|
|
.finally(() => setChecking(false))
|
|
}, [])
|
|
|
|
if (checking) {
|
|
return (
|
|
<div style={{
|
|
position: 'fixed', inset: 0,
|
|
background: 'var(--kds-bg)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}}>
|
|
<div className="spinner" style={{ width: 32, height: 32 }} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!user) return null
|
|
|
|
const ctx: AuthCtx = {
|
|
user,
|
|
token: '__session__',
|
|
restrictedPages: [],
|
|
login: () => {},
|
|
logout: () => { (window.top ?? window).location.href = '/login' },
|
|
}
|
|
|
|
return <Ctx.Provider value={ctx}>{children}</Ctx.Provider>
|
|
}
|