feat: first-run setup wizard on initial admin login

After first login, admins are redirected to a 4-step setup wizard:
1. Server hostname (FQDN)
2. Primary and secondary nameservers
3. Admin email and optional password change
4. Review and confirm

Setup state is persisted in the settings table (setup_complete=true).
Once complete, the redirect never triggers again. Settings are saved
to the DB and hostname is applied via hostnamectl.
This commit is contained in:
NixReaper
2026-04-05 13:17:16 -05:00
parent 75394fcaa5
commit fa61d0d766
5 changed files with 414 additions and 14 deletions
+23 -1
View File
@@ -11,6 +11,8 @@ interface User {
interface AuthContextValue {
user: User | null
loading: boolean
setupComplete: boolean
markSetupComplete: () => void
login: (username: string, password: string) => Promise<void>
logout: () => Promise<void>
}
@@ -20,6 +22,7 @@ const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [setupComplete, setSetupComplete] = useState(true)
const fetchMe = useCallback(async () => {
const token = localStorage.getItem('access_token')
@@ -32,6 +35,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return
}
setUser(data.data)
// Only admins trigger the setup check
if (data.data.role === 'admin') {
const { data: setupData } = await api.get('/nixserver/setup/status')
setSetupComplete(setupData.data.setupComplete)
}
} catch {
localStorage.clear()
} finally {
@@ -49,6 +58,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
localStorage.setItem('access_token', data.data.accessToken)
localStorage.setItem('refresh_token', data.data.refreshToken)
setUser(data.data.user)
// Check setup status after login
if (data.data.role === 'admin') {
try {
const { data: setupData } = await api.get('/nixserver/setup/status')
setSetupComplete(setupData.data.setupComplete)
} catch {
setSetupComplete(true) // fail open — don't block login on setup check failure
}
}
}
const logout = async () => {
@@ -56,10 +75,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
try { await api.post('/auth/logout', { refreshToken }) } catch {}
localStorage.clear()
setUser(null)
setSetupComplete(true)
}
const markSetupComplete = () => setSetupComplete(true)
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
<AuthContext.Provider value={{ user, loading, setupComplete, markSetupComplete, login, logout }}>
{children}
</AuthContext.Provider>
)