feat(auth): 新增密码登录注册与改密功能并加固安全
- 后端:新增密码登录/注册/重置/改密接口,users 表新增 password_hash 字段 - 安全加固:注册改为冲突即失败防止"注册即改密",登录用户不存在统一返回密码错误并计失败次数防枚举,改密需校验旧密码,清理登录失败计数中的死代码 - 前端:登录页重构为"登录/注册"两个 tab,登录内可切换密码/短信方式,默认密码登录 - 个人中心新增修改密码入口(PC 弹窗 + 移动端 popup),PC 个人资料页移除买家/卖家服务面板
This commit is contained in:
@@ -16,6 +16,7 @@ import { fetchPostRentalNotice, type PostRentalNotice } from '@/features/orders/
|
||||
import { formatDateMinute } from '@/shared/utils/time'
|
||||
import { uploadFile } from '@/shared/api/files'
|
||||
import { formatCent } from '@/shared/utils/money'
|
||||
import { setPassword } from '@/features/auth/api/auth'
|
||||
|
||||
const session = useSessionStore()
|
||||
const router = useRouter()
|
||||
@@ -36,6 +37,15 @@ const profileForm = reactive({
|
||||
const avatarFileInput = ref<HTMLInputElement | null>(null)
|
||||
const uploadingAvatar = ref(false)
|
||||
|
||||
// 修改密码
|
||||
const showPasswordEditor = ref(false)
|
||||
const savingPassword = ref(false)
|
||||
const passwordForm = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
function triggerAvatarUpload() {
|
||||
avatarFileInput.value?.click()
|
||||
}
|
||||
@@ -209,6 +219,39 @@ async function saveProfile() {
|
||||
}
|
||||
}
|
||||
|
||||
function openPasswordEditor() {
|
||||
passwordForm.oldPassword = ''
|
||||
passwordForm.newPassword = ''
|
||||
passwordForm.confirmPassword = ''
|
||||
showPasswordEditor.value = true
|
||||
}
|
||||
|
||||
async function savePassword() {
|
||||
const { newPassword, confirmPassword } = passwordForm
|
||||
if (!newPassword) {
|
||||
showToast({ message: '请输入新密码', icon: 'warning-o' })
|
||||
return
|
||||
}
|
||||
if (newPassword.length < 8 || newPassword.length > 20) {
|
||||
showToast({ message: '密码长度应为 8-20 位', icon: 'warning-o' })
|
||||
return
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast({ message: '两次输入的密码不一致', icon: 'warning-o' })
|
||||
return
|
||||
}
|
||||
savingPassword.value = true
|
||||
try {
|
||||
await setPassword(newPassword, passwordForm.oldPassword || undefined)
|
||||
showToast({ message: '密码已更新', icon: 'passed' })
|
||||
showPasswordEditor.value = false
|
||||
} catch (error) {
|
||||
showToast({ message: readError(error, '密码更新失败'), icon: 'cross' })
|
||||
} finally {
|
||||
savingPassword.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmLogout() {
|
||||
showSettings.value = false
|
||||
showDialog({
|
||||
@@ -387,6 +430,7 @@ function resolveAvatarURL(url: string | undefined | null) {
|
||||
</van-cell>
|
||||
<van-cell title="公告中心" icon="volume-o" is-link to="/m/announcements" />
|
||||
<van-cell title="资料修改" icon="edit" is-link @click="openProfileEditor" />
|
||||
<van-cell title="修改密码" icon="lock" is-link @click="openPasswordEditor" />
|
||||
<van-cell title="实名认证" icon="idcard" is-link to="/m/realname">
|
||||
<template #value>
|
||||
<span :class="isRealnameVerified ? 'verified-color' : 'unverified-color'">
|
||||
@@ -568,6 +612,65 @@ function resolveAvatarURL(url: string | undefined | null) {
|
||||
</section>
|
||||
</van-popup>
|
||||
|
||||
<!-- 修改密码 Popup -->
|
||||
<van-popup
|
||||
v-model:show="showPasswordEditor"
|
||||
position="bottom"
|
||||
round
|
||||
:style="{ maxWidth: '430px', margin: '0 auto', left: 0, right: 0 }"
|
||||
>
|
||||
<section class="profile-editor">
|
||||
<header class="profile-editor-header">
|
||||
<h2>修改密码</h2>
|
||||
<button type="button" @click="showPasswordEditor = false">
|
||||
<van-icon name="cross" :size="20" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<p class="password-hint">未设置过密码的老用户,"原密码"可留空直接设置新密码。</p>
|
||||
|
||||
<label class="editor-field">
|
||||
<span>原密码</span>
|
||||
<input
|
||||
v-model="passwordForm.oldPassword"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
placeholder="未设置过密码可留空"
|
||||
/>
|
||||
</label>
|
||||
<label class="editor-field">
|
||||
<span>新密码</span>
|
||||
<input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
placeholder="8-20 位"
|
||||
/>
|
||||
</label>
|
||||
<label class="editor-field">
|
||||
<span>确认新密码</span>
|
||||
<input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
placeholder="再次输入新密码"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
class="profile-save-btn"
|
||||
:loading="savingPassword"
|
||||
loading-text="保存中..."
|
||||
@click="savePassword"
|
||||
>
|
||||
保存密码
|
||||
</van-button>
|
||||
</section>
|
||||
</van-popup>
|
||||
|
||||
<MobileBottomNav />
|
||||
</main>
|
||||
</template>
|
||||
@@ -1156,6 +1259,13 @@ function resolveAvatarURL(url: string | undefined | null) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.password-hint {
|
||||
margin: 8px 0 14px;
|
||||
color: #8b9cb5;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.editor-field span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
|
||||
Reference in New Issue
Block a user