feat(01-02): wire OIDC middleware, /api/me route, and authenticated PWA shell
- src/auth/middleware.ts: re-exports oidcAuthMiddleware, processOAuthCallback,
getAuth from @hono/oidc-auth; documents required env vars and AUTH-02
refresh-token rotation (no iframe, D-12)
- src/routes/me.ts: GET / calls getAuth → upsertUser(iss, sub, email) →
returns { user: { id, displayName, color } }; identity keyed on iss+sub
- src/index.ts: /callback registered before oidcAuthMiddleware; /api/*
guarded; /health remains unauthenticated; /api/me mounted
- apps/pwa/src/api/client.ts: typed fetchMe() with credentials: 'include'
- apps/pwa/src/App.tsx: useQuery(['me'], fetchMe); renders member name and
color swatch; retains /health stack indicator from Plan 01
- tsc --noEmit: clean; all tests pass
This commit is contained in:
+70
-7
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchMe, type MeUser } from './api/client'
|
||||
|
||||
interface HealthResponse {
|
||||
ok: boolean
|
||||
@@ -13,8 +14,54 @@ async function fetchHealth(): Promise<HealthResponse> {
|
||||
return res.json() as Promise<HealthResponse>
|
||||
}
|
||||
|
||||
function ColorSwatch({ color }: { color: string }) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
borderRadius: '50%',
|
||||
background: color,
|
||||
marginRight: '0.5rem',
|
||||
verticalAlign: 'middle',
|
||||
border: '1px solid rgba(0,0,0,0.1)',
|
||||
}}
|
||||
aria-label={`Color: ${color}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MemberBadge({ user }: { user: MeUser }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0.75rem 1rem',
|
||||
borderRadius: '8px',
|
||||
background: '#f0f9ff',
|
||||
border: `2px solid ${user.color}`,
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
<ColorSwatch color={user.color} />
|
||||
<span style={{ fontWeight: 600 }}>
|
||||
{user.displayName ?? 'Member'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
const meQuery = useQuery({
|
||||
queryKey: ['me'],
|
||||
queryFn: fetchMe,
|
||||
retry: false, // 401 triggers Authelia redirect — don't retry
|
||||
staleTime: 5 * 60 * 1000, // 5 min — session persists via refresh rotation
|
||||
})
|
||||
|
||||
const healthQuery = useQuery({
|
||||
queryKey: ['health'],
|
||||
queryFn: fetchHealth,
|
||||
retry: 1,
|
||||
@@ -23,18 +70,34 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', padding: '2rem', maxWidth: '480px', margin: '0 auto' }}>
|
||||
<h1 style={{ fontSize: '1.5rem', marginBottom: '1rem' }}>FamilySync</h1>
|
||||
<h1 style={{ fontSize: '1.5rem', marginBottom: '1.5rem' }}>FamilySync</h1>
|
||||
|
||||
{/* Authenticated member identity (AUTH-03) */}
|
||||
{meQuery.isLoading && (
|
||||
<div style={{ color: '#666', marginBottom: '1rem' }}>Loading...</div>
|
||||
)}
|
||||
{meQuery.isError && (
|
||||
<div style={{ color: '#991b1b', marginBottom: '1rem', padding: '0.75rem', background: '#fee2e2', borderRadius: '8px' }}>
|
||||
Sign-in required
|
||||
</div>
|
||||
)}
|
||||
{meQuery.data && (
|
||||
<MemberBadge user={meQuery.data.user} />
|
||||
)}
|
||||
|
||||
{/* Stack health indicator (from Plan 01) */}
|
||||
<div
|
||||
style={{
|
||||
padding: '1rem',
|
||||
borderRadius: '8px',
|
||||
background: isLoading ? '#f5f5f5' : isError ? '#fee2e2' : '#dcfce7',
|
||||
color: isLoading ? '#666' : isError ? '#991b1b' : '#166534',
|
||||
background: healthQuery.isLoading ? '#f5f5f5' : healthQuery.isError ? '#fee2e2' : '#dcfce7',
|
||||
color: healthQuery.isLoading ? '#666' : healthQuery.isError ? '#991b1b' : '#166534',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{isLoading && 'Checking stack...'}
|
||||
{isError && 'stack: down'}
|
||||
{data && `stack: ${data.ok && data.db === 'up' ? 'up' : 'down'}`}
|
||||
{healthQuery.isLoading && 'Checking stack...'}
|
||||
{healthQuery.isError && 'stack: down'}
|
||||
{healthQuery.data && `stack: ${healthQuery.data.ok && healthQuery.data.db === 'up' ? 'up' : 'down'}`}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user