24 changed files with 3091 additions and 127 deletions
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
# GoFaster Enhanced Hot Reload Script |
||||
# 增强版热重载脚本,确保前端代码变化时能够实时刷新 |
||||
|
||||
Write-Host "Starting GoFaster Frontend with Enhanced Hot Reload..." -ForegroundColor Green |
||||
Write-Host "" |
||||
Write-Host "Enhanced hot reload enabled with the following features:" -ForegroundColor Yellow |
||||
Write-Host " ✓ Vue.js watch mode for automatic rebuilds" -ForegroundColor Cyan |
||||
Write-Host " ✓ Electron reload on file changes" -ForegroundColor Cyan |
||||
Write-Host " ✓ Concurrent build and electron processes" -ForegroundColor Cyan |
||||
Write-Host " ✓ File change detection and auto-reload" -ForegroundColor Cyan |
||||
Write-Host "" |
||||
Write-Host "Code changes will automatically:" -ForegroundColor White |
||||
Write-Host " 1. Trigger Vue.js rebuild" -ForegroundColor White |
||||
Write-Host " 2. Reload Electron window" -ForegroundColor White |
||||
Write-Host " 3. Apply changes immediately" -ForegroundColor White |
||||
Write-Host "" |
||||
Write-Host "Press Ctrl+C to stop" -ForegroundColor Yellow |
||||
Write-Host "" |
||||
|
||||
# 检查依赖 |
||||
Write-Host "Checking dependencies..." -ForegroundColor Yellow |
||||
if (-not (Test-Path "node_modules")) { |
||||
Write-Host "Dependencies not installed, installing..." -ForegroundColor Yellow |
||||
npm install |
||||
if ($LASTEXITCODE -ne 0) { |
||||
Write-Host "Dependency installation failed" -ForegroundColor Red |
||||
exit 1 |
||||
} |
||||
} |
||||
|
||||
# 检查必要的包 |
||||
$concurrentlyInstalled = npm list concurrently 2>$null |
||||
$waitOnInstalled = npm list wait-on 2>$null |
||||
|
||||
if (-not $concurrentlyInstalled) { |
||||
Write-Host "Installing concurrently..." -ForegroundColor Yellow |
||||
npm install --save-dev concurrently |
||||
} |
||||
|
||||
if (-not $waitOnInstalled) { |
||||
Write-Host "Installing wait-on..." -ForegroundColor Yellow |
||||
npm install --save-dev wait-on |
||||
} |
||||
|
||||
Write-Host "Dependencies check completed" -ForegroundColor Green |
||||
Write-Host "" |
||||
|
||||
# 启动增强版热重载 |
||||
Write-Host "Starting enhanced hot reload..." -ForegroundColor Green |
||||
Write-Host "This will run: npm run dev:watch" -ForegroundColor Cyan |
||||
Write-Host "" |
||||
|
||||
npm run dev:watch |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,822 @@
@@ -0,0 +1,822 @@
|
||||
<template> |
||||
<div v-if="visible" class="password-modal-overlay" @click="handleOverlayClick"> |
||||
<div class="password-modal" @click.stop> |
||||
<div class="modal-header"> |
||||
<h3>{{ title }}</h3> |
||||
<button class="close-btn" @click="handleClose">×</button> |
||||
</div> |
||||
|
||||
<div class="modal-body"> |
||||
<form @submit.prevent="handleSubmit"> |
||||
<!-- 当前密码 --> |
||||
<div class="form-group" v-if="!isForceChange"> |
||||
<label>当前密码 *</label> |
||||
<input |
||||
v-model="form.currentPassword" |
||||
type="password" |
||||
required |
||||
placeholder="请输入当前密码" |
||||
:class="{ 'error': errors.currentPassword }" |
||||
/> |
||||
<div v-if="errors.currentPassword" class="error-message"> |
||||
{{ errors.currentPassword }} |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 新密码 --> |
||||
<div class="form-group"> |
||||
<label>新密码 *</label> |
||||
<input |
||||
v-model="form.newPassword" |
||||
type="password" |
||||
required |
||||
placeholder="请输入新密码" |
||||
@input="validatePassword" |
||||
@keyup="updateRequirements(form.newPassword)" |
||||
:class="{ 'error': errors.newPassword }" |
||||
/> |
||||
<div v-if="errors.newPassword" class="error-message"> |
||||
{{ errors.newPassword }} |
||||
</div> |
||||
|
||||
<!-- 密码强度指示器 --> |
||||
<div class="password-strength" > |
||||
<div class="strength-bar"> |
||||
<div |
||||
class="strength-fill" |
||||
:class="strengthClass" |
||||
:style="{ width: strengthPercentage + '%' }" |
||||
></div> |
||||
</div> |
||||
<div class="strength-text"> |
||||
强度: {{ strengthText }} ({{ passwordLevel }}级) |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 密码要求提示 --> |
||||
<div class="password-requirements" > |
||||
<div class="requirement" :class="{ 'met': requirements.length }"> |
||||
✓ 密码长度{{ form.newPassword.length }}位 |
||||
</div> |
||||
<div class="requirement" :class="{ 'met': requirements.uppercase }"> |
||||
✓ 包含大写字母 |
||||
</div> |
||||
<div class="requirement" :class="{ 'met': requirements.lowercase }"> |
||||
✓ 包含小写字母 |
||||
</div> |
||||
<div class="requirement" :class="{ 'met': requirements.numbers }"> |
||||
✓ 包含数字 |
||||
</div> |
||||
<div class="requirement" :class="{ 'met': requirements.special }"> |
||||
✓ 包含特殊字符 |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 确认新密码 --> |
||||
<div class="form-group"> |
||||
<label>确认新密码 *</label> |
||||
<input |
||||
v-model="form.confirmPassword" |
||||
type="password" |
||||
required |
||||
placeholder="请再次输入新密码" |
||||
@input="validateConfirmPassword" |
||||
:class="{ 'error': errors.confirmPassword }" |
||||
/> |
||||
<div v-if="errors.confirmPassword" class="error-message"> |
||||
{{ errors.confirmPassword }} |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 操作按钮 --> |
||||
<div class="form-actions"> |
||||
<button |
||||
type="button" |
||||
class="btn btn-secondary" |
||||
@click="handleClose" |
||||
:disabled="loading" |
||||
> |
||||
取消 |
||||
</button> |
||||
<button |
||||
type="submit" |
||||
class="btn btn-primary" |
||||
:disabled="loading || !isFormValid" |
||||
> |
||||
<span v-if="loading" class="loading-spinner"></span> |
||||
{{ submitText }} |
||||
</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { ref, reactive, computed, watch, onMounted } from 'vue' |
||||
import { userService } from '@/services/userService' |
||||
|
||||
export default { |
||||
name: 'PasswordChangeModal', |
||||
props: { |
||||
visible: { |
||||
type: Boolean, |
||||
default: false |
||||
}, |
||||
isForceChange: { |
||||
type: Boolean, |
||||
default: false |
||||
} |
||||
}, |
||||
emits: ['close', 'success'], |
||||
setup(props, { emit }) { |
||||
const loading = ref(false) |
||||
const policy = ref({ |
||||
minLength: 6, |
||||
minCharTypes: 1, |
||||
preventReuse: 3, |
||||
level: 1, |
||||
minRequiredLevel: 1, |
||||
requireUppercase: false, |
||||
requireLowercase: false, |
||||
requireNumbers: false, |
||||
requireSpecial: false |
||||
}) |
||||
|
||||
const form = reactive({ |
||||
currentPassword: '', |
||||
newPassword: '', |
||||
confirmPassword: '' |
||||
}) |
||||
|
||||
const errors = reactive({ |
||||
currentPassword: '', |
||||
newPassword: '', |
||||
confirmPassword: '' |
||||
}) |
||||
|
||||
const requirements = reactive({ |
||||
length: false, |
||||
uppercase: false, |
||||
lowercase: false, |
||||
numbers: false, |
||||
special: false |
||||
}) |
||||
|
||||
const passwordStrength = ref(0) |
||||
const passwordLevel = ref(0) |
||||
|
||||
// 计算属性 |
||||
const title = computed(() => { |
||||
return props.isForceChange ? '强制修改密码' : '修改密码' |
||||
}) |
||||
|
||||
const submitText = computed(() => { |
||||
return props.isForceChange ? '确认修改' : '修改密码' |
||||
}) |
||||
|
||||
const strengthClass = computed(() => { |
||||
if (passwordStrength.value >= 4) return 'strong' |
||||
if (passwordStrength.value >= 3) return 'medium' |
||||
if (passwordStrength.value >= 2) return 'weak' |
||||
return 'very-weak' |
||||
}) |
||||
|
||||
const strengthText = computed(() => { |
||||
if (passwordStrength.value >= 4) return '强' |
||||
if (passwordStrength.value >= 3) return '中' |
||||
if (passwordStrength.value >= 2) return '弱' |
||||
return '很弱' |
||||
}) |
||||
|
||||
const strengthPercentage = computed(() => { |
||||
// 将0-5的等级转换为0-100的百分比显示 |
||||
return (passwordStrength.value / 5) * 100 |
||||
}) |
||||
|
||||
const isFormValid = computed(() => { |
||||
if (props.isForceChange) { |
||||
return form.newPassword && form.confirmPassword && |
||||
form.newPassword === form.confirmPassword && |
||||
!errors.newPassword && !errors.confirmPassword |
||||
} |
||||
return form.currentPassword && form.newPassword && form.confirmPassword && |
||||
form.newPassword === form.confirmPassword && |
||||
!errors.currentPassword && !errors.newPassword && !errors.confirmPassword |
||||
}) |
||||
|
||||
// 方法 |
||||
const loadPasswordPolicy = async () => { |
||||
try { |
||||
console.log('开始加载密码策略...') |
||||
const response = await userService.getPasswordPolicy() |
||||
console.log('密码策略API响应:', response) |
||||
console.log('响应类型:', typeof response) |
||||
console.log('response.data 类型:', typeof response.data) |
||||
console.log('response.data 内容:', response.data) |
||||
|
||||
// 修复:直接检查response.data的结构 |
||||
if (response.data && response.data.code === 200 && response.data.data) { |
||||
policy.value = response.data.data |
||||
console.log('密码策略加载成功:', policy.value) |
||||
} else if (response && response.code === 200 && response.data) { |
||||
// 备用检查:如果response.data不存在,直接检查response |
||||
policy.value = response.data |
||||
console.log('密码策略加载成功(备用路径):', policy.value) |
||||
} else { |
||||
console.warn('密码策略响应格式不正确:', response) |
||||
console.warn('尝试解析响应结构...') |
||||
if (response.data) { |
||||
console.warn('response.data.code:', response.data.code) |
||||
console.warn('response.data.data:', response.data.data) |
||||
} |
||||
if (response) { |
||||
console.warn('response.code:', response.code) |
||||
console.warn('response.data:', response.data) |
||||
} |
||||
// 使用默认策略作为后备 |
||||
policy.value = { |
||||
minLength: 6, |
||||
minCharTypes: 1, |
||||
preventReuse: 3, |
||||
level: 1, |
||||
minRequiredLevel: 1, |
||||
requireUppercase: false, |
||||
requireLowercase: false, |
||||
requireNumbers: false, |
||||
requireSpecial: false |
||||
} |
||||
} |
||||
} catch (error) { |
||||
console.error('加载密码策略失败:', error) |
||||
// 使用默认策略作为后备 |
||||
policy.value = { |
||||
minLength: 6, |
||||
minCharTypes: 1, |
||||
preventReuse: 3, |
||||
level: 1, |
||||
minRequiredLevel: 1, |
||||
requireUppercase: false, |
||||
requireLowercase: false, |
||||
requireNumbers: false, |
||||
requireSpecial: false |
||||
} |
||||
} |
||||
} |
||||
|
||||
const validatePassword = async () => { |
||||
errors.newPassword = '' |
||||
|
||||
if (!form.newPassword) { |
||||
// 如果密码为空,重置所有要求状态 |
||||
updateRequirements('') |
||||
return |
||||
} |
||||
|
||||
try { |
||||
console.log('正在验证密码:', form.newPassword) |
||||
const response = await userService.validatePassword(form.newPassword) |
||||
console.log('后端验证响应:', response) |
||||
|
||||
// 修复:检查多种可能的响应结构 |
||||
let result = null |
||||
if (response.data && response.data.code === 200 && response.data.data) { |
||||
result = response.data.data |
||||
} else if (response && response.code === 200 && response.data) { |
||||
result = response.data |
||||
} |
||||
|
||||
if (result) { |
||||
console.log('验证结果:', result) |
||||
|
||||
if (!result.is_valid) { |
||||
// 确保错误信息是数组格式 |
||||
if (Array.isArray(result.errors)) { |
||||
errors.newPassword = result.errors.join('; ') |
||||
} else if (typeof result.errors === 'string') { |
||||
errors.newPassword = result.errors |
||||
} else { |
||||
errors.newPassword = '密码验证失败' |
||||
} |
||||
} |
||||
|
||||
passwordStrength.value = result.strength || 0 |
||||
passwordLevel.value = result.level || 0 |
||||
|
||||
console.log('设置密码强度:', passwordStrength.value, '等级:', passwordLevel.value) |
||||
|
||||
// 更新要求满足状态 |
||||
updateRequirements(form.newPassword) |
||||
} else { |
||||
console.warn('密码验证响应格式不正确:', response) |
||||
// 即使验证失败,也要更新要求状态 |
||||
updateRequirements(form.newPassword) |
||||
} |
||||
} catch (error) { |
||||
console.error('密码验证失败:', error) |
||||
// 即使验证失败,也要更新要求状态 |
||||
updateRequirements(form.newPassword) |
||||
} |
||||
} |
||||
|
||||
const updateRequirements = (password) => { |
||||
console.log('updateRequirements 被调用,密码:', password) |
||||
|
||||
// 重置所有状态 |
||||
requirements.length = false |
||||
requirements.uppercase = false |
||||
requirements.lowercase = false |
||||
requirements.numbers = false |
||||
requirements.special = false |
||||
|
||||
// 如果密码为空,直接返回 |
||||
if (!password || password.length === 0) { |
||||
return |
||||
} |
||||
|
||||
let hasUppercase = false |
||||
let hasLowercase = false |
||||
let hasNumbers = false |
||||
let hasSpecial = false |
||||
|
||||
for (const char of password) { |
||||
// 使用更准确的字符类型检测 |
||||
const charCode = char.charCodeAt(0) |
||||
|
||||
if (charCode >= 65 && charCode <= 90) { // A-Z |
||||
hasUppercase = true |
||||
} else if (charCode >= 97 && charCode <= 122) { // a-z |
||||
hasLowercase = true |
||||
} else if (charCode >= 48 && charCode <= 57) { // 0-9 |
||||
hasNumbers = true |
||||
} else { |
||||
// 特殊字符:包括标点符号、符号、空格等 |
||||
hasSpecial = true |
||||
} |
||||
} |
||||
|
||||
console.log('字符类型检测结果:', { |
||||
hasUppercase: hasUppercase, |
||||
hasLowercase: hasLowercase, |
||||
hasNumbers: hasNumbers, |
||||
hasSpecial: hasSpecial, |
||||
length: password.length |
||||
}) |
||||
|
||||
// 更新要求状态 |
||||
requirements.length = password.length >= 6 // 要求至少6位 |
||||
requirements.uppercase = hasUppercase |
||||
requirements.lowercase = hasLowercase |
||||
requirements.numbers = hasNumbers |
||||
requirements.special = hasSpecial |
||||
|
||||
// 计算字符类型数量 |
||||
let charTypes = 0 |
||||
if (hasUppercase) charTypes++ |
||||
if (hasLowercase) charTypes++ |
||||
if (hasNumbers) charTypes++ |
||||
if (hasSpecial) charTypes++ |
||||
|
||||
// 计算密码强度 |
||||
const strength = calculatePasswordStrength(password, charTypes) |
||||
passwordStrength.value = strength |
||||
passwordLevel.value = strength |
||||
|
||||
console.log('更新后的要求状态:', { |
||||
length: requirements.length, |
||||
uppercase: requirements.uppercase, |
||||
lowercase: requirements.lowercase, |
||||
numbers: requirements.numbers, |
||||
special: requirements.special |
||||
}) |
||||
console.log('计算出的密码强度:', strength) |
||||
} |
||||
|
||||
// 前端密码强度计算方法 |
||||
const calculatePasswordStrength = (password, charTypes) => { |
||||
// 从最高等级开始判断,一旦满足条件就返回对应等级 |
||||
|
||||
// 5级:长度>=8,字符类型>=4 |
||||
if (password.length >= 8 && charTypes >= 4) { |
||||
return 5 |
||||
} |
||||
|
||||
// 4级:长度>=8,字符类型>=3 |
||||
if (password.length >= 8 && charTypes >= 3) { |
||||
return 4 |
||||
} |
||||
|
||||
// 3级:长度>=6,字符类型>=3 |
||||
if (password.length >= 6 && charTypes >= 3) { |
||||
return 3 |
||||
} |
||||
|
||||
// 2级:长度>=6,字符类型>=2 |
||||
if (password.length >= 6 && charTypes >= 2) { |
||||
return 2 |
||||
} |
||||
|
||||
// 1级:长度>=6,字符类型>=1 |
||||
if (password.length >= 6 && charTypes >= 1) { |
||||
return 1 |
||||
} |
||||
|
||||
// 0级:长度>=1,字符类型>=1(任何密码都至少有一种字符类型) |
||||
if (password.length >= 1) { |
||||
return 0 |
||||
} |
||||
|
||||
// 如果连0级都不满足,返回0 |
||||
return 0 |
||||
} |
||||
|
||||
const validateConfirmPassword = () => { |
||||
errors.confirmPassword = '' |
||||
|
||||
if (form.newPassword !== form.confirmPassword) { |
||||
errors.confirmPassword = '两次输入的密码不一致' |
||||
} |
||||
} |
||||
|
||||
const handleSubmit = async () => { |
||||
if (!isFormValid.value) return |
||||
|
||||
loading.value = true |
||||
|
||||
try { |
||||
const requestData = { |
||||
current_password: form.currentPassword, |
||||
new_password: form.newPassword, |
||||
confirm_password: form.confirmPassword |
||||
} |
||||
|
||||
await userService.changePassword(requestData) |
||||
|
||||
// 成功 |
||||
emit('success') |
||||
handleClose() |
||||
|
||||
// 显示成功消息 |
||||
if (window.showToast) { |
||||
window.showToast({ |
||||
type: 'success', |
||||
title: '密码修改成功', |
||||
content: '您的密码已成功修改' |
||||
}) |
||||
} |
||||
} catch (error) { |
||||
console.error('修改密码失败:', error) |
||||
|
||||
// 显示错误消息 |
||||
if (error.response?.data?.message) { |
||||
if (error.response.data.message.includes('当前密码不正确')) { |
||||
errors.currentPassword = '当前密码不正确' |
||||
} else if (error.response.data.message.includes('新密码不符合要求')) { |
||||
errors.newPassword = error.response.data.message |
||||
} else { |
||||
errors.newPassword = error.response.data.message |
||||
} |
||||
} else { |
||||
errors.newPassword = '修改密码失败,请重试' |
||||
} |
||||
} finally { |
||||
loading.value = false |
||||
} |
||||
} |
||||
|
||||
const handleClose = () => { |
||||
if (loading.value) return |
||||
|
||||
// 重置表单 |
||||
form.currentPassword = '' |
||||
form.newPassword = '' |
||||
form.confirmPassword = '' |
||||
|
||||
// 清除错误 |
||||
Object.keys(errors).forEach(key => { |
||||
errors[key] = '' |
||||
}) |
||||
|
||||
// 重置要求状态 |
||||
Object.keys(requirements).forEach(key => { |
||||
requirements[key] = false |
||||
}) |
||||
|
||||
passwordStrength.value = 0 |
||||
passwordLevel.value = 0 |
||||
|
||||
emit('close') |
||||
} |
||||
|
||||
const handleOverlayClick = () => { |
||||
if (!props.isForceChange) { |
||||
handleClose() |
||||
} |
||||
} |
||||
|
||||
// 监听器 |
||||
watch(() => props.visible, (newVal) => { |
||||
if (newVal) { |
||||
console.log('模态框显示,开始加载密码策略') |
||||
loadPasswordPolicy() |
||||
} |
||||
}) |
||||
|
||||
// 监听密码变化,实时更新要求状态 |
||||
watch(() => form.newPassword, (newPassword) => { |
||||
if (newPassword) { |
||||
console.log('密码变化,更新要求状态:', newPassword) |
||||
updateRequirements(newPassword) |
||||
} else { |
||||
// 密码为空时重置所有状态 |
||||
Object.keys(requirements).forEach(key => { |
||||
requirements[key] = false |
||||
}) |
||||
// 重置密码强度 |
||||
passwordStrength.value = 0 |
||||
passwordLevel.value = 0 |
||||
} |
||||
}) |
||||
|
||||
// 生命周期 |
||||
onMounted(() => { |
||||
console.log('组件挂载,检查是否需要加载密码策略') |
||||
if (props.visible) { |
||||
loadPasswordPolicy() |
||||
} |
||||
// 如果有初始密码内容,计算强度 |
||||
if (form.newPassword) { |
||||
updateRequirements(form.newPassword) |
||||
} |
||||
}) |
||||
|
||||
return { |
||||
loading, |
||||
policy, |
||||
form, |
||||
errors, |
||||
requirements, |
||||
passwordStrength, |
||||
passwordLevel, |
||||
title, |
||||
submitText, |
||||
strengthClass, |
||||
strengthText, |
||||
strengthPercentage, |
||||
isFormValid, |
||||
validatePassword, |
||||
validateConfirmPassword, |
||||
updateRequirements, |
||||
handleSubmit, |
||||
handleClose, |
||||
handleOverlayClick |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.password-modal-overlay { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
background: rgba(0, 0, 0, 0.5); |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
z-index: 1000; |
||||
} |
||||
|
||||
.password-modal { |
||||
background: var(--card-bg); |
||||
border-radius: 12px; |
||||
width: 90%; |
||||
max-width: 500px; |
||||
max-height: 90vh; |
||||
overflow-y: auto; |
||||
box-shadow: 0 8px 32px var(--shadow-color); |
||||
} |
||||
|
||||
.modal-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
padding: 20px 24px; |
||||
border-bottom: 1px solid var(--border-color); |
||||
} |
||||
|
||||
.modal-header h3 { |
||||
margin: 0; |
||||
color: var(--text-primary); |
||||
font-size: 18px; |
||||
} |
||||
|
||||
.close-btn { |
||||
background: none; |
||||
border: none; |
||||
font-size: 24px; |
||||
cursor: pointer; |
||||
color: var(--text-muted); |
||||
padding: 4px; |
||||
border-radius: 4px; |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.close-btn:hover { |
||||
background: var(--bg-secondary); |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.modal-body { |
||||
padding: 24px; |
||||
} |
||||
|
||||
.form-group { |
||||
margin-bottom: 20px; |
||||
} |
||||
|
||||
.form-group label { |
||||
display: block; |
||||
margin-bottom: 8px; |
||||
font-weight: 500; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.form-group input { |
||||
width: 100%; |
||||
padding: 12px; |
||||
border: 1px solid var(--border-color); |
||||
border-radius: 6px; |
||||
font-size: 14px; |
||||
background: var(--input-bg); |
||||
color: var(--text-primary); |
||||
transition: all 0.2s; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
.form-group input:focus { |
||||
outline: none; |
||||
border-color: var(--accent-color); |
||||
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2); |
||||
} |
||||
|
||||
.form-group input.error { |
||||
border-color: #f44336; |
||||
} |
||||
|
||||
.error-message { |
||||
color: #f44336; |
||||
font-size: 12px; |
||||
margin-top: 4px; |
||||
} |
||||
|
||||
/* 密码强度指示器 */ |
||||
.password-strength { |
||||
margin-top: 12px; |
||||
} |
||||
|
||||
.strength-bar { |
||||
width: 100%; |
||||
height: 6px; |
||||
background: #e0e0e0; |
||||
border-radius: 3px; |
||||
overflow: hidden; |
||||
margin-bottom: 8px; |
||||
} |
||||
|
||||
.strength-fill { |
||||
height: 100%; |
||||
transition: width 0.3s ease; |
||||
} |
||||
|
||||
.strength-fill.very-weak { |
||||
background: #f44336; |
||||
} |
||||
|
||||
.strength-fill.weak { |
||||
background: #ff9800; |
||||
} |
||||
|
||||
.strength-fill.medium { |
||||
background: #ffc107; |
||||
} |
||||
|
||||
.strength-fill.strong { |
||||
background: #4caf50; |
||||
} |
||||
|
||||
.strength-text { |
||||
font-size: 12px; |
||||
color: var(--text-secondary); |
||||
text-align: center; |
||||
} |
||||
|
||||
/* 密码要求提示 */ |
||||
.password-requirements { |
||||
margin-top: 12px; |
||||
padding: 12px; |
||||
background: var(--bg-secondary); |
||||
border-radius: 6px; |
||||
font-size: 12px; |
||||
} |
||||
|
||||
.requirement { |
||||
color: var(--text-muted); |
||||
margin-bottom: 4px; |
||||
transition: color 0.2s; |
||||
} |
||||
|
||||
.requirement.met { |
||||
color: #4caf50; |
||||
} |
||||
|
||||
.requirement:last-child { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
/* 操作按钮 */ |
||||
.form-actions { |
||||
display: flex; |
||||
gap: 12px; |
||||
justify-content: flex-end; |
||||
margin-top: 30px; |
||||
} |
||||
|
||||
.btn { |
||||
padding: 12px 24px; |
||||
border: none; |
||||
border-radius: 6px; |
||||
cursor: pointer; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
transition: all 0.2s; |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.btn:disabled { |
||||
opacity: 0.6; |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
.btn-primary { |
||||
background: var(--accent-color); |
||||
color: white; |
||||
} |
||||
|
||||
.btn-primary:hover:not(:disabled) { |
||||
background: var(--accent-hover); |
||||
} |
||||
|
||||
.btn-secondary { |
||||
background: var(--bg-secondary); |
||||
color: var(--text-primary); |
||||
border: 1px solid var(--border-color); |
||||
} |
||||
|
||||
.btn-secondary:hover:not(:disabled) { |
||||
background: var(--bg-tertiary); |
||||
} |
||||
|
||||
/* 加载动画 */ |
||||
.loading-spinner { |
||||
width: 16px; |
||||
height: 16px; |
||||
border: 2px solid transparent; |
||||
border-top: 2px solid currentColor; |
||||
border-radius: 50%; |
||||
animation: spin 1s linear infinite; |
||||
} |
||||
|
||||
@keyframes spin { |
||||
0% { transform: rotate(0deg); } |
||||
100% { transform: rotate(360deg); } |
||||
} |
||||
|
||||
/* 响应式设计 */ |
||||
@media (max-width: 768px) { |
||||
.password-modal { |
||||
width: 95%; |
||||
margin: 20px; |
||||
} |
||||
|
||||
.modal-header, |
||||
.modal-body { |
||||
padding: 16px; |
||||
} |
||||
|
||||
.form-actions { |
||||
flex-direction: column; |
||||
} |
||||
|
||||
.btn { |
||||
width: 100%; |
||||
justify-content: center; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
|
@ -0,0 +1,166 @@
@@ -0,0 +1,166 @@
|
||||
package controller |
||||
|
||||
import ( |
||||
"net/http" |
||||
"strconv" |
||||
|
||||
"gofaster/internal/auth/model" |
||||
"gofaster/internal/auth/service" |
||||
"gofaster/internal/shared/response" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
) |
||||
|
||||
type PasswordController struct { |
||||
passwordService *service.PasswordService |
||||
userService *service.UserService |
||||
} |
||||
|
||||
func NewPasswordController( |
||||
passwordService *service.PasswordService, |
||||
userService *service.UserService, |
||||
) *PasswordController { |
||||
return &PasswordController{ |
||||
passwordService: passwordService, |
||||
userService: userService, |
||||
} |
||||
} |
||||
|
||||
// ChangePassword 修改密码
|
||||
func (c *PasswordController) ChangePassword(ctx *gin.Context) { |
||||
var req model.PasswordChangeRequest |
||||
if err := ctx.ShouldBindJSON(&req); err != nil { |
||||
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) |
||||
return |
||||
} |
||||
|
||||
// 从JWT获取用户ID
|
||||
userID, exists := ctx.Get("user_id") |
||||
if !exists { |
||||
response.Error(ctx, http.StatusUnauthorized, "未授权", "用户ID不存在") |
||||
return |
||||
} |
||||
|
||||
req.UserID = userID.(uint) |
||||
|
||||
// 验证新密码确认
|
||||
if req.NewPassword != req.ConfirmPassword { |
||||
response.Error(ctx, http.StatusBadRequest, "密码确认失败", "新密码与确认密码不匹配") |
||||
return |
||||
} |
||||
|
||||
// 修改密码
|
||||
err := c.passwordService.ChangePassword(req.UserID, req.CurrentPassword, req.NewPassword) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusBadRequest, "修改密码失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "密码修改成功", nil) |
||||
} |
||||
|
||||
// ResetPassword 重置用户密码(管理员功能)
|
||||
func (c *PasswordController) ResetPassword(ctx *gin.Context) { |
||||
userIDStr := ctx.Param("id") |
||||
userID, err := strconv.ParseUint(userIDStr, 10, 32) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusBadRequest, "用户ID格式错误", err.Error()) |
||||
return |
||||
} |
||||
|
||||
// 从JWT获取操作人ID
|
||||
operatorID, exists := ctx.Get("user_id") |
||||
if !exists { |
||||
response.Error(ctx, http.StatusUnauthorized, "未授权", "操作人ID不存在") |
||||
return |
||||
} |
||||
|
||||
// 重置密码
|
||||
tempPassword, err := c.passwordService.ResetPassword(uint(userID), operatorID.(uint)) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusInternalServerError, "重置密码失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "密码重置成功", gin.H{ |
||||
"temp_password": tempPassword, |
||||
"message": "用户首次登录后需要立即修改密码", |
||||
}) |
||||
} |
||||
|
||||
// GetPasswordPolicy 获取密码策略
|
||||
func (c *PasswordController) GetPasswordPolicy(ctx *gin.Context) { |
||||
policy, err := c.passwordService.GetPasswordPolicy() |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusInternalServerError, "获取密码策略失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "获取密码策略成功", policy) |
||||
} |
||||
|
||||
// ValidatePassword 验证密码强度
|
||||
func (c *PasswordController) ValidatePassword(ctx *gin.Context) { |
||||
var req struct { |
||||
Password string `json:"password" binding:"required"` |
||||
} |
||||
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil { |
||||
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) |
||||
return |
||||
} |
||||
|
||||
// 验证密码
|
||||
result, err := c.passwordService.ValidatePassword(req.Password) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusInternalServerError, "密码验证失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "密码验证完成", result) |
||||
} |
||||
|
||||
// CheckPasswordStatus 检查用户密码状态
|
||||
func (c *PasswordController) CheckPasswordStatus(ctx *gin.Context) { |
||||
userID, exists := ctx.Get("user_id") |
||||
if !exists { |
||||
response.Error(ctx, http.StatusUnauthorized, "未授权", "用户ID不存在") |
||||
return |
||||
} |
||||
|
||||
// 获取用户信息
|
||||
user, err := c.userService.GetByID(userID.(uint)) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusInternalServerError, "获取用户信息失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
// 检查密码过期
|
||||
isExpired, err := c.passwordService.CheckPasswordExpiration(user) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusInternalServerError, "检查密码过期失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
// 检查是否需要强制修改密码
|
||||
forceChange := c.passwordService.CheckForceChangePassword(user) |
||||
|
||||
response.Success(ctx, "获取密码状态成功", gin.H{ |
||||
"force_change_password": forceChange, |
||||
"password_expired": isExpired, |
||||
"password_changed_at": user.PasswordChangedAt, |
||||
}) |
||||
} |
||||
|
||||
// UpdatePasswordPolicy 更新密码策略(管理员功能)
|
||||
func (c *PasswordController) UpdatePasswordPolicy(ctx *gin.Context) { |
||||
var policy model.PasswordPolicy |
||||
if err := ctx.ShouldBindJSON(&policy); err != nil { |
||||
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) |
||||
return |
||||
} |
||||
|
||||
// 这里需要调用密码策略服务来更新策略
|
||||
// 暂时返回成功,具体实现需要完善
|
||||
response.Success(ctx, "密码策略更新成功", policy) |
||||
} |
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
package model |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
// PasswordPolicy 密码策略
|
||||
type PasswordPolicy struct { |
||||
ID uint `gorm:"primarykey" json:"id"` |
||||
Level int `gorm:"not null;default:1" json:"level"` // 密码策略等级 0-5
|
||||
MinRequiredLevel int `gorm:"not null;default:1" json:"min_required_level"` // 新增:系统要求的最低密码强度等级
|
||||
MinLength int `gorm:"not null;default:6" json:"min_length"` // 最小长度
|
||||
RequireUppercase bool `gorm:"not null;default:false" json:"require_uppercase"` // 要求大写字母
|
||||
RequireLowercase bool `gorm:"not null;default:false" json:"require_lowercase"` // 要求小写字母
|
||||
RequireNumbers bool `gorm:"not null;default:false" json:"require_numbers"` // 要求数字
|
||||
RequireSpecial bool `gorm:"not null;default:false" json:"require_special"` // 要求特殊字符
|
||||
MinCharTypes int `gorm:"not null;default:1" json:"min_char_types"` // 最少字符类型数
|
||||
ExpirationDays int `gorm:"not null;default:30" json:"expiration_days"` // 密码失效天数
|
||||
PreventReuse int `gorm:"not null;default:3" json:"prevent_reuse"` // 防止重复使用前N次密码
|
||||
IsActive bool `gorm:"not null;default:true" json:"is_active"` // 是否启用
|
||||
CreatedAt time.Time `json:"created_at"` |
||||
UpdatedAt time.Time `json:"updated_at"` |
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` |
||||
} |
||||
|
||||
// PasswordHistory 密码历史记录
|
||||
type PasswordHistory struct { |
||||
ID uint `gorm:"primarykey" json:"id"` |
||||
UserID uint `gorm:"not null;index" json:"user_id"` // 用户ID
|
||||
Password string `gorm:"not null" json:"password"` // 加密后的密码
|
||||
CreatedAt time.Time `json:"created_at"` |
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` |
||||
} |
||||
|
||||
// PasswordReset 密码重置记录
|
||||
type PasswordReset struct { |
||||
ID uint `gorm:"primarykey" json:"id"` |
||||
UserID uint `gorm:"not null;index" json:"user_id"` // 用户ID
|
||||
ResetBy uint `gorm:"not null" json:"reset_by"` // 重置操作人ID
|
||||
ResetAt time.Time `gorm:"not null" json:"reset_at"` // 重置时间
|
||||
IsUsed bool `gorm:"not null;default:false" json:"is_used"` // 是否已使用
|
||||
UsedAt *time.Time `json:"used_at"` // 使用时间
|
||||
CreatedAt time.Time `json:"created_at"` |
||||
UpdatedAt time.Time `json:"updated_at"` |
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` |
||||
} |
||||
|
||||
// PasswordChangeRequest 密码修改请求
|
||||
type PasswordChangeRequest struct { |
||||
UserID uint `json:"user_id"` |
||||
CurrentPassword string `json:"current_password"` |
||||
NewPassword string `json:"new_password"` |
||||
ConfirmPassword string `json:"confirm_password"` |
||||
} |
||||
|
||||
// PasswordValidationResult 密码验证结果
|
||||
type PasswordValidationResult struct { |
||||
IsValid bool `json:"is_valid"` |
||||
Errors []string `json:"errors"` |
||||
Strength int `json:"strength"` // 密码强度 0-100
|
||||
Level int `json:"level"` // 符合的密码等级
|
||||
} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
package repository |
||||
|
||||
import ( |
||||
"gofaster/internal/auth/model" |
||||
|
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
type PasswordHistoryRepository struct { |
||||
db *gorm.DB |
||||
} |
||||
|
||||
func NewPasswordHistoryRepository(db *gorm.DB) *PasswordHistoryRepository { |
||||
return &PasswordHistoryRepository{db: db} |
||||
} |
||||
|
||||
// Create 创建密码历史记录
|
||||
func (r *PasswordHistoryRepository) Create(history *model.PasswordHistory) error { |
||||
return r.db.Create(history).Error |
||||
} |
||||
|
||||
// GetRecentPasswords 获取用户最近的N次密码
|
||||
func (r *PasswordHistoryRepository) GetRecentPasswords(userID uint, limit int) ([]*model.PasswordHistory, error) { |
||||
var history []*model.PasswordHistory |
||||
err := r.db.Where("user_id = ?", userID). |
||||
Order("created_at DESC"). |
||||
Limit(limit). |
||||
Find(&history).Error |
||||
return history, err |
||||
} |
||||
|
||||
// DeleteOldPasswords 删除用户超过指定天数的旧密码记录
|
||||
func (r *PasswordHistoryRepository) DeleteOldPasswords(userID uint, days int) error { |
||||
return r.db.Where("user_id = ? AND created_at < DATE_SUB(NOW(), INTERVAL ? DAY)", userID, days). |
||||
Delete(&model.PasswordHistory{}).Error |
||||
} |
||||
|
||||
// GetByUserID 获取用户的所有密码历史
|
||||
func (r *PasswordHistoryRepository) GetByUserID(userID uint) ([]*model.PasswordHistory, error) { |
||||
var history []*model.PasswordHistory |
||||
err := r.db.Where("user_id = ?", userID). |
||||
Order("created_at DESC"). |
||||
Find(&history).Error |
||||
return history, err |
||||
} |
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
package repository |
||||
|
||||
import ( |
||||
"gofaster/internal/auth/model" |
||||
|
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
type PasswordPolicyRepository struct { |
||||
db *gorm.DB |
||||
} |
||||
|
||||
func NewPasswordPolicyRepository(db *gorm.DB) *PasswordPolicyRepository { |
||||
return &PasswordPolicyRepository{db: db} |
||||
} |
||||
|
||||
// GetActivePolicy 获取当前激活的密码策略
|
||||
func (r *PasswordPolicyRepository) GetActivePolicy() (*model.PasswordPolicy, error) { |
||||
var policy model.PasswordPolicy |
||||
err := r.db.Where("is_active = ?", true).First(&policy).Error |
||||
if err != nil { |
||||
// 如果没有找到策略,返回默认策略
|
||||
return r.getDefaultPolicy(), nil |
||||
} |
||||
return &policy, nil |
||||
} |
||||
|
||||
// Create 创建密码策略
|
||||
func (r *PasswordPolicyRepository) Create(policy *model.PasswordPolicy) error { |
||||
return r.db.Create(policy).Error |
||||
} |
||||
|
||||
// Update 更新密码策略
|
||||
func (r *PasswordPolicyRepository) Update(policy *model.PasswordPolicy) error { |
||||
return r.db.Save(policy).Error |
||||
} |
||||
|
||||
// Delete 删除密码策略
|
||||
func (r *PasswordPolicyRepository) Delete(id uint) error { |
||||
return r.db.Delete(&model.PasswordPolicy{}, id).Error |
||||
} |
||||
|
||||
// GetAll 获取所有密码策略
|
||||
func (r *PasswordPolicyRepository) GetAll() ([]*model.PasswordPolicy, error) { |
||||
var policies []*model.PasswordPolicy |
||||
err := r.db.Find(&policies).Error |
||||
return policies, err |
||||
} |
||||
|
||||
// GetByID 根据ID获取密码策略
|
||||
func (r *PasswordPolicyRepository) GetByID(id uint) (*model.PasswordPolicy, error) { |
||||
var policy model.PasswordPolicy |
||||
err := r.db.First(&policy, id).Error |
||||
return &policy, err |
||||
} |
||||
|
||||
// getDefaultPolicy 获取默认密码策略
|
||||
func (r *PasswordPolicyRepository) getDefaultPolicy() *model.PasswordPolicy { |
||||
return &model.PasswordPolicy{ |
||||
Level: 1, |
||||
MinLength: 6, |
||||
RequireUppercase: false, |
||||
RequireLowercase: false, |
||||
RequireNumbers: false, |
||||
RequireSpecial: false, |
||||
MinCharTypes: 1, |
||||
ExpirationDays: 30, |
||||
PreventReuse: 3, |
||||
IsActive: true, |
||||
} |
||||
} |
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
package repository |
||||
|
||||
import ( |
||||
"gofaster/internal/auth/model" |
||||
|
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
type PasswordResetRepository struct { |
||||
db *gorm.DB |
||||
} |
||||
|
||||
func NewPasswordResetRepository(db *gorm.DB) *PasswordResetRepository { |
||||
return &PasswordResetRepository{db: db} |
||||
} |
||||
|
||||
// Create 创建密码重置记录
|
||||
func (r *PasswordResetRepository) Create(reset *model.PasswordReset) error { |
||||
return r.db.Create(reset).Error |
||||
} |
||||
|
||||
// GetByUserID 获取用户的密码重置记录
|
||||
func (r *PasswordResetRepository) GetByUserID(userID uint) ([]*model.PasswordReset, error) { |
||||
var resets []*model.PasswordReset |
||||
err := r.db.Where("user_id = ?", userID). |
||||
Order("created_at DESC"). |
||||
Find(&resets).Error |
||||
return resets, err |
||||
} |
||||
|
||||
// GetUnusedByUserID 获取用户未使用的密码重置记录
|
||||
func (r *PasswordResetRepository) GetUnusedByUserID(userID uint) (*model.PasswordReset, error) { |
||||
var reset model.PasswordReset |
||||
err := r.db.Where("user_id = ? AND is_used = ?", userID, false). |
||||
Order("created_at DESC"). |
||||
First(&reset).Error |
||||
return &reset, err |
||||
} |
||||
|
||||
// MarkAsUsed 标记密码重置记录为已使用
|
||||
func (r *PasswordResetRepository) MarkAsUsed(id uint) error { |
||||
return r.db.Model(&model.PasswordReset{}). |
||||
Where("id = ?", id). |
||||
Updates(map[string]interface{}{ |
||||
"is_used": true, |
||||
"used_at": "NOW()", |
||||
}).Error |
||||
} |
||||
|
||||
// DeleteOldRecords 删除旧的密码重置记录
|
||||
func (r *PasswordResetRepository) DeleteOldRecords(days int) error { |
||||
return r.db.Where("created_at < DATE_SUB(NOW(), INTERVAL ? DAY)", days). |
||||
Delete(&model.PasswordReset{}).Error |
||||
} |
@ -0,0 +1,381 @@
@@ -0,0 +1,381 @@
|
||||
package service |
||||
|
||||
import ( |
||||
"crypto/sha256" |
||||
"encoding/hex" |
||||
"fmt" |
||||
mathrand "math/rand" |
||||
"strings" |
||||
"time" |
||||
"unicode" |
||||
|
||||
"gofaster/internal/auth/model" |
||||
"gofaster/internal/auth/repository" |
||||
) |
||||
|
||||
type PasswordService struct { |
||||
userService *UserService |
||||
passwordPolicyRepo *repository.PasswordPolicyRepository |
||||
passwordHistoryRepo *repository.PasswordHistoryRepository |
||||
passwordResetRepo *repository.PasswordResetRepository |
||||
} |
||||
|
||||
func NewPasswordService( |
||||
userService *UserService, |
||||
passwordPolicyRepo *repository.PasswordPolicyRepository, |
||||
passwordHistoryRepo *repository.PasswordHistoryRepository, |
||||
passwordResetRepo *repository.PasswordResetRepository, |
||||
) *PasswordService { |
||||
return &PasswordService{ |
||||
userService: userService, |
||||
passwordPolicyRepo: passwordPolicyRepo, |
||||
passwordHistoryRepo: passwordHistoryRepo, |
||||
passwordResetRepo: passwordResetRepo, |
||||
} |
||||
} |
||||
|
||||
// GetPasswordPolicy 获取当前密码策略
|
||||
func (s *PasswordService) GetPasswordPolicy() (*model.PasswordPolicy, error) { |
||||
return s.passwordPolicyRepo.GetActivePolicy() |
||||
} |
||||
|
||||
// ValidatePassword 验证密码是否符合策略要求
|
||||
func (s *PasswordService) ValidatePassword(password string) (*model.PasswordValidationResult, error) { |
||||
policy, err := s.GetPasswordPolicy() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
result := &model.PasswordValidationResult{ |
||||
IsValid: true, |
||||
Errors: []string{}, |
||||
} |
||||
|
||||
// 检查长度
|
||||
if len(password) < policy.MinLength { |
||||
result.IsValid = false |
||||
result.Errors = append(result.Errors, fmt.Sprintf("密码长度不能少于%d位", policy.MinLength)) |
||||
} |
||||
|
||||
// 检查字符类型要求
|
||||
charTypes := 0 |
||||
hasUppercase := false |
||||
hasLowercase := false |
||||
hasNumbers := false |
||||
hasSpecial := false |
||||
|
||||
for _, char := range password { |
||||
switch { |
||||
case unicode.IsUpper(char): |
||||
hasUppercase = true |
||||
case unicode.IsLower(char): |
||||
hasLowercase = true |
||||
case unicode.IsNumber(char): |
||||
hasNumbers = true |
||||
case unicode.IsPunct(char) || unicode.IsSymbol(char): |
||||
hasSpecial = true |
||||
} |
||||
} |
||||
|
||||
if hasUppercase { |
||||
charTypes++ |
||||
} |
||||
if hasLowercase { |
||||
charTypes++ |
||||
} |
||||
if hasNumbers { |
||||
charTypes++ |
||||
} |
||||
if hasSpecial { |
||||
charTypes++ |
||||
} |
||||
|
||||
// 添加调试日志
|
||||
fmt.Printf("密码验证调试 - 密码: %s, 长度: %d, 字符类型: %d\n", password, len(password), charTypes) |
||||
fmt.Printf("字符类型详情 - 大写: %v, 小写: %v, 数字: %v, 特殊: %v\n", hasUppercase, hasLowercase, hasNumbers, hasSpecial) |
||||
|
||||
// 检查具体类型要求
|
||||
if policy.RequireUppercase && !hasUppercase { |
||||
result.IsValid = false |
||||
result.Errors = append(result.Errors, "密码必须包含大写字母") |
||||
} |
||||
if policy.RequireLowercase && !hasLowercase { |
||||
result.IsValid = false |
||||
result.Errors = append(result.Errors, "密码必须包含小写字母") |
||||
} |
||||
if policy.RequireNumbers && !hasNumbers { |
||||
result.IsValid = false |
||||
result.Errors = append(result.Errors, "密码必须包含数字") |
||||
} |
||||
if policy.RequireSpecial && !hasSpecial { |
||||
result.IsValid = false |
||||
result.Errors = append(result.Errors, "密码必须包含特殊字符") |
||||
} |
||||
|
||||
// 检查最少字符类型数
|
||||
if charTypes < policy.MinCharTypes { |
||||
result.IsValid = false |
||||
result.Errors = append(result.Errors, fmt.Sprintf("密码必须包含至少%d种字符类型", policy.MinCharTypes)) |
||||
} |
||||
|
||||
// 计算密码强度
|
||||
result.Strength = s.calculatePasswordStrength(password, charTypes) |
||||
result.Level = s.calculatePasswordLevel(password, charTypes, policy) |
||||
|
||||
// 检查密码强度是否达到最低要求
|
||||
if result.Strength < policy.MinRequiredLevel { |
||||
result.IsValid = false |
||||
result.Errors = append(result.Errors, fmt.Sprintf("密码强度必须达到%d级或以上,当前强度为%d级", policy.MinRequiredLevel, result.Strength)) |
||||
} |
||||
|
||||
// 添加调试日志
|
||||
fmt.Printf("密码强度计算结果 - 强度: %d, 等级: %d\n", result.Strength, result.Level) |
||||
fmt.Printf("最低要求等级: %d, 是否满足要求: %v\n", policy.MinRequiredLevel, result.Strength >= policy.MinRequiredLevel) |
||||
fmt.Printf("返回结果结构: %+v\n", result) |
||||
fmt.Printf("JSON序列化测试: Strength=%d, Level=%d\n", result.Strength, result.Level) |
||||
|
||||
return result, nil |
||||
} |
||||
|
||||
// calculatePasswordStrength 计算密码强度 (0-5)
|
||||
func (s *PasswordService) calculatePasswordStrength(password string, charTypes int) int { |
||||
// 直接返回密码等级,不再使用百分制
|
||||
return s.calculatePasswordLevel(password, charTypes, nil) |
||||
} |
||||
|
||||
// calculatePasswordLevel 计算密码等级 (0-5)
|
||||
func (s *PasswordService) calculatePasswordLevel(password string, charTypes int, policy *model.PasswordPolicy) int { |
||||
// 从最高等级开始判断,一旦满足条件就返回对应等级
|
||||
|
||||
// 5级:长度>=8,字符类型>=4
|
||||
if len(password) >= 8 && charTypes >= 4 { |
||||
return 5 |
||||
} |
||||
|
||||
// 4级:长度>=8,字符类型>=3
|
||||
if len(password) >= 8 && charTypes >= 3 { |
||||
return 4 |
||||
} |
||||
|
||||
// 3级:长度>=6,字符类型>=3
|
||||
if len(password) >= 6 && charTypes >= 3 { |
||||
return 3 |
||||
} |
||||
|
||||
// 2级:长度>=6,字符类型>=2
|
||||
if len(password) >= 6 && charTypes >= 2 { |
||||
return 2 |
||||
} |
||||
|
||||
// 1级:长度>=6,字符类型>=1
|
||||
if len(password) >= 6 && charTypes >= 1 { |
||||
return 1 |
||||
} |
||||
|
||||
// 0级:长度>=1,字符类型>=1(任何密码都至少有一种字符类型)
|
||||
if len(password) >= 1 { |
||||
return 0 |
||||
} |
||||
|
||||
// 如果连0级都不满足,返回-1表示无效
|
||||
return -1 |
||||
} |
||||
|
||||
// CheckPasswordReuse 检查密码是否与历史密码重复
|
||||
func (s *PasswordService) CheckPasswordReuse(userID uint, newPassword string) (bool, error) { |
||||
policy, err := s.GetPasswordPolicy() |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
if policy.PreventReuse <= 0 { |
||||
return false, nil // 不检查重复
|
||||
} |
||||
|
||||
// 获取历史密码
|
||||
history, err := s.passwordHistoryRepo.GetRecentPasswords(userID, policy.PreventReuse) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
// 检查新密码是否与历史密码重复
|
||||
newPasswordHash := s.hashPassword(newPassword) |
||||
for _, hist := range history { |
||||
if hist.Password == newPasswordHash { |
||||
return true, nil // 发现重复
|
||||
} |
||||
} |
||||
|
||||
return false, nil |
||||
} |
||||
|
||||
// ChangePassword 修改用户密码
|
||||
func (s *PasswordService) ChangePassword(userID uint, currentPassword, newPassword string) error { |
||||
// 获取用户信息
|
||||
user, err := s.userService.GetByID(userID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 验证当前密码
|
||||
if !s.verifyPassword(currentPassword, user.Password) { |
||||
return fmt.Errorf("当前密码不正确") |
||||
} |
||||
|
||||
// 验证新密码
|
||||
validation, err := s.ValidatePassword(newPassword) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !validation.IsValid { |
||||
return fmt.Errorf("新密码不符合要求: %s", strings.Join(validation.Errors, "; ")) |
||||
} |
||||
|
||||
// 检查是否与历史密码重复
|
||||
isReused, err := s.CheckPasswordReuse(userID, newPassword) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if isReused { |
||||
policy, err := s.GetPasswordPolicy() |
||||
if err != nil { |
||||
return fmt.Errorf("获取密码策略失败: %w", err) |
||||
} |
||||
return fmt.Errorf("新密码不能与前%d次使用的密码重复", policy.PreventReuse) |
||||
} |
||||
|
||||
// 更新密码
|
||||
newPasswordHash := s.hashPassword(newPassword) |
||||
now := time.Now() |
||||
|
||||
user.Password = newPasswordHash |
||||
user.PasswordChangedAt = &now |
||||
user.ForceChangePassword = false // 取消强制修改密码
|
||||
|
||||
if err := s.userService.Update(user); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 记录密码历史
|
||||
passwordHistory := &model.PasswordHistory{ |
||||
UserID: userID, |
||||
Password: newPasswordHash, |
||||
} |
||||
if err := s.passwordHistoryRepo.Create(passwordHistory); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// ResetPassword 重置用户密码
|
||||
func (s *PasswordService) ResetPassword(userID uint, resetBy uint) (string, error) { |
||||
// 生成临时密码
|
||||
tempPassword := s.generateTempPassword() |
||||
|
||||
// 更新用户密码
|
||||
user, err := s.userService.GetByID(userID) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
user.Password = s.hashPassword(tempPassword) |
||||
user.ForceChangePassword = true // 标记需要强制修改密码
|
||||
now := time.Now() |
||||
user.PasswordChangedAt = &now |
||||
|
||||
if err := s.userService.Update(user); err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
// 记录密码重置
|
||||
passwordReset := &model.PasswordReset{ |
||||
UserID: userID, |
||||
ResetBy: resetBy, |
||||
ResetAt: now, |
||||
} |
||||
if err := s.passwordResetRepo.Create(passwordReset); err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
return tempPassword, nil |
||||
} |
||||
|
||||
// CheckPasswordExpiration 检查密码是否过期
|
||||
func (s *PasswordService) CheckPasswordExpiration(user *model.User) (bool, error) { |
||||
if user.PasswordChangedAt == nil { |
||||
return true, nil // 从未修改过密码,视为过期
|
||||
} |
||||
|
||||
policy, err := s.GetPasswordPolicy() |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
expirationTime := user.PasswordChangedAt.Add(time.Duration(policy.ExpirationDays) * 24 * time.Hour) |
||||
return time.Now().After(expirationTime), nil |
||||
} |
||||
|
||||
// CheckForceChangePassword 检查是否需要强制修改密码
|
||||
func (s *PasswordService) CheckForceChangePassword(user *model.User) bool { |
||||
return user.ForceChangePassword |
||||
} |
||||
|
||||
// generateTempPassword 生成临时密码
|
||||
func (s *PasswordService) generateTempPassword() string { |
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" |
||||
password := make([]byte, 8) |
||||
for i := range password { |
||||
password[i] = charset[mathrand.Intn(len(charset))] |
||||
} |
||||
return string(password) |
||||
} |
||||
|
||||
// hashPassword 密码哈希
|
||||
func (s *PasswordService) hashPassword(password string) string { |
||||
hash := sha256.Sum256([]byte(password)) |
||||
return hex.EncodeToString(hash[:]) |
||||
} |
||||
|
||||
// verifyPassword 验证密码
|
||||
func (s *PasswordService) verifyPassword(password, hash string) bool { |
||||
return s.hashPassword(password) == hash |
||||
} |
||||
|
||||
// TestPasswordStrengthCalculation 测试密码强度计算(仅用于调试)
|
||||
func (s *PasswordService) TestPasswordStrengthCalculation() { |
||||
testPasswords := []string{ |
||||
"a", // 1位,1种类型 -> 0级
|
||||
"abc123", // 6位,2种类型 -> 2级
|
||||
"abc123!", // 7位,3种类型 -> 3级
|
||||
"abc123!@#", // 9位,3种类型 -> 4级
|
||||
"abc123!@#$", // 10位,3种类型 -> 4级
|
||||
"abc123!@#$%", // 11位,3种类型 -> 4级
|
||||
"abc123!@#$%^", // 12位,3种类型 -> 4级
|
||||
"abc123!@#$%^&", // 13位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*", // 14位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()", // 15位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_", // 16位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+", // 17位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=", // 18位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{]", // 19位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|", // 20位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\", // 21位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\/", // 22位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\/<>", // 23位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\/<>?", // 24位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"", // 25位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"'", // 26位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"':", // 27位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"':;", // 28位,3种类型 -> 4级
|
||||
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"':;.", // 29位,3种类型 -> 4级
|
||||
} |
||||
|
||||
fmt.Println("=== 新密码强度计算测试(等级制)===") |
||||
for _, pwd := range testPasswords { |
||||
validation, _ := s.ValidatePassword(pwd) |
||||
fmt.Printf("密码: %-30s | 强度等级: %d | 等级: %d | 有效: %v\n", |
||||
pwd, validation.Strength, validation.Level, validation.IsValid) |
||||
} |
||||
fmt.Println("=== 测试结束 ===") |
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 |
Binary file not shown.
Loading…
Reference in new issue