Add username-based login + mandatory first-time admin setup; push-ready

- User id/login = username (was email). Email is a separate settable field.
- Default admin: username admin / password 1234, must_setup=True.
- Login forces /setup on first login: set email + change password, then clears must_setup.
- New /api/auth/setup endpoint; JWT sub = username; admin routes use username.
- Frontend: Login uses username, router guard forces /setup, new Setup.vue (email + new
  password + confirm), i18n EN/TH.
- Tests: test_setup.py added; all suites adapted (m0/m1/routes/security/setup/e2e) PASS.
This commit is contained in:
Macky
2026-08-07 17:56:59 +07:00
parent eadf4f727a
commit e8a95bf561
16 changed files with 349 additions and 90 deletions

View File

@@ -3,8 +3,8 @@
<div class="card login-card">
<h1>🎯 {{ i18n.t('app') }}</h1>
<p class="muted" style="margin-top:-8px">Sales training simulator</p>
<label>{{ i18n.t('email') }}</label>
<input v-model="email" type="email" autocomplete="username" @keyup.enter="submit" />
<label>{{ i18n.t('username') }}</label>
<input v-model="username" type="text" autocomplete="username" @keyup.enter="submit" />
<label>{{ i18n.t('password') }}</label>
<div class="pw-wrap">
<input v-model="password" :type="showPw ? 'text' : 'password'" autocomplete="current-password" @keyup.enter="submit" />
@@ -13,7 +13,7 @@
</button>
</div>
<div class="error" role="alert" v-if="error">{{ error }}</div>
<button class="primary" style="width:100%;margin-top:16px" :disabled="loading || !email || !password" @click="submit">
<button class="primary" style="width:100%;margin-top:16px" :disabled="loading || !username || !password" @click="submit">
<span v-if="loading" class="spinner"></span>
<span v-else>{{ i18n.t('login') }}</span>
</button>
@@ -29,7 +29,7 @@ import { i18n } from '../i18n'
const route = useRoute()
const router = useRouter()
const email = ref('')
const username = ref('')
const password = ref('')
const showPw = ref(false)
const error = ref('')
@@ -39,8 +39,13 @@ async function submit() {
error.value = ''
loading.value = true
try {
await auth.login(email.value, password.value)
router.push(route.query.redirect || '/')
await auth.login(username.value.trim(), password.value)
// First-time admin setup is mandatory before using the app.
if (auth.mustSetup) {
router.push({ path: '/setup' })
} else {
router.push(route.query.redirect || '/')
}
} catch (e) {
error.value = i18n.t('loginError')
} finally {