Implement Issue #11: Initial admin account setup on first deploy
When the database has no users, the UI redirects to /setup and prompts for creation of the first admin account. The setup endpoints are self-disabling — once any user exists, POST /setup/admin returns 403 and the frontend redirects /setup back to /login. The backend uses an atomic transaction to prevent race conditions on concurrent requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { BrowserRouter, Routes, Route, NavLink, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider, useAuth } from './auth';
|
||||
import { api } from './api';
|
||||
import Login from './pages/Login';
|
||||
import Activate from './pages/Activate';
|
||||
import Setup from './pages/Setup';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Schedules from './pages/Schedules';
|
||||
import TimeOff from './pages/TimeOff';
|
||||
@@ -50,15 +52,57 @@ function LoginRoute() {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
// Setup context lets the Setup page flip needsSetup after creating the admin.
|
||||
const SetupContext = createContext<{ setNeedsSetup: (v: boolean) => void }>({
|
||||
setNeedsSetup: () => {},
|
||||
});
|
||||
export const useSetup = () => useContext(SetupContext);
|
||||
|
||||
function SetupGate({ children }: { children: ReactNode }) {
|
||||
const [needsSetup, setNeedsSetup] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.getSetupStatus()
|
||||
.then(r => setNeedsSetup(r.needs_setup))
|
||||
.catch(() => setNeedsSetup(false));
|
||||
}, []);
|
||||
|
||||
if (needsSetup === null) return null;
|
||||
|
||||
if (needsSetup) {
|
||||
return (
|
||||
<SetupContext.Provider value={{ setNeedsSetup }}>
|
||||
<Routes>
|
||||
<Route path="/setup" element={<Setup />} />
|
||||
<Route path="*" element={<Navigate to="/setup" replace />} />
|
||||
</Routes>
|
||||
</SetupContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SetupContext.Provider value={{ setNeedsSetup }}>
|
||||
<Routes>
|
||||
<Route path="/setup" element={<Navigate to="/login" replace />} />
|
||||
<Route path="/login" element={<LoginRoute />} />
|
||||
<Route path="/activate" element={<Activate />} />
|
||||
<Route path="/*" element={<ProtectedLayout />} />
|
||||
</Routes>
|
||||
</SetupContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginRoute />} />
|
||||
<Route path="/activate" element={<Activate />} />
|
||||
<Route path="/*" element={<ProtectedLayout />} />
|
||||
</Routes>
|
||||
<SetupGate>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginRoute />} />
|
||||
<Route path="/activate" element={<Activate />} />
|
||||
<Route path="/*" element={<ProtectedLayout />} />
|
||||
</Routes>
|
||||
</SetupGate>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user