First-run creates the superadmin in-app; wizard seeds reference data
No CLI needed for a UI-driven install. - Login page: when the instance has no users yet (/setup/needs-admin), it shows a "create the first admin" form instead of login. Submitting creates the admin (admin role = full access / superadmin) via /setup/create-admin, logs in, and goes to the setup wizard. - Wizard Starter Data step: "Seed core reference data" button (statuses, types, permissions, default settings via /setup/seed-reference) alongside the common vendors, so a UI-first install gets the data the app needs. - Add waitress to requirements.txt (the IIS web.config launches it but it was missing - found while validating the Windows install). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,34 +4,40 @@
|
|||||||
<img src="/ge-aerospace-logo.svg" alt="GE Aerospace" class="login-logo" />
|
<img src="/ge-aerospace-logo.svg" alt="GE Aerospace" class="login-logo" />
|
||||||
<h1>ShopDB</h1>
|
<h1>ShopDB</h1>
|
||||||
|
|
||||||
<div v-if="error" class="error-message">
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="handleLogin">
|
<!-- First run: no users yet - create the first admin -->
|
||||||
|
<template v-if="mode === 'createadmin'">
|
||||||
|
<p class="first-run-note">First run - create your admin account.</p>
|
||||||
|
<form @submit.prevent="handleCreateAdmin">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newusername">Username <span class="hint">(your SSO)</span></label>
|
||||||
|
<input id="newusername" v-model="username" type="text" class="form-control" required autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input id="email" v-model="email" type="email" class="form-control" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newpassword">Password</label>
|
||||||
|
<input id="newpassword" v-model="password" type="password" class="form-control" required minlength="8" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||||
|
{{ loading ? 'Creating...' : 'Create admin & continue' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Normal login -->
|
||||||
|
<form v-else @submit.prevent="handleLogin">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input
|
<input id="username" v-model="username" type="text" class="form-control" required autofocus />
|
||||||
id="username"
|
|
||||||
v-model="username"
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
required
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input
|
<input id="password" v-model="password" type="password" class="form-control" required />
|
||||||
id="password"
|
|
||||||
v-model="password"
|
|
||||||
type="password"
|
|
||||||
class="form-control"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" :disabled="loading">
|
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||||
{{ loading ? 'Logging in...' : 'Login' }}
|
{{ loading ? 'Logging in...' : 'Login' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -41,30 +47,56 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import { setupApi } from '../api'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const mode = ref('login')
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
|
const email = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// If the instance has no users yet, offer to create the first admin.
|
||||||
|
try {
|
||||||
|
const response = await setupApi.needsAdmin()
|
||||||
|
if (response.data?.data?.needsadmin) mode.value = 'createadmin'
|
||||||
|
} catch (err) { /* fall back to normal login */ }
|
||||||
|
})
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
const result = await authStore.login(username.value, password.value)
|
const result = await authStore.login(username.value, password.value)
|
||||||
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
if (result.success) router.push('/')
|
||||||
|
else error.value = result.message
|
||||||
|
}
|
||||||
|
|
||||||
if (result.success) {
|
async function handleCreateAdmin() {
|
||||||
router.push('/')
|
error.value = ''
|
||||||
} else {
|
loading.value = true
|
||||||
error.value = result.message
|
try {
|
||||||
|
await setupApi.createAdmin({ username: username.value, email: email.value, password: password.value })
|
||||||
|
// 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')
|
||||||
|
else { mode.value = 'login'; error.value = 'Admin created - please sign in.' }
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.response?.data?.data?.error?.message || err.response?.data?.message || 'Could not create admin'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.first-run-note { color: var(--text-light); font-size: 0.9rem; margin-bottom: 1rem; }
|
||||||
|
.hint { color: var(--text-light); font-weight: normal; font-size: 0.85em; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -112,10 +112,21 @@
|
|||||||
<!-- Starter data -->
|
<!-- Starter data -->
|
||||||
<div v-else-if="current.key === 'data'">
|
<div v-else-if="current.key === 'data'">
|
||||||
<h2>Starter data</h2>
|
<h2>Starter data</h2>
|
||||||
<p class="hint">Add common hardware vendors so you can start adding assets right away. Safe to run - it skips ones you already have.</p>
|
<p class="hint">Seed the data a new site needs. Both are idempotent - safe to run more than once.</p>
|
||||||
<button class="btn btn-secondary" :disabled="seeding" @click="seedStarter">
|
<div class="starter-actions">
|
||||||
{{ seeding ? 'Adding...' : 'Add common vendors' }}
|
<div>
|
||||||
</button>
|
<button class="btn btn-secondary" :disabled="seeding" @click="seedReference">
|
||||||
|
{{ seeding ? 'Working...' : 'Seed core reference data' }}
|
||||||
|
</button>
|
||||||
|
<p class="hint">Statuses, machine/location/relationship types, permissions, default settings. Run this if you installed without the CLI seed.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-secondary" :disabled="seeding" @click="seedStarter">
|
||||||
|
{{ seeding ? 'Working...' : 'Add common vendors' }}
|
||||||
|
</button>
|
||||||
|
<p class="hint">Dell, HP, Lenovo, and other common hardware vendors.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p v-if="seedResult" class="seed-result">{{ seedResult }}</p>
|
<p v-if="seedResult" class="seed-result">{{ seedResult }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -325,6 +336,18 @@ async function togglePlugin(plugin, enabled) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function seedReference() {
|
||||||
|
seeding.value = true
|
||||||
|
try {
|
||||||
|
const response = await setupApi.seedReference()
|
||||||
|
seedResult.value = response.data?.message || 'Reference data seeded.'
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(apiError(err, 'Could not seed reference data'))
|
||||||
|
} finally {
|
||||||
|
seeding.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function seedStarter() {
|
async function seedStarter() {
|
||||||
seeding.value = true
|
seeding.value = true
|
||||||
try {
|
try {
|
||||||
@@ -384,6 +407,8 @@ async function finish() {
|
|||||||
.provision-note p:last-child { margin-bottom: 0; }
|
.provision-note p:last-child { margin-bottom: 0; }
|
||||||
.provision-tables code, .provision-docs code { background: var(--bg-card); border: 1px solid var(--border); border-radius: 3px; padding: 0 0.3rem; margin-right: 0.3rem; font-size: 0.78rem; }
|
.provision-tables code, .provision-docs code { background: var(--bg-card); border: 1px solid var(--border); border-radius: 3px; padding: 0 0.3rem; margin-right: 0.3rem; font-size: 0.78rem; }
|
||||||
.seed-result { margin-top: 0.75rem; color: var(--success); font-size: 0.88rem; }
|
.seed-result { margin-top: 0.75rem; color: var(--success); font-size: 0.88rem; }
|
||||||
|
.starter-actions { display: flex; flex-direction: column; gap: 1.1rem; }
|
||||||
|
.starter-actions .hint { margin: 0.35rem 0 0; }
|
||||||
.config-block { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--border); }
|
.config-block { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--border); }
|
||||||
.config-title { margin: 0 0 0.75rem; font-size: 1rem; text-transform: capitalize; }
|
.config-title { margin: 0 0 0.75rem; font-size: 1rem; text-transform: capitalize; }
|
||||||
.secret-tag { margin-left: 0.5rem; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--warning); border: 1px solid var(--warning); border-radius: 4px; padding: 0 0.3rem; }
|
.secret-tag { margin-left: 0.5rem; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--warning); border: 1px solid var(--warning); border-radius: 4px; padding: 0 0.3rem; }
|
||||||
|
|||||||
@@ -117,3 +117,4 @@ werkzeug==3.1.8
|
|||||||
# flask-cors
|
# flask-cors
|
||||||
# flask-jwt-extended
|
# flask-jwt-extended
|
||||||
# pytest-flask
|
# pytest-flask
|
||||||
|
waitress>=3.0
|
||||||
|
|||||||
Reference in New Issue
Block a user