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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user