fix: secure email change recovery flow
This commit is contained in:
@@ -25,7 +25,11 @@ onMounted(async () => {
|
||||
try {
|
||||
const resp = await verifyEmail(token.value)
|
||||
message.value = resp.message
|
||||
auth.markEmailVerified()
|
||||
if (resp.session_invalidated) {
|
||||
auth.logout()
|
||||
} else {
|
||||
auth.markEmailVerified()
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
error.value = `[${err.code}] ${err.message}`
|
||||
|
||||
@@ -9,7 +9,7 @@ const auth = useAuthStore()
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const profileForm = ref({ email: '', username: '' })
|
||||
const profileForm = ref({ email: '', username: '', currentPassword: '' })
|
||||
const profileBusy = ref(false)
|
||||
const profileMessage = ref<string | null>(null)
|
||||
const profileError = ref<string | null>(null)
|
||||
@@ -24,9 +24,13 @@ const verificationMessage = ref<string | null>(null)
|
||||
const verificationError = ref<string | null>(null)
|
||||
|
||||
const canResendVerification = computed(() => Boolean(auth.user && !auth.user.email_verified))
|
||||
const emailChangeRequested = computed(() => {
|
||||
const email = profileForm.value.email.trim().toLowerCase()
|
||||
return Boolean(email && auth.user && email !== auth.user.email)
|
||||
})
|
||||
|
||||
function syncProfile(user: UserProfile) {
|
||||
profileForm.value = { email: user.email ?? '', username: user.username ?? '' }
|
||||
profileForm.value = { email: user.email ?? '', username: user.username ?? '', currentPassword: '' }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -77,9 +81,16 @@ async function saveProfile() {
|
||||
try {
|
||||
const email = profileForm.value.email.trim().toLowerCase()
|
||||
const username = profileForm.value.username.trim()
|
||||
const payload: { email?: string; username?: string } = {}
|
||||
const payload: { email?: string; username?: string; current_password?: string } = {}
|
||||
|
||||
if (email && email !== auth.user.email) payload.email = email
|
||||
if (email && email !== auth.user.email) {
|
||||
if (!profileForm.value.currentPassword) {
|
||||
profileError.value = '修改邮箱需要输入当前密码'
|
||||
return
|
||||
}
|
||||
payload.email = email
|
||||
payload.current_password = profileForm.value.currentPassword
|
||||
}
|
||||
if (username && username !== auth.user.username) payload.username = username
|
||||
|
||||
if (!payload.email && !payload.username) {
|
||||
@@ -88,7 +99,11 @@ async function saveProfile() {
|
||||
}
|
||||
|
||||
const resp = await updateProfile(auth.token, payload)
|
||||
auth.updateUser(resp.user)
|
||||
if (resp.token) {
|
||||
auth.setAuth(resp.token, resp.user)
|
||||
} else {
|
||||
auth.updateUser(resp.user)
|
||||
}
|
||||
syncProfile(resp.user)
|
||||
profileMessage.value = resp.message
|
||||
} catch (err) {
|
||||
@@ -178,6 +193,24 @@ async function changePassword() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label v-if="emailChangeRequested" class="mt-3 block max-w-md space-y-1">
|
||||
<div class="text-xs font-medium text-slate-600">当前密码(修改邮箱必填)</div>
|
||||
<input
|
||||
v-model="profileForm.currentPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
|
||||
placeholder="用于确认是本人操作"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="auth.user?.pending_email"
|
||||
class="mt-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900"
|
||||
>
|
||||
待确认新邮箱:{{ auth.user.pending_email }}。确认前仍使用当前邮箱登录和找回密码。
|
||||
</div>
|
||||
|
||||
<div v-if="profileMessage" class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-900">
|
||||
{{ profileMessage }}
|
||||
</div>
|
||||
|
||||
@@ -29,8 +29,14 @@ export async function sendVerification(token: string): Promise<{ message: string
|
||||
return apiJson<{ message: string }>('/api/v1/auth/send-verification', undefined, token, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function verifyEmail(verificationToken: string): Promise<{ message: string }> {
|
||||
return apiJson<{ message: string }>('/api/v1/auth/verify-email', { token: verificationToken }, null)
|
||||
export async function verifyEmail(
|
||||
verificationToken: string,
|
||||
): Promise<{ message: string; session_invalidated: boolean }> {
|
||||
return apiJson<{ message: string; session_invalidated: boolean }>(
|
||||
'/api/v1/auth/verify-email',
|
||||
{ token: verificationToken },
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
export async function forgotPassword(email: string): Promise<{ message: string }> {
|
||||
@@ -49,9 +55,11 @@ export async function getProfile(token: string): Promise<UserProfile> {
|
||||
|
||||
export async function updateProfile(
|
||||
token: string,
|
||||
payload: { email?: string; username?: string },
|
||||
): Promise<{ user: UserProfile; message: string }> {
|
||||
return apiJson<{ user: UserProfile; message: string }>('/api/v1/user/profile', payload, token, { method: 'PUT' })
|
||||
payload: { email?: string; username?: string; current_password?: string },
|
||||
): Promise<{ user: UserProfile; message: string; token?: string }> {
|
||||
return apiJson<{ user: UserProfile; message: string; token?: string }>('/api/v1/user/profile', payload, token, {
|
||||
method: 'PUT',
|
||||
})
|
||||
}
|
||||
|
||||
export async function updatePassword(
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface User {
|
||||
username: string
|
||||
role: UserRole
|
||||
email_verified: boolean
|
||||
pending_email?: string | null
|
||||
}
|
||||
|
||||
interface StoredAuth {
|
||||
|
||||
Reference in New Issue
Block a user