feat(saas): Phase 3 — plan/seats/active model, ToS consent, signed expiring export

P3a: org carries plan/seats/active/created_at; create_user enforces seats + rejects
inactive org; verify blocks login for inactive orgs; PATCH /api/admin/orgs (super_admin)
updates plan/seats/active with audit. Fixed verify swallowing its AuthError.
P3b: export/token issues a 5-min HMAC one-time CSV link; export accepts ?token=.
P3c: setup requires accepted_terms (consent stored); Setup.vue consent checkbox.
All 8 backend suites pass. Rebuilt dist.
This commit is contained in:
Macky
2026-08-09 09:48:55 +07:00
parent 056753e8cb
commit 3d5c81fbd7
36 changed files with 230 additions and 64 deletions

View File

@@ -37,8 +37,8 @@ export const auth = reactive({
this.mustSetup = !!data.must_setup
return data.user
},
async finishSetup(email, password) {
const data = await api.setup({ username: this.user.username || this.user.id, email, password })
async finishSetup(email, password, acceptedTerms = false) {
const data = await api.setup({ username: this.user.username || this.user.id, email, password, accepted_terms: acceptedTerms })
this.user = data.user
this.mustSetup = false
return data.user

View File

@@ -13,9 +13,14 @@
<label>{{ i18n.t('confirmPassword') }}</label>
<input v-model="confirm" type="password" autocomplete="new-password" class="wide" @keyup.enter="submit" />
<label class="terms">
<input v-model="accepted" type="checkbox" />
<span>นยอมร <a :href="termsUrl" target="_blank" rel="noopener">อกำหนดการใชงาน</a> และ <a :href="termsUrl" target="_blank" rel="noopener">นโยบายความเปนสวนต</a></span>
</label>
<div class="error" role="alert" v-if="error">{{ error }}</div>
<button class="primary" style="width:100%;margin-top:16px" :disabled="busy || !email || !password || password !== confirm" @click="submit">
<button class="primary" style="width:100%;margin-top:16px" :disabled="busy || !email || !password || password !== confirm || !accepted" @click="submit">
<span v-if="busy" class="spinner"></span>
<span v-else>{{ i18n.t('save') }}</span>
</button>
@@ -34,6 +39,8 @@ const router = useRouter()
const email = ref('')
const password = ref('')
const confirm = ref('')
const accepted = ref(false)
const termsUrl = '/legal'
const error = ref('')
const busy = ref(false)
@@ -49,7 +56,7 @@ async function submit() {
}
busy.value = true
try {
await auth.finishSetup(email.value.trim(), password.value)
await auth.finishSetup(email.value.trim(), password.value, true)
router.push('/')
} catch (e) {
error.value = e.message
@@ -67,4 +74,7 @@ async function submit() {
.setup-card { width: 460px; max-width: 100%; padding: 28px 30px; }
h1 { margin-top: 0; font-size: 24px; }
.wide { width: 100%; min-height: 46px; font-size: 15px; padding: 12px 14px; }
.terms { display: flex; align-items: center; gap: 8px; margin-top: 14px; font-size: 13px; }
.terms input { width: auto; min-height: auto; margin: 0; }
.terms a { color: var(--accent); }
</style>