fix: AuthGate stuck on Loading — verify returns flat user object

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>
This commit is contained in:
jtricerolph 2026-07-03 16:25:20 +00:00
parent 2993dfa216
commit 5a2dab637c
5 changed files with 14 additions and 7 deletions

View file

@ -24,7 +24,7 @@ export default function AuthGate({ children }: { children: React.ReactNode }) {
if (!r.ok) throw new Error(`Auth check failed: ${r.status}`) if (!r.ok) throw new Error(`Auth check failed: ${r.status}`)
return r.json() return r.json()
}) })
.then(data => { if (data) setUser(data.user) }) .then(data => { if (data) setUser(data) })
.catch(err => setError(err.message)) .catch(err => setError(err.message))
}, []) }, [])

View file

@ -1,12 +1,13 @@
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { BedDouble, Settings, LogOut } from 'lucide-react' import { BedDouble, Settings, LogOut } from 'lucide-react'
import { useAuth } from './AuthGate' import { useAuth } from './AuthGate'
import { can } from '../types'
const ICON_PROPS = { size: 16, strokeWidth: 1.75 } const ICON_PROPS = { size: 16, strokeWidth: 1.75 }
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const { user } = useAuth() const { user } = useAuth()
const hasCap = (cap: string) => user.caps.includes(`room-planner:${cap}`) const hasCap = (cap: string) => can(user, cap)
return ( return (
<div className="app-shell"> <div className="app-shell">

View file

@ -1,5 +1,6 @@
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { X, CheckSquare, Square, Package, ClipboardList } from 'lucide-react' import { X, CheckSquare, Square, Package, ClipboardList } from 'lucide-react'
import { can } from '../types'
import type { RoomData, TaskData, AppConfig, User } from '../types' import type { RoomData, TaskData, AppConfig, User } from '../types'
import { bookingStatusColor, isDirty } from '../lib/booking-flow' import { bookingStatusColor, isDirty } from '../lib/booking-flow'
import { detectTwin } from '../lib/twin-detect' import { detectTwin } from '../lib/twin-detect'
@ -36,7 +37,7 @@ export default function RoomModal({ room, viewDate, config, user, onClose, onTas
const [statusLoading, setStatusLoading] = useState(false) const [statusLoading, setStatusLoading] = useState(false)
const [notesTab, setNotesTab] = useState<'booking' | 'departing'>('booking') const [notesTab, setNotesTab] = useState<'booking' | 'departing'>('booking')
const hasCap = (cap: string) => user.caps.includes(`room-planner:${cap}`) const hasCap = (cap: string) => can(user, cap)
const todayTasks = room.tasks.filter(t => { const todayTasks = room.tasks.filter(t => {
const d = t.task_when_date || t.task_period_from const d = t.task_when_date || t.task_period_from

View file

@ -1,11 +1,12 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useAuth } from '../components/AuthGate' import { useAuth } from '../components/AuthGate'
import { can } from '../types'
import type { AppConfig, TaskDisplayConfig } from '../types' import type { AppConfig, TaskDisplayConfig } from '../types'
import * as api from '../api' import * as api from '../api'
export default function Settings() { export default function Settings() {
const { user } = useAuth() const { user } = useAuth()
const hasCap = (cap: string) => user.caps.includes(`room-planner:${cap}`) const hasCap = (cap: string) => can(user, cap)
const [config, setConfig] = useState<AppConfig | null>(null) const [config, setConfig] = useState<AppConfig | null>(null)
const [taskTypes, setTaskTypes] = useState<Array<{ id: string; name: string }>>([]) const [taskTypes, setTaskTypes] = useState<Array<{ id: string; name: string }>>([])

View file

@ -126,11 +126,15 @@ export interface AppConfig {
} }
export interface User { export interface User {
id: number user_id: number
name: string name: string
email: string email: string
apps: string[] is_admin: boolean
caps: string[] caps: string[] // bare slugs — verify?app=room-planner strips the prefix
}
export function can(user: User, cap: string): boolean {
return user.is_admin || user.caps.includes(cap)
} }
export type PlannerCap = export type PlannerCap =