Initial KDS scaffold — Phase 2 kitchen port
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>
This commit is contained in:
commit
b94585084a
35 changed files with 5195 additions and 0 deletions
67
frontend/src/components/AuthGate.tsx
Normal file
67
frontend/src/components/AuthGate.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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>
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue