feat(frontend): gate first run on needs-admin so a fresh instance shows the wizard

A fresh install landed on the anonymous dashboard instead of prompting to create
the first admin, so an operator had no way to discover /setup.

The router now asks /api/setup/needs-admin before rendering any unauthenticated
route and redirects to /login?firstrun=1 while no user exists. The result is
cached in a composable so it costs one request per session, and the lookup fails
open (a backend that cannot answer must not lock the login screen). Racing it
against a 4s timeout keeps a slow or hung backend from blocking the first paint.

Login.vue clears the flag after creating the admin so the gate stops firing
without a reload.
This commit is contained in:
2026-08-02 16:08:42 -04:00
parent 6639afd1f0
commit 94d6878a03
3 changed files with 51 additions and 2 deletions

View File

@@ -2,7 +2,7 @@
// admin into the /setup wizard. Defaults to "complete" so the wizard never
// flashes before the real value loads.
import { ref } from 'vue'
import { settingsApi } from '../api'
import { settingsApi, setupApi } from '../api'
export const setupComplete = ref(true)
// Session-only: a fresh admin who clicks "Skip for now" is not nagged again
@@ -26,3 +26,31 @@ export async function refreshSetupState() {
loaded.value = true
return setupComplete.value
}
// First run, before any user exists. Defaults to false so the app never flashes
// the create-admin screen at an established site while this loads.
export const needsAdmin = ref(false)
const adminLoaded = ref(false)
export function isNeedsAdminLoaded() {
return adminLoaded.value
}
export async function refreshNeedsAdmin() {
try {
const response = await setupApi.needsAdmin()
needsAdmin.value = response.data?.data?.needsadmin === true
} catch (err) {
// Fail open: if we cannot tell, assume an admin exists rather than trap
// everyone behind a create-admin screen they cannot complete.
needsAdmin.value = false
}
adminLoaded.value = true
return needsAdmin.value
}
// Called after the first admin is created so the gate stops firing.
export function clearNeedsAdmin() {
needsAdmin.value = false
adminLoaded.value = true
}