mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 14:06:28 +00:00
feat(email): require verified approval email
Backfill approved legacy users with verified emails and replace the old unverified-email warning setting with a single approval email policy.
This commit is contained in:
@@ -48,12 +48,8 @@ export const userApi = {
|
||||
tokenStatus: () => apiClient.get<TokenStatus>('/api/users/me/token_status'),
|
||||
setEmail: (email: string) => apiClient.put<User>('/api/users/me/email', { email }),
|
||||
verifyEmail: (code: string) => apiClient.post<User>('/api/users/me/email/verify', { code }),
|
||||
updateProfile: (payload: {
|
||||
alias?: string
|
||||
email?: string
|
||||
current_password?: string
|
||||
new_password?: string
|
||||
}) => apiClient.put<User>('/api/users/me/profile', payload),
|
||||
updateProfile: (payload: { alias?: string; current_password?: string; new_password?: string }) =>
|
||||
apiClient.put<User>('/api/users/me/profile', payload),
|
||||
list: (params: Record<string, unknown> = {}) => apiClient.get<User[]>('/api/users', params),
|
||||
create: (payload: Partial<User> & { password?: string }) =>
|
||||
apiClient.post<User>('/api/users', payload),
|
||||
|
||||
@@ -215,7 +215,7 @@ export interface EmailNotificationSettings {
|
||||
notify_token_expiring: boolean
|
||||
notify_check_in_success: boolean
|
||||
require_admin_approval_for_registration: boolean
|
||||
warn_unverified_email_before_approval: boolean
|
||||
require_verified_email_for_approval: boolean
|
||||
has_smtp_sender_password: boolean
|
||||
created_at?: string | null
|
||||
updated_at?: string | null
|
||||
@@ -229,7 +229,7 @@ export interface EmailNotificationSettingsUpdate {
|
||||
notify_token_expiring: boolean
|
||||
notify_check_in_success: boolean
|
||||
require_admin_approval_for_registration: boolean
|
||||
warn_unverified_email_before_approval: boolean
|
||||
require_verified_email_for_approval: boolean
|
||||
smtp_sender_password?: string
|
||||
clear_smtp_sender_password?: boolean
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Save } from 'lucide-vue-next'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { MailCheck, Save, Send } from 'lucide-vue-next'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { userApi, type TokenStatus } from '@/api'
|
||||
import { useAuth } from '@/app/auth'
|
||||
import StateBlock from '@/components/StateBlock.vue'
|
||||
@@ -18,15 +18,22 @@ import { extractErrorMessage } from '@/utils/format'
|
||||
const auth = useAuth()
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const sendingCode = ref(false)
|
||||
const verifyingEmail = ref(false)
|
||||
const error = ref('')
|
||||
const message = ref('')
|
||||
const emailMessage = ref('')
|
||||
const token = ref<TokenStatus | null>(null)
|
||||
const form = reactive({
|
||||
alias: '',
|
||||
email: '',
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
})
|
||||
const emailForm = reactive({
|
||||
email: '',
|
||||
code: '',
|
||||
})
|
||||
const emailVerified = computed(() => Boolean(auth.state.user?.email_verified))
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -39,7 +46,7 @@ async function load() {
|
||||
auth.state.user = user
|
||||
token.value = tokenStatus
|
||||
form.alias = user.alias
|
||||
form.email = user.email ?? ''
|
||||
emailForm.email = user.email ?? ''
|
||||
} catch (err) {
|
||||
error.value = extractErrorMessage(err)
|
||||
} finally {
|
||||
@@ -54,7 +61,6 @@ async function save() {
|
||||
try {
|
||||
const user = await userApi.updateProfile({
|
||||
alias: form.alias,
|
||||
email: form.email || undefined,
|
||||
current_password: form.current_password || undefined,
|
||||
new_password: form.new_password || undefined,
|
||||
})
|
||||
@@ -69,6 +75,40 @@ async function save() {
|
||||
}
|
||||
}
|
||||
|
||||
async function requestEmailCode() {
|
||||
sendingCode.value = true
|
||||
error.value = ''
|
||||
emailMessage.value = ''
|
||||
try {
|
||||
const user = await userApi.setEmail(emailForm.email)
|
||||
auth.state.user = user
|
||||
emailForm.email = user.email ?? emailForm.email
|
||||
emailForm.code = ''
|
||||
emailMessage.value = '验证码已发送,请检查邮箱'
|
||||
} catch (err) {
|
||||
error.value = extractErrorMessage(err)
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyEmail() {
|
||||
verifyingEmail.value = true
|
||||
error.value = ''
|
||||
emailMessage.value = ''
|
||||
try {
|
||||
const user = await userApi.verifyEmail(emailForm.code)
|
||||
auth.state.user = user
|
||||
emailForm.email = user.email ?? emailForm.email
|
||||
emailForm.code = ''
|
||||
emailMessage.value = '邮箱已验证'
|
||||
} catch (err) {
|
||||
error.value = extractErrorMessage(err)
|
||||
} finally {
|
||||
verifyingEmail.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -92,10 +132,6 @@ onMounted(load)
|
||||
<span :class="labelClass">别名</span>
|
||||
<input v-model="form.alias" :class="inputClass" required />
|
||||
</label>
|
||||
<label class="grid gap-2">
|
||||
<span :class="labelClass">邮箱</span>
|
||||
<input v-model="form.email" :class="inputClass" type="email" placeholder="用于打卡通知" />
|
||||
</label>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="grid gap-2">
|
||||
<span :class="labelClass">当前密码</span>
|
||||
@@ -129,6 +165,61 @@ onMounted(load)
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section :class="[cardClass, 'overflow-hidden lg:col-span-2']">
|
||||
<div :class="sectionHeaderClass">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="font-semibold">邮箱验证</h2>
|
||||
<span :class="toneClass(emailVerified ? 'success' : 'warning')">
|
||||
{{ emailVerified ? '已验证' : '待验证' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 p-4">
|
||||
<label class="grid gap-2">
|
||||
<span :class="labelClass">邮箱</span>
|
||||
<input
|
||||
v-model.trim="emailForm.email"
|
||||
:class="inputClass"
|
||||
type="email"
|
||||
placeholder="用于审批与打卡通知"
|
||||
/>
|
||||
</label>
|
||||
<div class="grid gap-3 md:grid-cols-[minmax(0,1fr)_auto]">
|
||||
<label class="grid gap-2">
|
||||
<span :class="labelClass">验证码</span>
|
||||
<input
|
||||
v-model.trim="emailForm.code"
|
||||
:class="inputClass"
|
||||
inputmode="numeric"
|
||||
placeholder="请输入邮箱验证码"
|
||||
/>
|
||||
</label>
|
||||
<div class="flex items-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
:disabled="sendingCode || !emailForm.email"
|
||||
@click="requestEmailCode"
|
||||
>
|
||||
<Send class="size-4" :class="{ 'animate-spin': sendingCode }" />
|
||||
发送验证码
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
:disabled="verifyingEmail || !emailForm.code"
|
||||
@click="verifyEmail"
|
||||
>
|
||||
<MailCheck class="size-4" :class="{ 'animate-spin': verifyingEmail }" />
|
||||
验证
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="emailMessage" :class="alertClass.success">
|
||||
{{ emailMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside :class="[cardClass, 'h-fit overflow-hidden']">
|
||||
<div
|
||||
class="grid gap-2 border-b px-4 py-3"
|
||||
|
||||
@@ -29,7 +29,7 @@ const form = reactive({
|
||||
notify_token_expiring: true,
|
||||
notify_check_in_success: true,
|
||||
require_admin_approval_for_registration: true,
|
||||
warn_unverified_email_before_approval: true,
|
||||
require_verified_email_for_approval: true,
|
||||
})
|
||||
|
||||
const passwordState = computed(() => {
|
||||
@@ -50,7 +50,7 @@ function hydrate(next: EmailNotificationSettings) {
|
||||
form.notify_token_expiring = next.notify_token_expiring
|
||||
form.notify_check_in_success = next.notify_check_in_success
|
||||
form.require_admin_approval_for_registration = next.require_admin_approval_for_registration
|
||||
form.warn_unverified_email_before_approval = next.warn_unverified_email_before_approval
|
||||
form.require_verified_email_for_approval = next.require_verified_email_for_approval
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -85,7 +85,7 @@ async function save() {
|
||||
notify_token_expiring: form.notify_token_expiring,
|
||||
notify_check_in_success: form.notify_check_in_success,
|
||||
require_admin_approval_for_registration: form.require_admin_approval_for_registration,
|
||||
warn_unverified_email_before_approval: form.warn_unverified_email_before_approval,
|
||||
require_verified_email_for_approval: form.require_verified_email_for_approval,
|
||||
smtp_sender_password: form.smtp_sender_password || undefined,
|
||||
clear_smtp_sender_password: form.clear_smtp_sender_password,
|
||||
})
|
||||
@@ -233,15 +233,15 @@ onMounted(load)
|
||||
|
||||
<label class="grid gap-2 rounded-lg border border-border bg-background p-3">
|
||||
<span class="flex items-center justify-between gap-3 text-sm font-medium">
|
||||
未验证邮箱审批警告
|
||||
审批前要求验证邮箱
|
||||
<input
|
||||
v-model="form.warn_unverified_email_before_approval"
|
||||
v-model="form.require_verified_email_for_approval"
|
||||
type="checkbox"
|
||||
class="size-4 accent-primary"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>开启后,管理员审批邮箱未验证用户时需要确认覆盖。</span
|
||||
>开启后,管理员审批未验证邮箱用户时每次都需要确认覆盖。</span
|
||||
>
|
||||
</label>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user