Files
HarborForge.Frontend/src/App.tsx
hzhang ba55fee9d5 fix(auth): register /settings/oidc route for admins only
Non-admins fall through to the catch-all redirect instead of seeing
the OIDC settings page shell. Sidebar link, in-page guard and the
admin-only backend API remain as defense in depth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:33:28 +01:00

148 lines
5.8 KiB
TypeScript

import { useState, useEffect } from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { useAuth } from '@/hooks/useAuth'
import Sidebar from '@/components/Sidebar'
import LoginPage from '@/pages/LoginPage'
import SetupWizardPage from '@/pages/SetupWizardPage'
import DashboardPage from '@/pages/DashboardPage'
import TasksPage from '@/pages/TasksPage'
import TaskDetailPage from '@/pages/TaskDetailPage'
import ProjectsPage from '@/pages/ProjectsPage'
import ProjectDetailPage from '@/pages/ProjectDetailPage'
import MilestonesPage from '@/pages/MilestonesPage'
import MilestoneDetailPage from '@/pages/MilestoneDetailPage'
import NotificationsPage from '@/pages/NotificationsPage'
import RoleEditorPage from '@/pages/RoleEditorPage'
import MonitorPage from '@/pages/MonitorPage'
import ProposalsPage from '@/pages/ProposalsPage'
import ProposalDetailPage from '@/pages/ProposalDetailPage'
import UsersPage from '@/pages/UsersPage'
import CalendarPage from '@/pages/CalendarPage'
import SupportDetailPage from '@/pages/SupportDetailPage'
import MeetingDetailPage from '@/pages/MeetingDetailPage'
import OidcCallbackPage from '@/pages/OidcCallbackPage'
import OidcSettingsPage from '@/pages/OidcSettingsPage'
import axios from 'axios'
const getStoredWizardPort = (): number | null => {
const stored = Number(localStorage.getItem('HF_WIZARD_PORT'))
return stored && stored > 0 ? stored : null
}
const getApiBase = () => {
return localStorage.getItem('HF_BACKEND_BASE_URL') ?? undefined
}
type AppState = 'checking' | 'setup' | 'ready'
export default function App() {
const [appState, setAppState] = useState<AppState>('checking')
const { user, loading, login, loginWithToken, logout } = useAuth()
useEffect(() => {
checkInitialized()
}, [])
const checkInitialized = async () => {
// First try the backend /config/status endpoint (reads from config volume directly)
try {
const res = await axios.get(`${getApiBase()}/config/status`, { timeout: 5000 })
const cfg = res.data || {}
if (cfg.backend_url) {
localStorage.setItem('HF_BACKEND_BASE_URL', cfg.backend_url)
}
if (cfg.initialized === true) {
setAppState('ready')
return
}
} catch {
// Backend unreachable — fall through to wizard check
}
// Fallback: if a wizard port was previously saved during setup, try it directly
const storedPort = getStoredWizardPort()
if (storedPort) {
try {
const res = await axios.get(`http://127.0.0.1:${storedPort}/api/v1/config/harborforge.json`, {
timeout: 5000,
})
const cfg = res.data || {}
if (cfg.backend_url) {
localStorage.setItem('HF_BACKEND_BASE_URL', cfg.backend_url)
}
if (cfg.initialized === true) {
setAppState('ready')
return
}
} catch {
// ignore — fall through to setup
}
}
setAppState('setup')
}
if (appState === 'checking') {
return <div className="loading">Checking configuration status...</div>
}
if (appState === 'setup') {
return <SetupWizardPage initialWizardPort={getStoredWizardPort()} onComplete={checkInitialized} />
}
if (loading) return <div className="loading">Loading...</div>
if (!user) {
return (
<BrowserRouter>
<div className="app-layout">
<Sidebar user={null} onLogout={logout} />
<main className="main-content">
<Routes>
<Route path="/roles" element={<RoleEditorPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/monitor" element={<MonitorPage />} />
<Route path="/login" element={<LoginPage onLogin={login} />} />
<Route path="/oidc/callback" element={<OidcCallbackPage onToken={loginWithToken} />} />
<Route path="*" element={<Navigate to="/monitor" />} />
</Routes>
</main>
</div>
</BrowserRouter>
)
}
return (
<BrowserRouter>
<div className="app-layout">
<Sidebar user={user} onLogout={logout} />
<main className="main-content">
<Routes>
<Route path="/" element={<DashboardPage />} />
<Route path="/tasks" element={<TasksPage />} />
<Route path="/tasks/:taskCode" element={<TaskDetailPage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/projects/:id" element={<ProjectDetailPage />} />
<Route path="/milestones" element={<MilestonesPage />} />
<Route path="/milestones/:milestoneCode" element={<MilestoneDetailPage />} />
<Route path="/proposals" element={<ProposalsPage />} />
<Route path="/proposals/:proposalCode" element={<ProposalDetailPage />} />
<Route path="/calendar" element={<CalendarPage />} />
{/* Legacy routes for backward compatibility */}
<Route path="/proposes" element={<ProposalsPage />} />
<Route path="/proposes/:proposalCode" element={<ProposalDetailPage />} />
<Route path="/meetings/:meetingCode" element={<MeetingDetailPage />} />
<Route path="/supports/:supportCode" element={<SupportDetailPage />} />
<Route path="/notifications" element={<NotificationsPage />} />
<Route path="/roles" element={<RoleEditorPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/monitor" element={<MonitorPage />} />
{user?.is_admin && <Route path="/settings/oidc" element={<OidcSettingsPage />} />}
<Route path="/oidc/callback" element={<OidcCallbackPage onToken={loginWithToken} />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</main>
</div>
</BrowserRouter>
)
}