feat(auth): 新增密码登录注册与改密功能并加固安全
- 后端:新增密码登录/注册/重置/改密接口,users 表新增 password_hash 字段 - 安全加固:注册改为冲突即失败防止"注册即改密",登录用户不存在统一返回密码错误并计失败次数防枚举,改密需校验旧密码,清理登录失败计数中的死代码 - 前端:登录页重构为"登录/注册"两个 tab,登录内可切换密码/短信方式,默认密码登录 - 个人中心新增修改密码入口(PC 弹窗 + 移动端 popup),PC 个人资料页移除买家/卖家服务面板
This commit is contained in:
@@ -3,19 +3,11 @@ import { readError } from '@/shared/utils/error'
|
||||
import {
|
||||
Camera,
|
||||
CircleCheckFilled,
|
||||
CirclePlus,
|
||||
Coin,
|
||||
EditPen,
|
||||
Finished,
|
||||
Goods,
|
||||
Lock,
|
||||
Postcard,
|
||||
RefreshRight,
|
||||
Shop,
|
||||
Tickets,
|
||||
User,
|
||||
Van,
|
||||
VideoPlay,
|
||||
Wallet,
|
||||
WarningFilled,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -25,6 +17,7 @@ import { useRouter } from 'vue-router'
|
||||
import { uploadFile } from '@/shared/api/files'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { realnameStatusLabel } from '@/shared/utils/statusLabels'
|
||||
import { setPassword } from '@/features/auth/api/auth'
|
||||
|
||||
const session = useSessionStore()
|
||||
const router = useRouter()
|
||||
@@ -35,37 +28,13 @@ const form = reactive({
|
||||
nickname: '',
|
||||
avatar_url: '',
|
||||
})
|
||||
const buyerServices = [
|
||||
{
|
||||
label: '待支付',
|
||||
icon: Coin,
|
||||
tone: 'warning',
|
||||
to: { path: '/orders', query: { tab: 'pending_payment' } },
|
||||
},
|
||||
{
|
||||
label: '待交接',
|
||||
icon: Van,
|
||||
tone: 'info',
|
||||
to: { path: '/orders', query: { tab: 'pending_handoff' } },
|
||||
},
|
||||
{
|
||||
label: '使用中',
|
||||
icon: VideoPlay,
|
||||
tone: 'primary',
|
||||
to: { path: '/orders', query: { tab: 'renting' } },
|
||||
},
|
||||
{
|
||||
label: '已完成',
|
||||
icon: Finished,
|
||||
tone: 'success',
|
||||
to: { path: '/orders', query: { tab: 'completed' } },
|
||||
},
|
||||
]
|
||||
const sellerServices = [
|
||||
{ label: '发布商品', icon: CirclePlus, tone: 'orange', to: '/seller/listings/create' },
|
||||
{ label: '我的商品', icon: Shop, tone: 'purple', to: '/seller/listings' },
|
||||
{ label: '提现/账单', icon: Wallet, tone: 'teal', to: '/wallet' },
|
||||
]
|
||||
const showPasswordDialog = ref(false)
|
||||
const savingPassword = ref(false)
|
||||
const passwordForm = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const displayName = computed(() => session.displayName)
|
||||
const maskedPhone = computed(() => {
|
||||
@@ -145,6 +114,39 @@ async function saveProfile() {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPasswordDialog() {
|
||||
passwordForm.oldPassword = ''
|
||||
passwordForm.newPassword = ''
|
||||
passwordForm.confirmPassword = ''
|
||||
showPasswordDialog.value = true
|
||||
}
|
||||
|
||||
async function savePassword() {
|
||||
const { newPassword, confirmPassword } = passwordForm
|
||||
if (!newPassword) {
|
||||
ElMessage.warning('请输入新密码')
|
||||
return
|
||||
}
|
||||
if (newPassword.length < 8 || newPassword.length > 20) {
|
||||
ElMessage.warning('密码长度应为 8-20 位')
|
||||
return
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
ElMessage.warning('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
savingPassword.value = true
|
||||
try {
|
||||
await setPassword(newPassword, passwordForm.oldPassword || undefined)
|
||||
ElMessage.success('密码已更新')
|
||||
showPasswordDialog.value = false
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '密码更新失败'))
|
||||
} finally {
|
||||
savingPassword.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -261,56 +263,55 @@ async function saveProfile() {
|
||||
<span>查看实名认证</span>
|
||||
<el-icon><CircleCheckFilled /></el-icon>
|
||||
</button>
|
||||
<button class="realname-shortcut" type="button" @click="openPasswordDialog">
|
||||
<span>修改密码</span>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</button>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="service-panels">
|
||||
<section class="service-panel">
|
||||
<div class="service-head">
|
||||
<h2>买家服务</h2>
|
||||
<RouterLink class="service-all" :to="{ path: '/orders', query: { tab: 'all' } }">
|
||||
<span>全部订单</span>
|
||||
<el-icon><Tickets /></el-icon>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="service-grid buyer-grid">
|
||||
<RouterLink
|
||||
v-for="item in buyerServices"
|
||||
:key="item.label"
|
||||
class="service-item"
|
||||
:to="item.to"
|
||||
>
|
||||
<span class="service-icon" :class="`is-${item.tone}`">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</span>
|
||||
<strong>{{ item.label }}</strong>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="service-panel">
|
||||
<div class="service-head">
|
||||
<h2>卖家服务</h2>
|
||||
<RouterLink class="service-all" to="/seller/listings">
|
||||
<span>管理发布</span>
|
||||
<el-icon><Goods /></el-icon>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="service-grid seller-grid">
|
||||
<RouterLink
|
||||
v-for="item in sellerServices"
|
||||
:key="item.label"
|
||||
class="service-item"
|
||||
:to="item.to"
|
||||
>
|
||||
<span class="service-icon" :class="`is-${item.tone}`">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</span>
|
||||
<strong>{{ item.label }}</strong>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showPasswordDialog"
|
||||
title="修改密码"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p class="password-hint">未设置过密码的老用户,"原密码"可留空直接设置新密码。</p>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="原密码">
|
||||
<el-input
|
||||
v-model="passwordForm.oldPassword"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
placeholder="未设置过密码可留空"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
placeholder="8-20 位"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
placeholder="再次输入新密码"
|
||||
show-password
|
||||
@keyup.enter="savePassword"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showPasswordDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingPassword" @click="savePassword">保存密码</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -623,145 +624,16 @@ async function saveProfile() {
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.service-panels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 22px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.service-panel {
|
||||
padding: 24px;
|
||||
border: 1px solid #eef1f5;
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 10px 28px rgba(23, 35, 61, 0.06);
|
||||
}
|
||||
|
||||
.service-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.service-head h2 {
|
||||
margin: 0;
|
||||
color: #17233d;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.service-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.service-all:hover {
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.service-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.buyer-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.seller-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 118px;
|
||||
place-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 10px;
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 14px;
|
||||
background: #fbfdff;
|
||||
color: #1f2937;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
border-color: rgba(255, 106, 0, 0.35);
|
||||
box-shadow: 0 12px 24px rgba(23, 35, 61, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.service-item strong {
|
||||
color: #334155;
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
display: grid;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
place-items: center;
|
||||
border-radius: 14px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.service-icon.is-warning {
|
||||
background: #fff7ed;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.service-icon.is-info {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.service-icon.is-primary {
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.service-icon.is-success {
|
||||
background: #ecfdf3;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.service-icon.is-orange {
|
||||
background: #fff1e8;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.service-icon.is-purple {
|
||||
background: #f3e8ff;
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.service-icon.is-teal {
|
||||
background: #e8f7f5;
|
||||
color: #0f766e;
|
||||
.password-hint {
|
||||
margin: 0 0 12px;
|
||||
color: #8b9cb5;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.profile-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.service-panels {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user