feat: 安全增强 + 删除密码重置申请功能 + 登录提醒开关

安全增强:
- 新增 SSRF、XXE、模板注入、敏感路径探测检测规则
- security/constants.py: 添加新的威胁类型和检测模式
- security/threat_detector.py: 实现新检测逻辑

删除密码重置申请功能:
- 移除 /api/password_resets 相关API
- 删除 password_reset_requests 数据库表
- 前端移除密码重置申请页面和菜单
- 用户只能通过邮��找回密码,未绑定邮箱需联系管理员

登录提醒全局开关:
- email_service.py: 添加 login_alert_enabled 字段
- routes/api_auth.py: 检查开关状态再发送登录提醒
- EmailPage.vue: 添加新设备登录提醒开关

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-27 12:08:36 +08:00
parent 4ba933b001
commit 89f3fd9759
65 changed files with 555 additions and 784 deletions

View File

@@ -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

View File

@@ -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 = ''
}
}
@@ -136,80 +128,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 +286,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>