Windows installer: payload hash-locking, SBOM, client-IP handling, operator docs

This commit is contained in:
2026-08-03 15:08:20 -04:00
parent ff61a4c5e9
commit 3d43cb3d86
51 changed files with 9002 additions and 190 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
}

View File

@@ -2,7 +2,8 @@ import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import AppLayout from '../views/AppLayout.vue'
import SettingsLayout from '../views/settings/SettingsLayout.vue'
import { setupComplete, setupSkipped, isSetupLoaded, refreshSetupState } from '../composables/setupState'
import { setupComplete, setupSkipped, isSetupLoaded, refreshSetupState,
needsAdmin, isNeedsAdminLoaded, refreshNeedsAdmin } from '../composables/setupState'
import { loadEnabledPlugins, isPluginEnabled } from '../composables/enabledPlugins'
import { useToast } from '../composables/toast'
import { getFacilityName } from '../utils/siteSettings'
@@ -196,6 +197,24 @@ router.afterEach(() => {
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// FIRST RUN: until an admin exists, nothing else in the app is useful. Without
// this, a fresh instance serves an anonymous dashboard and the operator has no
// indication that setup is unfinished (they must guess their way to /login).
// Unauthenticated only, so it costs an established site nothing.
if (!authStore.isAuthenticated && to.path !== '/login') {
if (!isNeedsAdminLoaded()) {
// Bounded like the plugin gate: a hung request must not hold navigation
// open. refreshNeedsAdmin fails open on error.
await Promise.race([
refreshNeedsAdmin(),
new Promise(resolve => setTimeout(resolve, 4000)),
])
}
if (needsAdmin.value) {
return next({ path: '/login', query: { firstrun: '1' } })
}
}
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
// Remember where they were headed so login can send them back there.
return next({ path: '/login', query: { redirect: to.fullPath } })

View File

@@ -51,6 +51,7 @@ import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { setupApi } from '../api'
import { clearNeedsAdmin } from '../composables/setupState'
import { getSiteLogo } from '../utils/siteSettings'
import { withBase, stripBase } from '../utils/basePath'
@@ -102,6 +103,7 @@ async function handleCreateAdmin() {
loading.value = true
try {
await setupApi.createAdmin({ username: username.value, email: email.value, password: password.value })
clearNeedsAdmin() // an admin now exists; stop the first-run gate firing
// Log straight in with the new credentials, then on to the setup wizard.
const result = await authStore.login(username.value, password.value)
if (result.success) router.push('/setup')

View File

@@ -117,8 +117,9 @@
<!-- Starter data -->
<div v-else-if="current.key === 'data'">
<h2>Starter data</h2>
<p class="hint">Seed the data a new site needs. Both are idempotent - safe to run more than once.</p>
<h2>Reference data</h2>
<p class="hint">Seed the lookup data a new site needs: statuses, types, permissions, and a short list of common vendors. Both are idempotent - safe to run more than once.</p>
<p class="hint">This does not create any assets. The site starts empty and assets are loaded afterwards via the import API, so an empty dashboard here is expected.</p>
<div class="starter-actions">
<div>
<button class="btn btn-secondary" :disabled="seeding" @click="seedReference">
@@ -181,7 +182,7 @@ const steps = [
{ key: 'site', label: 'Site' },
{ key: 'plugins', label: 'Features' },
{ key: 'map', label: 'Floor Map' },
{ key: 'data', label: 'Starter Data' },
{ key: 'data', label: 'Reference Data' },
{ key: 'finish', label: 'Finish' },
]
const step = ref(0)