feat: 添加安全模块 + Dockerfile添加curl支持健康检查
主要更新: - 新增 security/ 安全模块 (风险评估、威胁检测、蜜罐等) - Dockerfile 添加 curl 以支持 Docker 健康检查 - 前端页面更新 (管理后台、用户端) - 数据库迁移和 schema 更新 - 新增 kdocs 上传服务 - 添加安全相关测试用例 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -30,11 +30,6 @@ export async function forgotPassword(payload) {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function requestPasswordReset(payload) {
|
||||
const { data } = await publicApi.post('/reset_password_request', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function confirmPasswordReset(payload) {
|
||||
const { data } = await publicApi.post('/reset-password-confirm', payload)
|
||||
return data
|
||||
|
||||
@@ -30,3 +30,12 @@ export async function changePassword(payload) {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchKdocsSettings() {
|
||||
const { data } = await publicApi.get('/user/kdocs')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function updateKdocsSettings(payload) {
|
||||
const { data } = await publicApi.post('/user/kdocs', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -11,10 +11,13 @@ import {
|
||||
changePassword,
|
||||
fetchEmailNotify,
|
||||
fetchUserEmail,
|
||||
fetchKdocsSettings,
|
||||
unbindEmail,
|
||||
updateKdocsSettings,
|
||||
updateEmailNotify,
|
||||
} from '../api/settings'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { validateStrongPassword } from '../utils/password'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -28,6 +31,56 @@ const announcementOpen = ref(false)
|
||||
const announcement = ref(null)
|
||||
const announcementLoading = ref(false)
|
||||
|
||||
const announcementPageToken = (() => {
|
||||
try {
|
||||
const timeOrigin = window.performance?.timeOrigin
|
||||
if (typeof timeOrigin === 'number' && Number.isFinite(timeOrigin)) return String(timeOrigin)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return String(Date.now())
|
||||
})()
|
||||
|
||||
function announcementOnceKey(announcementId) {
|
||||
return `announcement_closed_once_${announcementId}`
|
||||
}
|
||||
|
||||
function announcementPermanentKey(announcementId) {
|
||||
return `announcement_closed_${announcementId}`
|
||||
}
|
||||
|
||||
function wasAnnouncementClosedOnce(announcementId) {
|
||||
try {
|
||||
return window.sessionStorage.getItem(announcementOnceKey(announcementId)) === announcementPageToken
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function wasAnnouncementClosedPermanently(announcementId) {
|
||||
try {
|
||||
return window.localStorage.getItem(announcementPermanentKey(announcementId)) === '1'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function markAnnouncementClosedOnce(announcementId) {
|
||||
try {
|
||||
window.sessionStorage.setItem(announcementOnceKey(announcementId), announcementPageToken)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function markAnnouncementClosedPermanently(announcementId) {
|
||||
try {
|
||||
window.localStorage.setItem(announcementPermanentKey(announcementId), '1')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const feedbackOpen = ref(false)
|
||||
const feedbackTab = ref('new')
|
||||
const feedbackSubmitting = ref(false)
|
||||
@@ -60,6 +113,10 @@ const passwordForm = reactive({
|
||||
confirm_password: '',
|
||||
})
|
||||
|
||||
const kdocsLoading = ref(false)
|
||||
const kdocsSaving = ref(false)
|
||||
const kdocsUnitValue = ref('')
|
||||
|
||||
function syncIsMobile() {
|
||||
isMobile.value = Boolean(mediaQuery?.matches)
|
||||
if (!isMobile.value) drawerOpen.value = false
|
||||
@@ -180,7 +237,7 @@ async function openSettings() {
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
await Promise.all([loadEmailInfo(), loadEmailNotify()])
|
||||
await Promise.all([loadEmailInfo(), loadEmailNotify(), loadKdocsSettings()])
|
||||
}
|
||||
|
||||
async function loadEmailInfo() {
|
||||
@@ -211,6 +268,30 @@ async function loadEmailNotify() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKdocsSettings() {
|
||||
kdocsLoading.value = true
|
||||
try {
|
||||
const data = await fetchKdocsSettings()
|
||||
kdocsUnitValue.value = data?.kdocs_unit || ''
|
||||
} catch {
|
||||
kdocsUnitValue.value = ''
|
||||
} finally {
|
||||
kdocsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveKdocsSettings() {
|
||||
kdocsSaving.value = true
|
||||
try {
|
||||
await updateKdocsSettings({ kdocs_unit: kdocsUnitValue.value.trim() })
|
||||
ElMessage.success('已更新表格县区设置')
|
||||
} catch {
|
||||
// handled by interceptor
|
||||
} finally {
|
||||
kdocsSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onBindEmail() {
|
||||
const email = bindEmailValue.value.trim().toLowerCase()
|
||||
if (!email) {
|
||||
@@ -292,8 +373,9 @@ async function onChangePassword() {
|
||||
ElMessage.error('请填写完整信息')
|
||||
return
|
||||
}
|
||||
if (String(newPassword).length < 6) {
|
||||
ElMessage.error('新密码至少6位')
|
||||
const passwordCheck = validateStrongPassword(newPassword)
|
||||
if (!passwordCheck.ok) {
|
||||
ElMessage.error(passwordCheck.message)
|
||||
return
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
@@ -327,8 +409,8 @@ async function loadAnnouncement() {
|
||||
const ann = data?.announcement
|
||||
if (!ann?.id) return
|
||||
|
||||
const sessionKey = `announcement_closed_${ann.id}`
|
||||
if (window.sessionStorage.getItem(sessionKey) === '1') return
|
||||
if (wasAnnouncementClosedPermanently(ann.id)) return
|
||||
if (wasAnnouncementClosedOnce(ann.id)) return
|
||||
|
||||
announcement.value = ann
|
||||
announcementOpen.value = true
|
||||
@@ -341,7 +423,7 @@ async function loadAnnouncement() {
|
||||
|
||||
function closeAnnouncementOnce() {
|
||||
const ann = announcement.value
|
||||
if (ann?.id) window.sessionStorage.setItem(`announcement_closed_${ann.id}`, '1')
|
||||
if (ann?.id) markAnnouncementClosedOnce(ann.id)
|
||||
announcementOpen.value = false
|
||||
}
|
||||
|
||||
@@ -351,6 +433,7 @@ async function dismissAnnouncementPermanently() {
|
||||
announcementOpen.value = false
|
||||
return
|
||||
}
|
||||
markAnnouncementClosedPermanently(ann.id)
|
||||
try {
|
||||
const res = await dismissAnnouncement(ann.id)
|
||||
if (res?.success) ElMessage.success('已永久关闭')
|
||||
@@ -433,6 +516,9 @@ async function dismissAnnouncementPermanently() {
|
||||
<el-dialog v-model="announcementOpen" width="min(560px, 92vw)" :title="announcement?.title || '系统公告'">
|
||||
<div class="announcement-body" v-loading="announcementLoading">
|
||||
<div class="announcement-content">{{ announcement?.content || '' }}</div>
|
||||
<div v-if="announcement?.image_url" class="announcement-image">
|
||||
<img :src="announcement.image_url" alt="公告图片" loading="lazy" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="closeAnnouncementOnce">当次关闭</el-button>
|
||||
@@ -562,7 +648,7 @@ async function dismissAnnouncementPermanently() {
|
||||
<el-form-item label="当前密码">
|
||||
<el-input v-model="passwordForm.current_password" type="password" show-password autocomplete="current-password" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码(至少6位)">
|
||||
<el-form-item label="新密码(至少8位且包含字母和数字)">
|
||||
<el-input v-model="passwordForm.new_password" type="password" show-password autocomplete="new-password" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码">
|
||||
@@ -579,6 +665,24 @@ async function dismissAnnouncementPermanently() {
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="表格上传" name="kdocs">
|
||||
<div v-loading="kdocsLoading" class="settings-section">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="县区(可选)">
|
||||
<el-input v-model="kdocsUnitValue" placeholder="留空使用系统默认县区" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="kdocsSaving" @click="saveKdocsSettings">保存</el-button>
|
||||
</el-form>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="自动上传开关在“账号管理”页面设置(测试功能)。"
|
||||
show-icon
|
||||
class="settings-hint"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="VIP信息" name="vip">
|
||||
<div class="settings-section">
|
||||
<el-alert
|
||||
@@ -726,6 +830,20 @@ async function dismissAnnouncementPermanently() {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.announcement-image {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.announcement-image img {
|
||||
max-width: 100%;
|
||||
max-height: 320px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--app-border);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.feedback-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
updateAccount,
|
||||
updateAccountRemark,
|
||||
} from '../api/accounts'
|
||||
import { fetchKdocsSettings, updateKdocsSettings } from '../api/settings'
|
||||
import { fetchRunStats } from '../api/stats'
|
||||
import { useSocket } from '../composables/useSocket'
|
||||
import { useUserStore } from '../stores/user'
|
||||
@@ -57,6 +58,9 @@ watch(batchEnableScreenshot, (value) => {
|
||||
}
|
||||
})
|
||||
|
||||
const kdocsAutoUpload = ref(false)
|
||||
const kdocsSettingsLoading = ref(false)
|
||||
|
||||
const addOpen = ref(false)
|
||||
const editOpen = ref(false)
|
||||
const upgradeOpen = ref(false)
|
||||
@@ -189,6 +193,30 @@ async function refreshAccounts() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKdocsSettings() {
|
||||
kdocsSettingsLoading.value = true
|
||||
try {
|
||||
const data = await fetchKdocsSettings()
|
||||
kdocsAutoUpload.value = Number(data?.kdocs_auto_upload || 0) === 1
|
||||
} catch {
|
||||
kdocsAutoUpload.value = false
|
||||
} finally {
|
||||
kdocsSettingsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleKdocsAutoUpload(value) {
|
||||
kdocsSettingsLoading.value = true
|
||||
try {
|
||||
await updateKdocsSettings({ kdocs_auto_upload: value ? 1 : 0 })
|
||||
ElMessage.success(value ? '已开启自动上传(测试)' : '已关闭自动上传')
|
||||
} catch (e) {
|
||||
kdocsAutoUpload.value = !value
|
||||
} finally {
|
||||
kdocsSettingsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onStart(acc) {
|
||||
try {
|
||||
await startAccount(acc.id, { browse_type: browseTypeById[acc.id] || '应读', enable_screenshot: batchEnableScreenshot.value })
|
||||
@@ -524,6 +552,7 @@ onMounted(async () => {
|
||||
unbindSocket = bindSocket()
|
||||
|
||||
await refreshAccounts()
|
||||
await loadKdocsSettings()
|
||||
await refreshStats()
|
||||
syncStatsPolling()
|
||||
})
|
||||
@@ -612,6 +641,15 @@ onBeforeUnmount(() => {
|
||||
<el-option v-for="opt in browseTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-switch v-model="batchEnableScreenshot" inline-prompt active-text="截图" inactive-text="不截图" />
|
||||
<el-switch
|
||||
v-model="kdocsAutoUpload"
|
||||
:disabled="kdocsSettingsLoading"
|
||||
inline-prompt
|
||||
active-text="上传"
|
||||
inactive-text="不传"
|
||||
@change="onToggleKdocsAutoUpload"
|
||||
/>
|
||||
<span class="app-muted">表格(测试)</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
|
||||
@@ -8,10 +8,8 @@ import {
|
||||
forgotPassword,
|
||||
generateCaptcha,
|
||||
login,
|
||||
requestPasswordReset,
|
||||
resendVerifyEmail,
|
||||
} from '../api/auth'
|
||||
import { validateStrongPassword } from '../utils/password'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -32,20 +30,14 @@ const registerVerifyEnabled = ref(false)
|
||||
const forgotOpen = ref(false)
|
||||
const resendOpen = ref(false)
|
||||
|
||||
const emailResetForm = reactive({
|
||||
email: '',
|
||||
const forgotForm = reactive({
|
||||
username: '',
|
||||
captcha: '',
|
||||
})
|
||||
const emailResetCaptchaImage = ref('')
|
||||
const emailResetCaptchaSession = ref('')
|
||||
const emailResetLoading = ref(false)
|
||||
|
||||
const manualResetForm = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
new_password: '',
|
||||
})
|
||||
const manualResetLoading = ref(false)
|
||||
const forgotCaptchaImage = ref('')
|
||||
const forgotCaptchaSession = ref('')
|
||||
const forgotLoading = ref(false)
|
||||
const forgotHint = ref('')
|
||||
|
||||
const resendForm = reactive({
|
||||
email: '',
|
||||
@@ -72,12 +64,12 @@ async function refreshLoginCaptcha() {
|
||||
async function refreshEmailResetCaptcha() {
|
||||
try {
|
||||
const data = await generateCaptcha()
|
||||
emailResetCaptchaSession.value = data?.session_id || ''
|
||||
emailResetCaptchaImage.value = data?.captcha_image || ''
|
||||
emailResetForm.captcha = ''
|
||||
forgotCaptchaSession.value = data?.session_id || ''
|
||||
forgotCaptchaImage.value = data?.captcha_image || ''
|
||||
forgotForm.captcha = ''
|
||||
} catch {
|
||||
emailResetCaptchaSession.value = ''
|
||||
emailResetCaptchaImage.value = ''
|
||||
forgotCaptchaSession.value = ''
|
||||
forgotCaptchaImage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +105,14 @@ async function onSubmit() {
|
||||
need_captcha: needCaptcha.value,
|
||||
})
|
||||
ElMessage.success('登录成功,正在跳转...')
|
||||
const urlParams = new URLSearchParams(window.location.search || '')
|
||||
const next = String(urlParams.get('next') || '').trim()
|
||||
const safeNext = next && next.startsWith('/') && !next.startsWith('//') && !next.startsWith('/\\') ? next : ''
|
||||
setTimeout(() => {
|
||||
window.location.href = '/app'
|
||||
const target = safeNext || '/app'
|
||||
router.push(target).catch(() => {
|
||||
window.location.href = target
|
||||
})
|
||||
}, 300)
|
||||
} catch (e) {
|
||||
const status = e?.response?.status
|
||||
@@ -136,80 +134,54 @@ async function onSubmit() {
|
||||
|
||||
async function openForgot() {
|
||||
forgotOpen.value = true
|
||||
|
||||
forgotHint.value = ''
|
||||
forgotForm.username = ''
|
||||
forgotForm.captcha = ''
|
||||
if (emailEnabled.value) {
|
||||
emailResetForm.email = ''
|
||||
emailResetForm.captcha = ''
|
||||
await refreshEmailResetCaptcha()
|
||||
} else {
|
||||
manualResetForm.username = ''
|
||||
manualResetForm.email = ''
|
||||
manualResetForm.new_password = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForgot() {
|
||||
if (emailEnabled.value) {
|
||||
const email = emailResetForm.email.trim()
|
||||
if (!email) {
|
||||
ElMessage.error('请输入邮箱')
|
||||
return
|
||||
}
|
||||
if (!emailResetForm.captcha.trim()) {
|
||||
ElMessage.error('请输入验证码')
|
||||
return
|
||||
}
|
||||
forgotHint.value = ''
|
||||
|
||||
emailResetLoading.value = true
|
||||
try {
|
||||
const res = await forgotPassword({
|
||||
email,
|
||||
captcha_session: emailResetCaptchaSession.value,
|
||||
captcha: emailResetForm.captcha.trim(),
|
||||
})
|
||||
ElMessage.success(res?.message || '已发送重置邮件')
|
||||
setTimeout(() => {
|
||||
forgotOpen.value = false
|
||||
}, 800)
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
ElMessage.error(data?.error || '发送失败')
|
||||
await refreshEmailResetCaptcha()
|
||||
} finally {
|
||||
emailResetLoading.value = false
|
||||
}
|
||||
if (!emailEnabled.value) {
|
||||
ElMessage.warning('邮件功能未启用,请联系管理员重置密码。')
|
||||
return
|
||||
}
|
||||
|
||||
const username = manualResetForm.username.trim()
|
||||
const newPassword = manualResetForm.new_password
|
||||
if (!username || !newPassword) {
|
||||
ElMessage.error('用户名和新密码不能为空')
|
||||
const username = forgotForm.username.trim()
|
||||
if (!username) {
|
||||
ElMessage.error('请输入用户名')
|
||||
return
|
||||
}
|
||||
if (!forgotForm.captcha.trim()) {
|
||||
ElMessage.error('请输入验证码')
|
||||
return
|
||||
}
|
||||
|
||||
const check = validateStrongPassword(newPassword)
|
||||
if (!check.ok) {
|
||||
ElMessage.error(check.message)
|
||||
return
|
||||
}
|
||||
|
||||
manualResetLoading.value = true
|
||||
forgotLoading.value = true
|
||||
try {
|
||||
await requestPasswordReset({
|
||||
const res = await forgotPassword({
|
||||
username,
|
||||
email: manualResetForm.email.trim(),
|
||||
new_password: newPassword,
|
||||
captcha_session: forgotCaptchaSession.value,
|
||||
captcha: forgotForm.captcha.trim(),
|
||||
})
|
||||
ElMessage.success('申请已提交,请等待审核')
|
||||
ElMessage.success(res?.message || '已发送重置邮件')
|
||||
setTimeout(() => {
|
||||
forgotOpen.value = false
|
||||
}, 800)
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
ElMessage.error(data?.error || '提交失败')
|
||||
const message = data?.error || '发送失败'
|
||||
if (data?.code === 'email_not_bound') {
|
||||
forgotHint.value = message
|
||||
} else {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
await refreshEmailResetCaptcha()
|
||||
} finally {
|
||||
manualResetLoading.value = false
|
||||
forgotLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,51 +292,55 @@ onMounted(async () => {
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="forgotOpen" title="找回密码" width="min(560px, 92vw)">
|
||||
<template v-if="emailEnabled">
|
||||
<el-alert type="info" :closable="false" title="输入注册邮箱,我们将发送重置链接。" show-icon />
|
||||
<el-form label-position="top" class="dialog-form">
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="emailResetForm.email" placeholder="name@example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<div class="captcha-row">
|
||||
<el-input v-model="emailResetForm.captcha" placeholder="请输入验证码" />
|
||||
<img
|
||||
v-if="emailResetCaptchaImage"
|
||||
class="captcha-img"
|
||||
:src="emailResetCaptchaImage"
|
||||
alt="验证码"
|
||||
title="点击刷新"
|
||||
@click="refreshEmailResetCaptcha"
|
||||
/>
|
||||
<el-button @click="refreshEmailResetCaptcha">刷新</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-alert type="warning" :closable="false" title="邮件功能未启用:提交申请后等待管理员审核。" show-icon />
|
||||
<el-form label-position="top" class="dialog-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="manualResetForm.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱(可选)">
|
||||
<el-input v-model="manualResetForm.email" placeholder="可选填写邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码(至少8位且包含字母和数字)">
|
||||
<el-input v-model="manualResetForm.new_password" type="password" show-password placeholder="请输入新密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<el-alert
|
||||
v-if="!emailEnabled"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="邮件功能未启用"
|
||||
description="无法通过邮箱找回密码,请联系管理员重置密码。"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert
|
||||
v-else
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="通过邮箱找回密码"
|
||||
description="输入用户名并完成验证码,我们将向该账号绑定的邮箱发送重置链接。"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert
|
||||
v-if="forgotHint"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="无法通过邮箱找回密码"
|
||||
:description="forgotHint"
|
||||
show-icon
|
||||
class="alert"
|
||||
/>
|
||||
<el-form label-position="top" class="dialog-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="forgotForm.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<div class="captcha-row">
|
||||
<el-input v-model="forgotForm.captcha" placeholder="请输入验证码" />
|
||||
<img
|
||||
v-if="forgotCaptchaImage"
|
||||
class="captcha-img"
|
||||
:src="forgotCaptchaImage"
|
||||
alt="验证码"
|
||||
title="点击刷新"
|
||||
@click="refreshEmailResetCaptcha"
|
||||
/>
|
||||
<el-button @click="refreshEmailResetCaptcha">刷新</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="forgotOpen = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="emailEnabled ? emailResetLoading : manualResetLoading"
|
||||
@click="submitForgot"
|
||||
>
|
||||
{{ emailEnabled ? '发送重置邮件' : '提交申请' }}
|
||||
<el-button type="primary" :loading="forgotLoading" :disabled="!emailEnabled" @click="submitForgot">
|
||||
发送重置邮件
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { fetchEmailVerifyStatus, generateCaptcha, register } from '../api/auth'
|
||||
import { validateStrongPassword } from '../utils/password'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -68,8 +69,9 @@ async function onSubmit() {
|
||||
ElMessage.error(errorText.value)
|
||||
return
|
||||
}
|
||||
if (password.length < 6) {
|
||||
errorText.value = '密码至少6个字符'
|
||||
const passwordCheck = validateStrongPassword(password)
|
||||
if (!passwordCheck.ok) {
|
||||
errorText.value = passwordCheck.message || '密码格式不正确'
|
||||
ElMessage.error(errorText.value)
|
||||
return
|
||||
}
|
||||
@@ -166,10 +168,10 @@ onMounted(async () => {
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="至少6个字符"
|
||||
placeholder="至少8位且包含字母和数字"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div class="hint app-muted">至少6个字符</div>
|
||||
<div class="hint app-muted">至少8位且包含字母和数字</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码 *">
|
||||
<el-input
|
||||
|
||||
Reference in New Issue
Block a user