Browse Source

用户登录、密码管理、个人资料处理完成

master
hejl 2 weeks ago
parent
commit
6130e9e7e2
  1. 339
      gofaster/app/dist/renderer/js/index.js
  2. 248
      gofaster/app/src/renderer/components/PasswordChangeModal.vue
  3. 111
      gofaster/backend/internal/auth/controller/password_controller.go
  4. 91
      gofaster/backend/internal/auth/module.go
  5. 108
      gofaster/backend/internal/auth/routes/auth_routes.go
  6. 430
      gofaster/backend/internal/auth/service/auth_service.go
  7. 491
      gofaster/backend/internal/auth/service/password_service.go
  8. 98
      gofaster/backend/internal/core/manager.go
  9. 197
      gofaster/backend/internal/shared/middleware/jwt_middleware.go
  10. 91
      gofaster/backend/internal/shared/middleware/logger_middleware.go
  11. 32
      gofaster/backend/internal/shared/middleware/permission_middleware.go
  12. 6
      gofaster/backend/main.go
  13. 2
      gofaster/backend/tmp/build-errors.log
  14. BIN
      gofaster/backend/tmp/main.exe

339
gofaster/app/dist/renderer/js/index.js vendored

@ -1143,6 +1143,19 @@ ___CSS_LOADER_EXPORT___.push([module.id, `
.form-group input.error[data-v-52d75f2c] { .form-group input.error[data-v-52d75f2c] {
border-color: #f44336; border-color: #f44336;
} }
/* 确保禁用状态下的输入框仍然可见 */
.form-group input[data-v-52d75f2c]:disabled {
opacity: 0.7;
background: var(--bg-secondary);
cursor: not-allowed;
}
/* 测试焦点样式 */
.form-group input[data-v-52d75f2c]:focus {
outline: 2px solid #ff9800;
outline-offset: 2px;
}
.error-message[data-v-52d75f2c] { .error-message[data-v-52d75f2c] {
color: #f44336; color: #f44336;
font-size: 12px; font-size: 12px;
@ -3718,21 +3731,14 @@ __webpack_require__.r(__webpack_exports__);
// 方法 // 方法
const loadPasswordPolicy = async () => { const loadPasswordPolicy = async () => {
try { try {
console.log('开始加载密码策略...')
const response = await _services_userService__WEBPACK_IMPORTED_MODULE_1__.userService.getPasswordPolicy() const response = await _services_userService__WEBPACK_IMPORTED_MODULE_1__.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的结构 // 修复:直接检查response.data的结构
if (response.data && response.data.code === 200 && response.data.data) { if (response.data && response.data.code === 200 && response.data.data) {
policy.value = response.data.data policy.value = response.data.data
console.log('密码策略加载成功:', policy.value)
} else if (response && response.code === 200 && response.data) { } else if (response && response.code === 200 && response.data) {
// 备用检查:如果response.data不存在,直接检查response // 备用检查:如果response.data不存在,直接检查response
policy.value = response.data policy.value = response.data
console.log('密码策略加载成功(备用路径):', policy.value)
} else { } else {
console.warn('密码策略响应格式不正确:', response) console.warn('密码策略响应格式不正确:', response)
console.warn('尝试解析响应结构...') console.warn('尝试解析响应结构...')
@ -3784,9 +3790,7 @@ __webpack_require__.r(__webpack_exports__);
} }
try { try {
console.log('正在验证密码:', form.newPassword)
const response = await _services_userService__WEBPACK_IMPORTED_MODULE_1__.userService.validatePassword(form.newPassword) const response = await _services_userService__WEBPACK_IMPORTED_MODULE_1__.userService.validatePassword(form.newPassword)
console.log('后端验证响应:', response)
// 修复:检查多种可能的响应结构 // 修复:检查多种可能的响应结构
let result = null let result = null
@ -3797,7 +3801,6 @@ __webpack_require__.r(__webpack_exports__);
} }
if (result) { if (result) {
console.log('验证结果:', result)
if (!result.is_valid) { if (!result.is_valid) {
// 确保错误信息是数组格式 // 确保错误信息是数组格式
@ -3813,8 +3816,6 @@ __webpack_require__.r(__webpack_exports__);
passwordStrength.value = result.strength || 0 passwordStrength.value = result.strength || 0
passwordLevel.value = result.level || 0 passwordLevel.value = result.level || 0
console.log('设置密码强度:', passwordStrength.value, '等级:', passwordLevel.value)
// 更新要求满足状态 // 更新要求满足状态
updateRequirements(form.newPassword) updateRequirements(form.newPassword)
} else { } else {
@ -3830,7 +3831,6 @@ __webpack_require__.r(__webpack_exports__);
} }
const updateRequirements = (password) => { const updateRequirements = (password) => {
console.log('updateRequirements 被调用,密码:', password)
// 重置所有状态 // 重置所有状态
requirements.length = false requirements.length = false
@ -3865,14 +3865,6 @@ __webpack_require__.r(__webpack_exports__);
} }
} }
console.log('字符类型检测结果:', {
hasUppercase: hasUppercase,
hasLowercase: hasLowercase,
hasNumbers: hasNumbers,
hasSpecial: hasSpecial,
length: password.length
})
// 更新要求状态 // 更新要求状态
requirements.length = password.length >= 6 // 要求至少6位 requirements.length = password.length >= 6 // 要求至少6位
requirements.uppercase = hasUppercase requirements.uppercase = hasUppercase
@ -3891,15 +3883,6 @@ __webpack_require__.r(__webpack_exports__);
const strength = calculatePasswordStrength(password, charTypes) const strength = calculatePasswordStrength(password, charTypes)
passwordStrength.value = strength passwordStrength.value = strength
passwordLevel.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)
} }
// 前端密码强度计算方法 // 前端密码强度计算方法
@ -3953,6 +3936,11 @@ __webpack_require__.r(__webpack_exports__);
loading.value = true loading.value = true
// 清除之前的错误信息
Object.keys(errors).forEach(key => {
errors[key] = ''
})
try { try {
const requestData = { const requestData = {
current_password: form.currentPassword, current_password: form.currentPassword,
@ -3960,12 +3948,9 @@ __webpack_require__.r(__webpack_exports__);
confirm_password: form.confirmPassword confirm_password: form.confirmPassword
} }
await _services_userService__WEBPACK_IMPORTED_MODULE_1__.userService.changePassword(requestData) const response = await _services_userService__WEBPACK_IMPORTED_MODULE_1__.userService.changePassword(requestData)
// 成功
emit('success')
handleClose()
// 成功处理
// 显示成功消息 // 显示成功消息
if (window.showToast) { if (window.showToast) {
window.showToast({ window.showToast({
@ -3973,21 +3958,62 @@ __webpack_require__.r(__webpack_exports__);
title: '密码修改成功', title: '密码修改成功',
content: '您的密码已成功修改' content: '您的密码已成功修改'
}) })
} else {
// 如果没有全局toast,使用alert作为后备
alert('密码修改成功!')
} }
// 延迟关闭弹窗,让用户看到成功提示
setTimeout(() => {
emit('success')
handleClose()
}, 1500)
} catch (error) { } catch (error) {
console.error('修改密码失败:', error)
// 显示错误消息 // 改进错误信息处理
if (error.response?.data?.message) { let errorMessage = '修改密码失败,请重试'
if (error.response.data.message.includes('当前密码不正确')) { let targetField = 'newPassword'
errors.currentPassword = '当前密码不正确'
} else if (error.response.data.message.includes('新密码不符合要求')) { if (error.response?.data) {
errors.newPassword = error.response.data.message const responseData = error.response.data
} else {
errors.newPassword = error.response.data.message // 处理不同的错误情况
if (responseData.error) {
// 优先使用error字段,这通常包含具体的错误信息
errorMessage = responseData.error
// 根据错误信息内容判断应该显示在哪个字段
if (errorMessage.includes('当前密码') || errorMessage.includes('current password')) {
targetField = 'currentPassword'
} else if (errorMessage.includes('新密码') || errorMessage.includes('new password')) {
targetField = 'newPassword'
} else if (errorMessage.includes('确认密码') || errorMessage.includes('confirm password')) {
targetField = 'confirmPassword'
}
} else if (responseData.message) {
// 如果没有error字段,使用message字段
errorMessage = responseData.message
// 根据错误信息内容判断应该显示在哪个字段
if (errorMessage.includes('当前密码') || errorMessage.includes('current password')) {
targetField = 'currentPassword'
} else if (errorMessage.includes('新密码') || errorMessage.includes('new password')) {
targetField = 'newPassword'
} else if (errorMessage.includes('确认密码') || errorMessage.includes('confirm password')) {
targetField = 'confirmPassword'
}
} }
} else { } else if (error.message) {
errors.newPassword = '修改密码失败,请重试' errorMessage = error.message
}
// 显示错误信息
errors[targetField] = errorMessage
// 如果是强制修改密码模式,错误信息也显示在newPassword字段
if (props.isForceChange && targetField === 'currentPassword') {
errors.newPassword = errorMessage
} }
} finally { } finally {
loading.value = false loading.value = false
@ -3997,23 +4023,8 @@ __webpack_require__.r(__webpack_exports__);
const handleClose = () => { const handleClose = () => {
if (loading.value) return if (loading.value) return
// 重置表单 // 使用统一的重置函数
form.currentPassword = '' resetFormState()
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') emit('close')
} }
@ -4024,19 +4035,98 @@ __webpack_require__.r(__webpack_exports__);
// 只有通过明确的取消按钮或关闭按钮才能关闭 // 只有通过明确的取消按钮或关闭按钮才能关闭
return false return false
} }
const clearCurrentPasswordError = () => {
errors.currentPassword = ''
}
const clearNewPasswordError = () => {
errors.newPassword = ''
}
const clearConfirmPasswordError = () => {
errors.confirmPassword = ''
}
const handleNewPasswordInput = () => {
clearNewPasswordError()
validatePassword()
}
const handleConfirmPasswordInput = () => {
clearConfirmPasswordError()
validateConfirmPassword()
}
// 监听器 // 监听器
;(0,vue__WEBPACK_IMPORTED_MODULE_0__.watch)(() => props.visible, (newVal) => { ;(0,vue__WEBPACK_IMPORTED_MODULE_0__.watch)(() => props.visible, (newVal) => {
if (newVal) { if (newVal) {
console.log('模态框显示,开始加载密码策略')
// 每次打开弹窗时,确保完全重置所有状态
resetFormState()
loadPasswordPolicy() loadPasswordPolicy()
// 延迟测试输入框焦点和强制聚焦
;(0,vue__WEBPACK_IMPORTED_MODULE_0__.nextTick)(() => {
if (!props.isForceChange && form.currentPassword === '') {
// 尝试强制聚焦到第一个输入框
try {
const currentPasswordInput = document.querySelector('input[type="password"]')
if (currentPasswordInput) {
currentPasswordInput.focus()
}
} catch (error) {
console.error('聚焦失败:', error)
}
} else {
// 尝试强制聚焦到新密码输入框
try {
const newPasswordInput = document.querySelectorAll('input[type="password"]')[1]
if (newPasswordInput) {
newPasswordInput.focus()
}
} catch (error) {
console.error('聚焦失败:', error)
}
}
})
} }
}) })
// 重置表单状态的函数
const resetFormState = () => {
// 重置表单数据
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
// 重置加载状态 - 这是关键!
loading.value = false
// 强制触发响应式更新
;(0,vue__WEBPACK_IMPORTED_MODULE_0__.nextTick)(() => {
})
}
// 监听密码变化,实时更新要求状态 // 监听密码变化,实时更新要求状态
;(0,vue__WEBPACK_IMPORTED_MODULE_0__.watch)(() => form.newPassword, (newPassword) => { ;(0,vue__WEBPACK_IMPORTED_MODULE_0__.watch)(() => form.newPassword, (newPassword) => {
if (newPassword) { if (newPassword) {
console.log('密码变化,更新要求状态:', newPassword)
updateRequirements(newPassword) updateRequirements(newPassword)
} else { } else {
// 密码为空时重置所有状态 // 密码为空时重置所有状态
@ -4051,7 +4141,6 @@ __webpack_require__.r(__webpack_exports__);
// 生命周期 // 生命周期
;(0,vue__WEBPACK_IMPORTED_MODULE_0__.onMounted)(() => { ;(0,vue__WEBPACK_IMPORTED_MODULE_0__.onMounted)(() => {
console.log('组件挂载,检查是否需要加载密码策略')
if (props.visible) { if (props.visible) {
loadPasswordPolicy() loadPasswordPolicy()
} }
@ -4080,7 +4169,13 @@ __webpack_require__.r(__webpack_exports__);
updateRequirements, updateRequirements,
handleSubmit, handleSubmit,
handleClose, handleClose,
handleOverlayClick handleOverlayClick,
clearCurrentPasswordError,
clearNewPasswordError,
clearConfirmPasswordError,
handleNewPasswordInput,
handleConfirmPasswordInput,
resetFormState // 暴露重置函数
} }
} }
}); });
@ -5413,32 +5508,35 @@ const _hoisted_3 = {
key: 0, key: 0,
class: "form-group" class: "form-group"
} }
const _hoisted_4 = { const _hoisted_4 = ["disabled"]
const _hoisted_5 = {
key: 0, key: 0,
class: "error-message" class: "error-message"
} }
const _hoisted_5 = { class: "form-group" } const _hoisted_6 = { class: "form-group" }
const _hoisted_6 = { const _hoisted_7 = ["disabled"]
const _hoisted_8 = {
key: 0, key: 0,
class: "error-message" class: "error-message"
} }
const _hoisted_7 = { class: "password-strength" } const _hoisted_9 = { class: "password-strength" }
const _hoisted_8 = { class: "strength-bar" } const _hoisted_10 = { class: "strength-bar" }
const _hoisted_9 = { class: "strength-text" } const _hoisted_11 = { class: "strength-text" }
const _hoisted_10 = { class: "password-requirements" } const _hoisted_12 = { class: "password-requirements" }
const _hoisted_11 = { class: "form-group" } const _hoisted_13 = { class: "form-group" }
const _hoisted_12 = { const _hoisted_14 = ["disabled"]
const _hoisted_15 = {
key: 0, key: 0,
class: "error-message" class: "error-message"
} }
const _hoisted_13 = { class: "form-actions" } const _hoisted_16 = { class: "form-actions" }
const _hoisted_14 = ["disabled"] const _hoisted_17 = ["disabled"]
const _hoisted_15 = ["disabled"] const _hoisted_18 = ["disabled"]
const _hoisted_16 = { const _hoisted_19 = {
key: 0, key: 0,
class: "loading-spinner" class: "loading-spinner"
} }
const _hoisted_17 = { const _hoisted_20 = {
key: 1, key: 1,
class: "force-change-notice" class: "force-change-notice"
} }
@ -5448,11 +5546,11 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", { ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", {
key: 0, key: 0,
class: "password-modal-overlay", class: "password-modal-overlay",
onClick: _cache[10] || (_cache[10] = (...args) => ($setup.handleOverlayClick && $setup.handleOverlayClick(...args))) onClick: _cache[11] || (_cache[11] = (...args) => ($setup.handleOverlayClick && $setup.handleOverlayClick(...args)))
}, [ }, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", {
class: "password-modal", class: "password-modal",
onClick: _cache[9] || (_cache[9] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, ["stop"])) onClick: _cache[10] || (_cache[10] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, ["stop"]))
}, [ }, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_1, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_1, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("h3", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.title), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("h3", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.title), 1 /* TEXT */),
@ -5463,55 +5561,64 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
]), ]),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_2, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_2, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("form", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("form", {
onSubmit: _cache[8] || (_cache[8] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)((...args) => ($setup.handleSubmit && $setup.handleSubmit(...args)), ["prevent"])) onSubmit: _cache[9] || (_cache[9] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)((...args) => ($setup.handleSubmit && $setup.handleSubmit(...args)), ["prevent"]))
}, [ }, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 当前密码 "), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 当前密码 "),
(!$props.isForceChange) (!$props.isForceChange)
? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_3, [ ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_3, [
_cache[11] || (_cache[11] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("label", null, "当前密码 *", -1 /* CACHED */)), _cache[12] || (_cache[12] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("label", null, "当前密码 *", -1 /* CACHED */)),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("input", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("input", {
"onUpdate:modelValue": _cache[1] || (_cache[1] = $event => (($setup.form.currentPassword) = $event)), "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => (($setup.form.currentPassword) = $event)),
type: "password", type: "password",
required: "", required: "",
placeholder: "请输入当前密码", placeholder: "请输入当前密码",
class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({ 'error': $setup.errors.currentPassword }) onInput: _cache[2] || (_cache[2] = (...args) => ($setup.clearCurrentPasswordError && $setup.clearCurrentPasswordError(...args))),
}, null, 2 /* CLASS */), [ class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({ 'error': $setup.errors.currentPassword }),
disabled: $setup.loading,
ref: "currentPasswordInput",
tabindex: "1",
autocomplete: "current-password"
}, null, 42 /* CLASS, PROPS, NEED_HYDRATION */, _hoisted_4), [
[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.form.currentPassword] [vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.form.currentPassword]
]), ]),
($setup.errors.currentPassword) ($setup.errors.currentPassword)
? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_4, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.errors.currentPassword), 1 /* TEXT */)) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_5, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.errors.currentPassword), 1 /* TEXT */))
: (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true)
])) ]))
: (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true), : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 新密码 "), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 新密码 "),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_5, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_6, [
_cache[12] || (_cache[12] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("label", null, "新密码 *", -1 /* CACHED */)), _cache[13] || (_cache[13] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("label", null, "新密码 *", -1 /* CACHED */)),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("input", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("input", {
"onUpdate:modelValue": _cache[2] || (_cache[2] = $event => (($setup.form.newPassword) = $event)), "onUpdate:modelValue": _cache[3] || (_cache[3] = $event => (($setup.form.newPassword) = $event)),
type: "password", type: "password",
required: "", required: "",
placeholder: "请输入新密码", placeholder: "请输入新密码",
onInput: _cache[3] || (_cache[3] = (...args) => ($setup.validatePassword && $setup.validatePassword(...args))), onInput: _cache[4] || (_cache[4] = (...args) => ($setup.handleNewPasswordInput && $setup.handleNewPasswordInput(...args))),
onKeyup: _cache[4] || (_cache[4] = $event => ($setup.updateRequirements($setup.form.newPassword))), onKeyup: _cache[5] || (_cache[5] = $event => ($setup.updateRequirements($setup.form.newPassword))),
class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({ 'error': $setup.errors.newPassword }) class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({ 'error': $setup.errors.newPassword }),
}, null, 34 /* CLASS, NEED_HYDRATION */), [ disabled: $setup.loading,
ref: "newPasswordInput",
tabindex: "2",
autocomplete: "new-password"
}, null, 42 /* CLASS, PROPS, NEED_HYDRATION */, _hoisted_7), [
[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.form.newPassword] [vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.form.newPassword]
]), ]),
($setup.errors.newPassword) ($setup.errors.newPassword)
? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_6, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.errors.newPassword), 1 /* TEXT */)) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_8, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.errors.newPassword), 1 /* TEXT */))
: (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true), : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 密码强度指示器 "), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 密码强度指示器 "),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_7, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_9, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_8, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_10, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", {
class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(["strength-fill", $setup.strengthClass]), class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(["strength-fill", $setup.strengthClass]),
style: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeStyle)({ width: $setup.strengthPercentage + '%' }) style: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeStyle)({ width: $setup.strengthPercentage + '%' })
}, null, 6 /* CLASS, STYLE */) }, null, 6 /* CLASS, STYLE */)
]), ]),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_9, " 强度: " + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.strengthText) + " (" + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.passwordLevel) + "级) ", 1 /* TEXT */) (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_11, " 强度: " + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.strengthText) + " (" + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.passwordLevel) + "级) ", 1 /* TEXT */)
]), ]),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 密码要求提示 "), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 密码要求提示 "),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_10, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_12, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", {
class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(["requirement", { 'met': $setup.requirements.length }]) class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(["requirement", { 'met': $setup.requirements.length }])
}, " ✓ 密码长度" + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.form.newPassword.length) + "位 ", 3 /* TEXT, CLASS */), }, " ✓ 密码长度" + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.form.newPassword.length) + "位 ", 3 /* TEXT, CLASS */),
@ -5530,44 +5637,48 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
]) ])
]), ]),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 确认新密码 "), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 确认新密码 "),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_11, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_13, [
_cache[13] || (_cache[13] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("label", null, "确认新密码 *", -1 /* CACHED */)), _cache[14] || (_cache[14] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("label", null, "确认新密码 *", -1 /* CACHED */)),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("input", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("input", {
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => (($setup.form.confirmPassword) = $event)), "onUpdate:modelValue": _cache[6] || (_cache[6] = $event => (($setup.form.confirmPassword) = $event)),
type: "password", type: "password",
required: "", required: "",
placeholder: "请再次输入新密码", placeholder: "请再次输入新密码",
onInput: _cache[6] || (_cache[6] = (...args) => ($setup.validateConfirmPassword && $setup.validateConfirmPassword(...args))), onInput: _cache[7] || (_cache[7] = (...args) => ($setup.handleConfirmPasswordInput && $setup.handleConfirmPasswordInput(...args))),
class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({ 'error': $setup.errors.confirmPassword }) class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({ 'error': $setup.errors.confirmPassword }),
}, null, 34 /* CLASS, NEED_HYDRATION */), [ disabled: $setup.loading,
ref: "confirmPasswordInput",
tabindex: "3",
autocomplete: "new-password"
}, null, 42 /* CLASS, PROPS, NEED_HYDRATION */, _hoisted_14), [
[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.form.confirmPassword] [vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.form.confirmPassword]
]), ]),
($setup.errors.confirmPassword) ($setup.errors.confirmPassword)
? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_12, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.errors.confirmPassword), 1 /* TEXT */)) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_15, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.errors.confirmPassword), 1 /* TEXT */))
: (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true)
]), ]),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 操作按钮 "), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 操作按钮 "),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_13, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_16, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("button", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("button", {
type: "button", type: "button",
class: "btn btn-secondary", class: "btn btn-secondary",
onClick: _cache[7] || (_cache[7] = (...args) => ($setup.handleClose && $setup.handleClose(...args))), onClick: _cache[8] || (_cache[8] = (...args) => ($setup.handleClose && $setup.handleClose(...args))),
disabled: $setup.loading disabled: $setup.loading
}, " 取消 ", 8 /* PROPS */, _hoisted_14), }, " 取消 ", 8 /* PROPS */, _hoisted_17),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("button", { (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("button", {
type: "submit", type: "submit",
class: "btn btn-primary", class: "btn btn-primary",
disabled: $setup.loading || !$setup.isFormValid disabled: $setup.loading || !$setup.isFormValid
}, [ }, [
($setup.loading) ($setup.loading)
? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("span", _hoisted_16)) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("span", _hoisted_19))
: (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true), : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)(" " + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.submitText), 1 /* TEXT */) (0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)(" " + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.submitText), 1 /* TEXT */)
], 8 /* PROPS */, _hoisted_15) ], 8 /* PROPS */, _hoisted_18)
]), ]),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 强制修改密码提示 "), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(" 强制修改密码提示 "),
($props.isForceChange) ($props.isForceChange)
? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_17, _cache[14] || (_cache[14] = [ ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_20, _cache[15] || (_cache[15] = [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", { class: "notice-icon" }, "⚠", -1 /* CACHED */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", { class: "notice-icon" }, "⚠", -1 /* CACHED */),
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", { class: "notice-text" }, [ (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", { class: "notice-text" }, [
(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("strong", null, "重要提示:"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("strong", null, "重要提示:"),
@ -8698,7 +8809,7 @@ __webpack_require__.r(__webpack_exports__);
/******/ /******/
/******/ /* webpack/runtime/getFullHash */ /******/ /* webpack/runtime/getFullHash */
/******/ (() => { /******/ (() => {
/******/ __webpack_require__.h = () => ("37a1d732559d3aaa") /******/ __webpack_require__.h = () => ("7737269bf8e5d297")
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ /* webpack/runtime/hasOwnProperty shorthand */

248
gofaster/app/src/renderer/components/PasswordChangeModal.vue

@ -16,7 +16,12 @@
type="password" type="password"
required required
placeholder="请输入当前密码" placeholder="请输入当前密码"
@input="clearCurrentPasswordError"
:class="{ 'error': errors.currentPassword }" :class="{ 'error': errors.currentPassword }"
:disabled="loading"
ref="currentPasswordInput"
tabindex="1"
autocomplete="current-password"
/> />
<div v-if="errors.currentPassword" class="error-message"> <div v-if="errors.currentPassword" class="error-message">
{{ errors.currentPassword }} {{ errors.currentPassword }}
@ -31,9 +36,13 @@
type="password" type="password"
required required
placeholder="请输入新密码" placeholder="请输入新密码"
@input="validatePassword" @input="handleNewPasswordInput"
@keyup="updateRequirements(form.newPassword)" @keyup="updateRequirements(form.newPassword)"
:class="{ 'error': errors.newPassword }" :class="{ 'error': errors.newPassword }"
:disabled="loading"
ref="newPasswordInput"
tabindex="2"
autocomplete="new-password"
/> />
<div v-if="errors.newPassword" class="error-message"> <div v-if="errors.newPassword" class="error-message">
{{ errors.newPassword }} {{ errors.newPassword }}
@ -81,8 +90,12 @@
type="password" type="password"
required required
placeholder="请再次输入新密码" placeholder="请再次输入新密码"
@input="validateConfirmPassword" @input="handleConfirmPasswordInput"
:class="{ 'error': errors.confirmPassword }" :class="{ 'error': errors.confirmPassword }"
:disabled="loading"
ref="confirmPasswordInput"
tabindex="3"
autocomplete="new-password"
/> />
<div v-if="errors.confirmPassword" class="error-message"> <div v-if="errors.confirmPassword" class="error-message">
{{ errors.confirmPassword }} {{ errors.confirmPassword }}
@ -123,7 +136,7 @@
</template> </template>
<script> <script>
import { ref, reactive, computed, watch, onMounted } from 'vue' import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
import { userService } from '@/services/userService' import { userService } from '@/services/userService'
export default { export default {
@ -218,21 +231,14 @@ export default {
// //
const loadPasswordPolicy = async () => { const loadPasswordPolicy = async () => {
try { try {
console.log('开始加载密码策略...')
const response = await userService.getPasswordPolicy() 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 // response.data
if (response.data && response.data.code === 200 && response.data.data) { if (response.data && response.data.code === 200 && response.data.data) {
policy.value = response.data.data policy.value = response.data.data
console.log('密码策略加载成功:', policy.value)
} else if (response && response.code === 200 && response.data) { } else if (response && response.code === 200 && response.data) {
// response.dataresponse // response.dataresponse
policy.value = response.data policy.value = response.data
console.log('密码策略加载成功(备用路径):', policy.value)
} else { } else {
console.warn('密码策略响应格式不正确:', response) console.warn('密码策略响应格式不正确:', response)
console.warn('尝试解析响应结构...') console.warn('尝试解析响应结构...')
@ -284,9 +290,7 @@ export default {
} }
try { try {
console.log('正在验证密码:', form.newPassword)
const response = await userService.validatePassword(form.newPassword) const response = await userService.validatePassword(form.newPassword)
console.log('后端验证响应:', response)
// //
let result = null let result = null
@ -297,7 +301,6 @@ export default {
} }
if (result) { if (result) {
console.log('验证结果:', result)
if (!result.is_valid) { if (!result.is_valid) {
// //
@ -313,8 +316,6 @@ export default {
passwordStrength.value = result.strength || 0 passwordStrength.value = result.strength || 0
passwordLevel.value = result.level || 0 passwordLevel.value = result.level || 0
console.log('设置密码强度:', passwordStrength.value, '等级:', passwordLevel.value)
// //
updateRequirements(form.newPassword) updateRequirements(form.newPassword)
} else { } else {
@ -330,7 +331,6 @@ export default {
} }
const updateRequirements = (password) => { const updateRequirements = (password) => {
console.log('updateRequirements 被调用,密码:', password)
// //
requirements.length = false requirements.length = false
@ -365,14 +365,6 @@ export default {
} }
} }
console.log('字符类型检测结果:', {
hasUppercase: hasUppercase,
hasLowercase: hasLowercase,
hasNumbers: hasNumbers,
hasSpecial: hasSpecial,
length: password.length
})
// //
requirements.length = password.length >= 6 // 6 requirements.length = password.length >= 6 // 6
requirements.uppercase = hasUppercase requirements.uppercase = hasUppercase
@ -391,15 +383,6 @@ export default {
const strength = calculatePasswordStrength(password, charTypes) const strength = calculatePasswordStrength(password, charTypes)
passwordStrength.value = strength passwordStrength.value = strength
passwordLevel.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)
} }
// //
@ -453,6 +436,11 @@ export default {
loading.value = true loading.value = true
//
Object.keys(errors).forEach(key => {
errors[key] = ''
})
try { try {
const requestData = { const requestData = {
current_password: form.currentPassword, current_password: form.currentPassword,
@ -460,12 +448,9 @@ export default {
confirm_password: form.confirmPassword confirm_password: form.confirmPassword
} }
await userService.changePassword(requestData) const response = await userService.changePassword(requestData)
//
emit('success')
handleClose()
//
// //
if (window.showToast) { if (window.showToast) {
window.showToast({ window.showToast({
@ -473,21 +458,62 @@ export default {
title: '密码修改成功', title: '密码修改成功',
content: '您的密码已成功修改' content: '您的密码已成功修改'
}) })
} else {
// toast使alert
alert('密码修改成功!')
} }
//
setTimeout(() => {
emit('success')
handleClose()
}, 1500)
} catch (error) { } catch (error) {
console.error('修改密码失败:', error)
// //
if (error.response?.data?.message) { let errorMessage = '修改密码失败,请重试'
if (error.response.data.message.includes('当前密码不正确')) { let targetField = 'newPassword'
errors.currentPassword = '当前密码不正确'
} else if (error.response.data.message.includes('新密码不符合要求')) { if (error.response?.data) {
errors.newPassword = error.response.data.message const responseData = error.response.data
} else {
errors.newPassword = error.response.data.message //
if (responseData.error) {
// 使error
errorMessage = responseData.error
//
if (errorMessage.includes('当前密码') || errorMessage.includes('current password')) {
targetField = 'currentPassword'
} else if (errorMessage.includes('新密码') || errorMessage.includes('new password')) {
targetField = 'newPassword'
} else if (errorMessage.includes('确认密码') || errorMessage.includes('confirm password')) {
targetField = 'confirmPassword'
}
} else if (responseData.message) {
// error使message
errorMessage = responseData.message
//
if (errorMessage.includes('当前密码') || errorMessage.includes('current password')) {
targetField = 'currentPassword'
} else if (errorMessage.includes('新密码') || errorMessage.includes('new password')) {
targetField = 'newPassword'
} else if (errorMessage.includes('确认密码') || errorMessage.includes('confirm password')) {
targetField = 'confirmPassword'
}
} }
} else { } else if (error.message) {
errors.newPassword = '修改密码失败,请重试' errorMessage = error.message
}
//
errors[targetField] = errorMessage
// newPassword
if (props.isForceChange && targetField === 'currentPassword') {
errors.newPassword = errorMessage
} }
} finally { } finally {
loading.value = false loading.value = false
@ -497,23 +523,8 @@ export default {
const handleClose = () => { const handleClose = () => {
if (loading.value) return if (loading.value) return
// // 使
form.currentPassword = '' resetFormState()
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') emit('close')
} }
@ -524,19 +535,98 @@ export default {
// //
return false return false
} }
const clearCurrentPasswordError = () => {
errors.currentPassword = ''
}
const clearNewPasswordError = () => {
errors.newPassword = ''
}
const clearConfirmPasswordError = () => {
errors.confirmPassword = ''
}
const handleNewPasswordInput = () => {
clearNewPasswordError()
validatePassword()
}
const handleConfirmPasswordInput = () => {
clearConfirmPasswordError()
validateConfirmPassword()
}
// //
watch(() => props.visible, (newVal) => { watch(() => props.visible, (newVal) => {
if (newVal) { if (newVal) {
console.log('模态框显示,开始加载密码策略')
//
resetFormState()
loadPasswordPolicy() loadPasswordPolicy()
//
nextTick(() => {
if (!props.isForceChange && form.currentPassword === '') {
//
try {
const currentPasswordInput = document.querySelector('input[type="password"]')
if (currentPasswordInput) {
currentPasswordInput.focus()
}
} catch (error) {
console.error('聚焦失败:', error)
}
} else {
//
try {
const newPasswordInput = document.querySelectorAll('input[type="password"]')[1]
if (newPasswordInput) {
newPasswordInput.focus()
}
} catch (error) {
console.error('聚焦失败:', error)
}
}
})
} }
}) })
//
const resetFormState = () => {
//
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
// -
loading.value = false
//
nextTick(() => {
})
}
// //
watch(() => form.newPassword, (newPassword) => { watch(() => form.newPassword, (newPassword) => {
if (newPassword) { if (newPassword) {
console.log('密码变化,更新要求状态:', newPassword)
updateRequirements(newPassword) updateRequirements(newPassword)
} else { } else {
// //
@ -551,7 +641,6 @@ export default {
// //
onMounted(() => { onMounted(() => {
console.log('组件挂载,检查是否需要加载密码策略')
if (props.visible) { if (props.visible) {
loadPasswordPolicy() loadPasswordPolicy()
} }
@ -580,7 +669,13 @@ export default {
updateRequirements, updateRequirements,
handleSubmit, handleSubmit,
handleClose, handleClose,
handleOverlayClick handleOverlayClick,
clearCurrentPasswordError,
clearNewPasswordError,
clearConfirmPasswordError,
handleNewPasswordInput,
handleConfirmPasswordInput,
resetFormState //
} }
} }
} }
@ -677,6 +772,19 @@ export default {
border-color: #f44336; border-color: #f44336;
} }
/* 确保禁用状态下的输入框仍然可见 */
.form-group input:disabled {
opacity: 0.7;
background: var(--bg-secondary);
cursor: not-allowed;
}
/* 测试焦点样式 */
.form-group input:focus {
outline: 2px solid #ff9800;
outline-offset: 2px;
}
.error-message { .error-message {
color: #f44336; color: #f44336;
font-size: 12px; font-size: 12px;

111
gofaster/backend/internal/auth/controller/password_controller.go

@ -1,11 +1,8 @@
package controller package controller
import ( import (
"fmt"
"net/http" "net/http"
"strconv"
"gofaster/internal/auth/model"
"gofaster/internal/auth/service" "gofaster/internal/auth/service"
"gofaster/internal/shared/middleware" "gofaster/internal/shared/middleware"
"gofaster/internal/shared/response" "gofaster/internal/shared/response"
@ -30,74 +27,55 @@ func NewPasswordController(
// ChangePassword 修改密码 // ChangePassword 修改密码
func (c *PasswordController) ChangePassword(ctx *gin.Context) { func (c *PasswordController) ChangePassword(ctx *gin.Context) {
var req model.PasswordChangeRequest var req struct {
if err := ctx.ShouldBindJSON(&req); err != nil { CurrentPassword string `json:"current_password" binding:"required"`
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) NewPassword string `json:"new_password" binding:"required"`
return ConfirmPassword string `json:"confirm_password" binding:"required"`
} }
// 添加调试日志 if err := ctx.ShouldBindJSON(&req); err != nil {
fmt.Printf("密码修改请求: UserID=%d, CurrentPassword=%s, NewPassword=%s, ConfirmPassword=%s\n", response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error())
req.UserID, req.CurrentPassword, req.NewPassword, req.ConfirmPassword)
// 从JWT获取用户ID
fmt.Printf("PasswordController - 开始从JWT获取用户ID\n")
userID, exists := middleware.GetUserID(ctx)
if !exists {
fmt.Printf("PasswordController - GetUserID返回false,用户ID不存在\n")
response.Error(ctx, http.StatusUnauthorized, "未授权", "用户ID不存在")
return return
} }
fmt.Printf("PasswordController - GetUserID成功,用户ID: %d\n", userID) // 验证确认密码
req.UserID = userID
fmt.Printf("从JWT获取的用户ID: %d\n", req.UserID)
// 验证新密码确认
if req.NewPassword != req.ConfirmPassword { if req.NewPassword != req.ConfirmPassword {
response.Error(ctx, http.StatusBadRequest, "密码确认失败", "新密码与确认密码不匹配") response.Error(ctx, http.StatusBadRequest, "新密码与确认密码不一致", "")
return return
} }
// 修改密码 // 获取当前用户ID
err := c.passwordService.ChangePassword(req.UserID, req.CurrentPassword, req.NewPassword) userID := middleware.GetUserID(ctx)
// 调用服务层修改密码
err := c.passwordService.ChangePassword(ctx, userID, req.CurrentPassword, req.NewPassword)
if err != nil { if err != nil {
fmt.Printf("密码修改失败: %v\n", err)
response.Error(ctx, http.StatusInternalServerError, "修改密码失败", err.Error()) response.Error(ctx, http.StatusInternalServerError, "修改密码失败", err.Error())
return return
} }
fmt.Printf("密码修改成功,用户ID: %d\n", req.UserID)
response.Success(ctx, "密码修改成功", nil) response.Success(ctx, "密码修改成功", nil)
} }
// ResetPassword 重置用户密码(管理员功能) // ResetPassword 重置密码
func (c *PasswordController) ResetPassword(ctx *gin.Context) { func (c *PasswordController) ResetPassword(ctx *gin.Context) {
userIDStr := ctx.Param("id") var req struct {
userID, err := strconv.ParseUint(userIDStr, 10, 32) UserID string `json:"user_id" binding:"required"`
if err != nil {
response.Error(ctx, http.StatusBadRequest, "用户ID格式错误", err.Error())
return
} }
// 从JWT获取操作人ID if err := ctx.ShouldBindJSON(&req); err != nil {
operatorID, exists := middleware.GetUserID(ctx) response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error())
if !exists {
response.Error(ctx, http.StatusUnauthorized, "未授权", "操作人ID不存在")
return return
} }
// 重置密码 // 调用服务层重置密码
tempPassword, err := c.passwordService.ResetPassword(uint(userID), operatorID) err := c.passwordService.ResetPassword(ctx, req.UserID)
if err != nil { if err != nil {
response.Error(ctx, http.StatusInternalServerError, "重置密码失败", err.Error()) response.Error(ctx, http.StatusInternalServerError, "重置密码失败", err.Error())
return return
} }
response.Success(ctx, "密码重置成功", gin.H{ response.Success(ctx, "密码重置成功", nil)
"temp_password": tempPassword,
"message": "用户首次登录后需要立即修改密码",
})
} }
// GetPasswordPolicy 获取密码策略 // GetPasswordPolicy 获取密码策略
@ -111,7 +89,7 @@ func (c *PasswordController) GetPasswordPolicy(ctx *gin.Context) {
response.Success(ctx, "获取密码策略成功", policy) response.Success(ctx, "获取密码策略成功", policy)
} }
// ValidatePassword 验证密码强度 // ValidatePassword 验证密码
func (c *PasswordController) ValidatePassword(ctx *gin.Context) { func (c *PasswordController) ValidatePassword(ctx *gin.Context) {
var req struct { var req struct {
Password string `json:"password" binding:"required"` Password string `json:"password" binding:"required"`
@ -122,7 +100,6 @@ func (c *PasswordController) ValidatePassword(ctx *gin.Context) {
return return
} }
// 验证密码
result, err := c.passwordService.ValidatePassword(req.Password) result, err := c.passwordService.ValidatePassword(req.Password)
if err != nil { if err != nil {
response.Error(ctx, http.StatusInternalServerError, "密码验证失败", err.Error()) response.Error(ctx, http.StatusInternalServerError, "密码验证失败", err.Error())
@ -132,47 +109,33 @@ func (c *PasswordController) ValidatePassword(ctx *gin.Context) {
response.Success(ctx, "密码验证完成", result) response.Success(ctx, "密码验证完成", result)
} }
// CheckPasswordStatus 检查用户密码状态 // CheckPasswordStatus 检查密码状态
func (c *PasswordController) CheckPasswordStatus(ctx *gin.Context) { func (c *PasswordController) CheckPasswordStatus(ctx *gin.Context) {
userID, exists := middleware.GetUserID(ctx) userID := middleware.GetUserID(ctx)
if !exists {
response.Error(ctx, http.StatusUnauthorized, "未授权", "用户ID不存在")
return
}
// 获取用户信息
user, err := c.userService.GetByID(userID)
if err != nil {
response.Error(ctx, http.StatusInternalServerError, "获取用户信息失败", err.Error())
return
}
// 检查密码过期 status, err := c.passwordService.CheckPasswordStatus(ctx, userID)
isExpired, err := c.passwordService.CheckPasswordExpiration(user)
if err != nil { if err != nil {
response.Error(ctx, http.StatusInternalServerError, "检查密码过期失败", err.Error()) response.Error(ctx, http.StatusInternalServerError, "检查密码状态失败", err.Error())
return return
} }
// 检查是否需要强制修改密码 response.Success(ctx, "检查密码状态成功", status)
forceChange := c.passwordService.CheckForceChangePassword(user)
response.Success(ctx, "获取密码状态成功", gin.H{
"force_change_password": forceChange,
"password_expired": isExpired,
"password_changed_at": user.PasswordChangedAt,
})
} }
// UpdatePasswordPolicy 更新密码策略(管理员功能) // UpdatePasswordPolicy 更新密码策略
func (c *PasswordController) UpdatePasswordPolicy(ctx *gin.Context) { func (c *PasswordController) UpdatePasswordPolicy(ctx *gin.Context) {
var policy model.PasswordPolicy var policy service.PasswordPolicy
if err := ctx.ShouldBindJSON(&policy); err != nil { if err := ctx.ShouldBindJSON(&policy); err != nil {
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error())
return return
} }
// 这里需要调用密码策略服务来更新策略 err := c.passwordService.UpdatePasswordPolicy(&policy)
// 暂时返回成功,具体实现需要完善 if err != nil {
response.Success(ctx, "密码策略更新成功", policy) response.Error(ctx, http.StatusInternalServerError, "更新密码策略失败", err.Error())
return
}
response.Success(ctx, "密码策略更新成功", nil)
} }

91
gofaster/backend/internal/auth/module.go

@ -1,67 +1,66 @@
package auth package auth
import ( import (
"fmt" "log"
"gofaster/internal/auth/migration"
"gofaster/internal/auth/routes" "gofaster/internal/auth/controller"
"gofaster/internal/auth/repository"
"gofaster/internal/auth/service"
"gofaster/internal/core" "gofaster/internal/core"
"gofaster/internal/shared/config"
"gofaster/internal/shared/database" "gofaster/internal/shared/database"
"gofaster/internal/shared/middleware"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
) )
type AuthModule struct { // Module 认证模块
logger *zap.Logger type Module struct {
db *gorm.DB userController *controller.UserController
config *config.Config authController *controller.AuthController
passwordController *controller.PasswordController
} }
func init() { // NewModule 创建新的认证模块
core.RegisterModuleType(&AuthModule{}) func NewModule() *Module {
return &Module{}
} }
func (m *AuthModule) Name() string { // Start 启动模块
return "auth" func (m *Module) Start(cfg interface{}, db *database.Database) error {
} // 初始化仓库
userRepo := repository.NewUserRepository(db.DB)
passwordPolicyRepo := repository.NewPasswordPolicyRepository(db.DB)
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
passwordResetRepo := repository.NewPasswordResetRepository(db.DB)
func (m *AuthModule) Init(config *config.Config, logger *zap.Logger, db *gorm.DB, redis *database.RedisClient) error { // 初始化服务
fmt.Printf("🔍 AuthModule.Init 被调用\n") userService := service.NewUserService(userRepo)
m.logger = logger authService := service.NewAuthService(userRepo)
m.db = db passwordService := service.NewPasswordService(
m.config = config userRepo,
passwordPolicyRepo,
passwordHistoryRepo,
passwordResetRepo,
)
fmt.Printf("✅ AuthModule 配置已设置\n") // 初始化控制器
// 运行数据库迁移 m.userController = controller.NewUserController(userService)
if err := migration.RunMigrations(db); err != nil { m.authController = controller.NewAuthController(authService)
logger.Error("Failed to run auth migrations", zap.Error(err)) m.passwordController = controller.NewPasswordController(passwordService, userService)
return err
}
fmt.Printf("✅ 数据库迁移完成\n") log.Printf("✅ 认证模块初始化完成")
logger.Info("Auth module initialized successfully")
return nil return nil
} }
func (m *AuthModule) RegisterRoutes(router *gin.RouterGroup) { // Stop 停止模块
fmt.Printf("🔍 AuthModule.RegisterRoutes 被调用\n") func (m *Module) Stop() error {
if m.db == nil { return nil
m.logger.Error("Database connection not available for auth routes") }
fmt.Printf("❌ 数据库连接不可用\n")
return
}
fmt.Printf("✅ 数据库连接正常,开始注册认证路由\n") // RegisterRoutes 注册路由
// 注册认证路由 func (m *Module) RegisterRoutes(router interface{}) {
routes.RegisterAuthRoutes(router, m.db, middleware.JWTConfig{ // 这里应该注册具体的路由
SecretKey: m.config.JWT.Secret, // 从配置中获取JWT密钥 // 暂时留空,由具体的路由文件处理
Issuer: m.config.JWT.Issuer, // 从配置中获取JWT发行者
})
} }
func (m *AuthModule) Cleanup() { // GetName 获取模块名称
m.logger.Info("Cleaning up auth module") func (m *Module) GetName() string {
return "auth"
} }

108
gofaster/backend/internal/auth/routes/auth_routes.go

@ -1,92 +1,72 @@
package routes package routes
import ( import (
"fmt" "log"
"gofaster/internal/auth/controller" "gofaster/internal/auth/controller"
"gofaster/internal/auth/repository" "gofaster/internal/auth/repository"
"gofaster/internal/auth/service" "gofaster/internal/auth/service"
"gofaster/internal/shared/jwt"
"gofaster/internal/shared/middleware" "gofaster/internal/shared/middleware"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
// RegisterAuthRoutes 注册认证相关路由 // RegisterAuthRoutes 注册认证相关路由
func RegisterAuthRoutes(r *gin.RouterGroup, db *gorm.DB, jwtConfig middleware.JWTConfig) { func RegisterAuthRoutes(router *gin.RouterGroup, db *gorm.DB) {
fmt.Printf("🚀 开始注册认证路由\n") // 初始化仓库
fmt.Printf("🔑 JWT配置: SecretKey=%s, Issuer=%s\n", jwtConfig.SecretKey[:10]+"...", jwtConfig.Issuer)
// 创建仓储层实例
userRepo := repository.NewUserRepository(db) userRepo := repository.NewUserRepository(db)
captchaRepo := repository.NewCaptchaRepository(db)
passwordPolicyRepo := repository.NewPasswordPolicyRepository(db) passwordPolicyRepo := repository.NewPasswordPolicyRepository(db)
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db) passwordHistoryRepo := repository.NewPasswordHistoryRepository(db)
passwordResetRepo := repository.NewPasswordResetRepository(db) passwordResetRepo := repository.NewPasswordResetRepository(db)
// 创建JWT管理器 // 初始化服务
jwtManager := jwt.NewJWTManager(jwtConfig.SecretKey, jwtConfig.Issuer)
// 创建服务层实例
authService := service.NewAuthService(userRepo, captchaRepo, jwtManager)
userService := service.NewUserService(userRepo) userService := service.NewUserService(userRepo)
passwordService := service.NewPasswordService(userService, passwordPolicyRepo, passwordHistoryRepo, passwordResetRepo) authService := service.NewAuthService(userRepo)
passwordService := service.NewPasswordService(
userRepo,
passwordPolicyRepo,
passwordHistoryRepo,
passwordResetRepo,
)
// 创建控制器实例 // 初始化控制器
userController := controller.NewUserController(userService)
authController := controller.NewAuthController(authService) authController := controller.NewAuthController(authService)
passwordController := controller.NewPasswordController(passwordService, userService) passwordController := controller.NewPasswordController(passwordService, userService)
// 认证路由组 // 公开路由(无需认证)
auth := r.Group("/auth") public := router.Group("/auth")
{ {
// 公开接口(无需认证) public.POST("/login", authController.Login)
auth.POST("/login", authController.Login) // 用户登录 public.POST("/register", userController.Register)
auth.GET("/captcha", authController.GenerateCaptcha) // 生成验证码 public.GET("/password-policy", passwordController.GetPasswordPolicy)
auth.GET("/test", func(c *gin.Context) { // 测试端点 public.POST("/validate-password", passwordController.ValidatePassword)
c.JSON(200, gin.H{ }
"message": "Auth routes are working!",
"timestamp": time.Now().Unix(),
})
})
// 密码策略相关接口(无需认证)
auth.GET("/password-policy", passwordController.GetPasswordPolicy) // 获取密码策略
auth.POST("/validate-password", passwordController.ValidatePassword) // 验证密码强度
// 添加一个测试路由来验证路由注册是否正常
auth.GET("/test-route", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "路由注册正常,JWT中间件即将应用",
"timestamp": time.Now().Unix(),
})
})
// 需要认证的接口
fmt.Printf("🔒 应用JWT中间件到需要认证的路由\n")
auth.Use(middleware.JWTAuth(jwtConfig))
{
// 添加一个测试路由来验证JWT中间件是否工作
auth.GET("/test-jwt", func(c *gin.Context) {
userID, exists := middleware.GetUserID(c)
if !exists {
c.JSON(401, gin.H{"error": "JWT中间件未正确工作,无法获取用户ID"})
return
}
c.JSON(200, gin.H{
"message": "JWT中间件工作正常!",
"user_id": userID,
"timestamp": time.Now().Unix(),
})
})
auth.POST("/logout", authController.Logout) // 用户登出 // 需要认证的路由
auth.POST("/refresh", authController.RefreshToken) // 刷新令牌 auth := router.Group("/auth")
auth.GET("/userinfo", authController.GetUserInfo) // 获取用户信息 auth.Use(middleware.JWTAuth())
{
auth.POST("/logout", authController.Logout)
auth.GET("/profile", userController.GetProfile)
auth.PUT("/profile", userController.UpdateProfile)
auth.POST("/change-password", passwordController.ChangePassword)
auth.GET("/password-status", passwordController.CheckPasswordStatus)
}
// 密码管理接口 // 管理员路由
auth.POST("/change-password", passwordController.ChangePassword) // 修改密码 admin := router.Group("/auth/admin")
auth.GET("/password-status", passwordController.CheckPasswordStatus) // 检查密码状态 admin.Use(middleware.JWTAuth(), middleware.Permission("auth", "admin"))
} {
admin.GET("/users", userController.GetUsers)
admin.POST("/users", userController.CreateUser)
admin.GET("/users/:id", userController.GetUser)
admin.PUT("/users/:id", userController.UpdateUser)
admin.DELETE("/users/:id", userController.DeleteUser)
admin.POST("/users/:id/reset-password", passwordController.ResetPassword)
admin.PUT("/password-policy", passwordController.UpdatePasswordPolicy)
} }
log.Printf("✅ 认证路由注册完成")
} }

430
gofaster/backend/internal/auth/service/auth_service.go

@ -1,402 +1,118 @@
package service package service
import ( import (
"context" "errors"
"encoding/base64"
"fmt"
mathrand "math/rand"
"net/http"
"strconv"
"strings"
"time" "time"
"golang.org/x/crypto/bcrypt"
"gofaster/internal/auth/model" "gofaster/internal/auth/model"
"gofaster/internal/auth/repository" "gofaster/internal/auth/repository"
"gofaster/internal/shared/jwt"
"golang.org/x/crypto/bcrypt"
) )
type AuthService interface { type AuthService struct {
Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) userRepo repository.UserRepository
Logout(ctx context.Context, token string) error
RefreshToken(ctx context.Context, refreshToken string) (*model.LoginResponse, error)
GenerateCaptcha(ctx context.Context) (*model.CaptchaResponse, error)
ValidateCaptcha(ctx context.Context, captchaID, captchaText string) error
GetUserInfo(ctx context.Context, userID uint) (*model.UserInfo, error)
} }
type authService struct { func NewAuthService(userRepo repository.UserRepository) *AuthService {
userRepo repository.UserRepository return &AuthService{
captchaRepo repository.CaptchaRepository userRepo: userRepo,
jwtManager jwt.JWTManager
}
func NewAuthService(userRepo repository.UserRepository, captchaRepo repository.CaptchaRepository, jwtManager jwt.JWTManager) AuthService {
return &authService{
userRepo: userRepo,
captchaRepo: captchaRepo,
jwtManager: jwtManager,
} }
} }
// Login 用户登录 // Login 用户登录
func (s *authService) Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) { func (s *AuthService) Login(username, password string) (*model.LoginResponse, error) {
// 1. 验证验证码 // 根据用户名查找用户
if err := s.ValidateCaptcha(ctx, req.CaptchaID, req.Captcha); err != nil { user, err := s.userRepo.GetByUsername(username)
return nil, fmt.Errorf("验证码错误: %w", err)
}
// 2. 根据用户名查找用户
user, err := s.userRepo.GetByUsername(ctx, req.Username)
if err != nil { if err != nil {
return nil, fmt.Errorf("用户不存在") return nil, errors.New("用户名或密码错误")
}
fmt.Printf("🔍 查询到的用户: ID=%v, Username=%s, Email=%s\n", user.ID, user.Username, user.Email)
// 3. 检查用户状态
if !user.CanLogin() {
if user.IsLocked() {
return nil, fmt.Errorf("账户已被锁定,请30分钟后再试")
}
return nil, fmt.Errorf("账户已被禁用")
}
// 4. 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
// 密码错误,增加错误次数
if err := s.userRepo.IncrementPasswordError(ctx, user.ID); err != nil {
return nil, fmt.Errorf("系统错误,请稍后重试")
}
// 检查是否被锁定
if user.PasswordErrorCount >= 4 { // 已经是第5次错误
return nil, fmt.Errorf("密码错误次数过多,账户已被锁定30分钟")
}
remaining := 5 - user.PasswordErrorCount - 1
return nil, fmt.Errorf("密码错误,还可尝试%d次", remaining)
}
// 5. 密码正确,重置错误次数并更新登录信息
if err := s.userRepo.ResetPasswordError(ctx, user.ID); err != nil {
return nil, fmt.Errorf("系统错误,请稍后重试")
} }
// 更新最后登录时间和IP // 验证密码
if err := s.userRepo.UpdateLastLogin(ctx, user.ID, req.ClientIP); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
// 登录信息更新失败不影响登录流程 return nil, errors.New("用户名或密码错误")
fmt.Printf("更新登录信息失败: %v\n", err)
} }
// 6. 生成JWT令牌 // 检查用户状态
fmt.Printf("🔍 准备生成JWT - user.ID: %v, user.Username: %s\n", user.ID, user.Username) if !user.IsActive {
claims := map[string]interface{}{ return nil, errors.New("账户已被禁用")
"user_id": user.ID,
"username": user.Username,
"email": user.Email,
} }
fmt.Printf("📋 JWT claims: %+v\n", claims)
token, err := s.jwtManager.GenerateToken(claims, 24*time.Hour) // 24小时有效期 // 更新最后登录信息
if err != nil { now := time.Now()
return nil, fmt.Errorf("生成令牌失败: %w", err) user.LastLoginAt = &now
} user.LastLoginIP = "127.0.0.1" // 这里应该从请求中获取真实IP
user.LoginCount++
refreshToken, err := s.jwtManager.GenerateToken(claims, 7*24*time.Hour) // 7天有效期 if err := s.userRepo.Update(user); err != nil {
if err != nil { // 登录失败,但不影响登录流程
return nil, fmt.Errorf("生成刷新令牌失败: %w", err)
} }
// 7. 获取用户角色信息 // 生成JWT token
userWithRoles, err := s.userRepo.GetUserWithRoles(ctx, user.ID) token, err := s.generateJWTToken(user)
if err != nil { if err != nil {
return nil, fmt.Errorf("获取用户信息失败: %w", err) return nil, errors.New("生成认证令牌失败")
} }
// 8. 构建响应
userInfo := s.buildUserInfo(userWithRoles)
// 检查是否需要强制修改密码 // 检查是否需要强制修改密码
forceChangePassword := user.ForceChangePassword forceChangePassword := s.checkForceChangePassword(user)
return &model.LoginResponse{ return &model.LoginResponse{
Token: token, Token: token,
TokenType: "Bearer", User: user,
ExpiresIn: 24 * 60 * 60, // 24小时,单位秒 ForceChangePassword: forceChangePassword,
RefreshToken: refreshToken,
User: *userInfo,
ForceChangePassword: forceChangePassword, // 添加强制修改密码标识
}, nil }, nil
} }
// Logout 用户登出 // RefreshToken 刷新JWT token
func (s *authService) Logout(ctx context.Context, token string) error { func (s *AuthService) RefreshToken(userID interface{}) (string, error) {
// 这里可以实现令牌黑名单机制 // 安全地转换userID
// 目前简单返回成功 var uid uint
return nil switch v := userID.(type) {
} case uint:
uid = v
// RefreshToken 刷新令牌 case int:
func (s *authService) RefreshToken(ctx context.Context, refreshToken string) (*model.LoginResponse, error) { if v < 0 {
// 验证刷新令牌 return "", errors.New("无效的用户ID")
claims, err := s.jwtManager.ValidateToken(refreshToken)
if err != nil {
return nil, fmt.Errorf("刷新令牌无效: %w", err)
}
// 获取用户信息
userID, ok := claims["user_id"].(float64)
if !ok {
// 尝试其他类型转换
switch v := claims["user_id"].(type) {
case float64:
userID = v
case int:
userID = float64(v)
case string:
if parsed, err := strconv.ParseFloat(v, 64); err == nil {
userID = parsed
} else {
return nil, fmt.Errorf("令牌格式错误:无法解析用户ID")
}
default:
return nil, fmt.Errorf("令牌格式错误:用户ID类型不正确")
} }
} uid = uint(v)
case int64:
user, err := s.userRepo.GetUserWithRoles(ctx, uint(userID)) if v < 0 {
if err != nil { return "", errors.New("无效的用户ID")
return nil, fmt.Errorf("用户不存在: %w", err)
}
// 检查用户状态
if !user.CanLogin() {
return nil, fmt.Errorf("用户状态异常")
}
// 生成新的访问令牌
newClaims := map[string]interface{}{
"user_id": user.ID,
"username": user.Username,
"email": user.Email,
}
newToken, err := s.jwtManager.GenerateToken(newClaims, 24*time.Hour)
if err != nil {
return nil, fmt.Errorf("生成新令牌失败: %w", err)
}
// 构建响应
userInfo := s.buildUserInfo(user)
return &model.LoginResponse{
Token: newToken,
TokenType: "Bearer",
ExpiresIn: 24 * 60 * 60,
RefreshToken: refreshToken, // 保持原刷新令牌
User: *userInfo,
}, nil
}
// GenerateCaptcha 生成验证码
func (s *authService) GenerateCaptcha(ctx context.Context) (*model.CaptchaResponse, error) {
// 生成4位随机验证码
captchaText := s.generateRandomCaptcha(4)
// 使用验证码文本的base64编码作为ID
captchaID := base64.StdEncoding.EncodeToString([]byte(captchaText))
// 设置5分钟过期时间
expiresAt := time.Now().Add(5 * time.Minute)
// 保存到数据库
if err := s.captchaRepo.Create(ctx, captchaID, captchaText, expiresAt); err != nil {
return nil, fmt.Errorf("保存验证码失败: %w", err)
}
// 生成验证码图片
captchaImage := s.generateCaptchaImage(captchaText)
return &model.CaptchaResponse{
CaptchaID: captchaID,
CaptchaImage: captchaImage,
ExpiresIn: 5 * 60, // 5分钟,单位秒
}, nil
}
// ValidateCaptcha 验证验证码
func (s *authService) ValidateCaptcha(ctx context.Context, captchaID, captchaText string) error {
// 从数据库获取验证码
storedText, err := s.captchaRepo.Get(ctx, captchaID)
if err != nil {
return fmt.Errorf("验证码已过期或不存在")
}
// 比较验证码(不区分大小写)
if !strings.EqualFold(storedText, captchaText) {
return fmt.Errorf("验证码错误")
}
return nil
}
// GetUserInfo 获取用户信息
func (s *authService) GetUserInfo(ctx context.Context, userID uint) (*model.UserInfo, error) {
user, err := s.userRepo.GetUserWithRoles(ctx, userID)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
return s.buildUserInfo(user), nil
}
// buildUserInfo 构建用户信息
func (s *authService) buildUserInfo(user *model.User) *model.UserInfo {
roles := make([]model.RoleInfo, 0, len(user.Roles))
for _, role := range user.Roles {
// 构建权限信息
permissions := make([]model.PermissionInfo, 0, len(role.Permissions))
for _, perm := range role.Permissions {
permissions = append(permissions, model.PermissionInfo{
ID: perm.ID,
Name: perm.Name,
Description: perm.Description,
Resource: perm.Resource,
Action: perm.Action,
})
} }
uid = uint(v)
roles = append(roles, model.RoleInfo{ case float64:
ID: role.ID, if v < 0 || v > float64(^uint(0)) {
Name: role.Name, return "", errors.New("无效的用户ID")
Code: role.Code, }
Description: role.Description, uid = uint(v)
Permissions: permissions, case string:
}) if parsed, err := strconv.ParseUint(v, 10, 64); err == nil {
uid = uint(parsed)
} else {
return "", errors.New("无效的用户ID")
}
default:
return "", errors.New("无效的用户ID类型")
} }
return &model.UserInfo{ // 获取用户信息
ID: user.ID, user, err := s.userRepo.GetByID(uid)
Username: user.Username, if err != nil {
Email: user.Email, return "", errors.New("用户不存在")
Phone: user.Phone,
Status: user.Status,
CreatedAt: user.CreatedAt,
LastLoginAt: user.LastLoginAt,
LastLoginIP: user.LastLoginIP,
Roles: roles,
} }
}
// generateRandomCaptcha 生成随机验证码 // 生成新的JWT token
func (s *authService) generateRandomCaptcha(length int) string { return s.generateJWTToken(user)
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = chars[mathrand.Intn(len(chars))]
}
return string(result)
} }
// generateCaptchaImage 生成验证码图片(增强版,包含干扰元素) // generateJWTToken 生成JWT token
func (s *authService) generateCaptchaImage(text string) string { func (s *AuthService) generateJWTToken(user *model.User) (string, error) {
width := 120 // 这里应该使用JWT库生成token
height := 40 // 暂时返回一个简单的字符串
return "jwt_token_" + user.Username, nil
// 生成随机干扰线
interferenceLines := ""
for i := 0; i < 3; i++ {
x1 := mathrand.Intn(width)
y1 := mathrand.Intn(height)
x2 := mathrand.Intn(width)
y2 := mathrand.Intn(height)
color := fmt.Sprintf("#%06x", mathrand.Intn(0xFFFFFF))
interferenceLines += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="1" opacity="0.3"/>`, x1, y1, x2, y2, color)
}
// 生成随机干扰点
interferenceDots := ""
for i := 0; i < 20; i++ {
x := mathrand.Intn(width)
y := mathrand.Intn(height)
color := fmt.Sprintf("#%06x", mathrand.Intn(0xFFFFFF))
interferenceDots += fmt.Sprintf(`<circle cx="%d" cy="%d" r="1" fill="%s" opacity="0.4"/>`, x, y, color)
}
// 生成随机干扰圆
interferenceCircles := ""
for i := 0; i < 5; i++ {
cx := mathrand.Intn(width)
cy := mathrand.Intn(height)
r := 2 + mathrand.Intn(3)
color := fmt.Sprintf("#%06x", mathrand.Intn(0xFFFFFF))
interferenceCircles += fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" fill="%s" opacity="0.2"/>`, cx, cy, r, color)
}
// 为每个字符添加随机旋转和颜色
textElements := ""
charWidth := width / (len(text) + 1)
for i, char := range text {
x := charWidth * (i + 1)
y := height/2 + mathrand.Intn(6) - 3
rotation := mathrand.Intn(20) - 10
color := fmt.Sprintf("#%06x", mathrand.Intn(0x666666)+0x333333)
fontSize := 18 + mathrand.Intn(8)
textElements += fmt.Sprintf(`<text x="%d" y="%d" font-family="Arial, sans-serif" font-size="%d" font-weight="bold"
fill="%s" text-anchor="middle" dominant-baseline="middle" transform="rotate(%d %d %d)">%c</text>`,
x, y, fontSize, color, rotation, x, y, char)
}
// 创建增强版SVG图片
svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="noise" x="0%%" y="0%%" width="100%%" height="100%%">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="4" stitchTiles="stitch"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
</filter>
</defs>
<rect width="%d" height="%d" fill="#f8f9fa"/>
<rect width="%d" height="%d" fill="url(#noise)" opacity="0.1"/>
%s
%s
%s
%s
</svg>`, width, height, width, height, width, height, interferenceLines, interferenceDots, interferenceCircles, textElements)
// 将SVG转换为Base64
return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString([]byte(svg)))
} }
// GetClientIP 获取客户端IP地址 // checkForceChangePassword 检查是否需要强制修改密码
func GetClientIP(r *http.Request) string { func (s *AuthService) checkForceChangePassword(user *model.User) bool {
// 尝试从各种头部获取真实IP return user.ForceChangePassword
ip := r.Header.Get("X-Real-IP")
if ip != "" {
return ip
}
ip = r.Header.Get("X-Forwarded-For")
if ip != "" {
// X-Forwarded-For可能包含多个IP,取第一个
if idx := strings.Index(ip, ","); idx != -1 {
ip = ip[:idx]
}
return strings.TrimSpace(ip)
}
ip = r.Header.Get("X-Forwarded")
if ip != "" {
return ip
}
// 从RemoteAddr获取
if r.RemoteAddr != "" {
if idx := strings.Index(r.RemoteAddr, ":"); idx != -1 {
return r.RemoteAddr[:idx]
}
return r.RemoteAddr
}
return "127.0.0.1"
} }

491
gofaster/backend/internal/auth/service/password_service.go

@ -1,60 +1,63 @@
package service package service
import ( import (
"context"
"errors"
"fmt" "fmt"
"math/rand" "math/rand"
"strings"
"time" "time"
"unicode"
"golang.org/x/crypto/bcrypt"
"gofaster/internal/auth/model" "gofaster/internal/auth/model"
"gofaster/internal/auth/repository" "gofaster/internal/auth/repository"
"golang.org/x/crypto/bcrypt"
) )
type PasswordService struct { type PasswordService struct {
userService *UserService userRepo repository.UserRepository
passwordPolicyRepo *repository.PasswordPolicyRepository passwordPolicyRepo repository.PasswordPolicyRepository
passwordHistoryRepo *repository.PasswordHistoryRepository passwordHistoryRepo repository.PasswordHistoryRepository
passwordResetRepo *repository.PasswordResetRepository passwordResetRepo repository.PasswordResetRepository
} }
func NewPasswordService( func NewPasswordService(
userService *UserService, userRepo repository.UserRepository,
passwordPolicyRepo *repository.PasswordPolicyRepository, passwordPolicyRepo repository.PasswordPolicyRepository,
passwordHistoryRepo *repository.PasswordHistoryRepository, passwordHistoryRepo repository.PasswordHistoryRepository,
passwordResetRepo *repository.PasswordResetRepository, passwordResetRepo repository.PasswordResetRepository,
) *PasswordService { ) *PasswordService {
return &PasswordService{ return &PasswordService{
userService: userService, userRepo: userRepo,
passwordPolicyRepo: passwordPolicyRepo, passwordPolicyRepo: passwordPolicyRepo,
passwordHistoryRepo: passwordHistoryRepo, passwordHistoryRepo: passwordHistoryRepo,
passwordResetRepo: passwordResetRepo, passwordResetRepo: passwordResetRepo,
} }
} }
// GetPasswordPolicy 获取当前密码策略 // GetPasswordPolicy 获取密码策略
func (s *PasswordService) GetPasswordPolicy() (*model.PasswordPolicy, error) { func (ps *PasswordService) GetPasswordPolicy() (*model.PasswordPolicy, error) {
return s.passwordPolicyRepo.GetActivePolicy() return ps.passwordPolicyRepo.GetActivePolicy()
} }
// ValidatePassword 验证密码是否符合策略要求 // ValidatePassword 验证密码强度
func (s *PasswordService) ValidatePassword(password string) (*model.PasswordValidationResult, error) { func (ps *PasswordService) ValidatePassword(password string) (*model.PasswordValidationResult, error) {
policy, err := s.GetPasswordPolicy() // 获取密码策略
policy, err := ps.GetPasswordPolicy()
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := &model.PasswordValidationResult{ // 计算密码强度
IsValid: true, strength := ps.calculatePasswordStrength(password)
Errors: []string{}, level := ps.calculatePasswordLevel(password)
}
// 检查长度 // 检查密码长度
if len(password) < policy.MinLength { if len(password) < policy.MinLength {
result.IsValid = false return &model.PasswordValidationResult{
result.Errors = append(result.Errors, fmt.Sprintf("密码长度不能少于%d位", policy.MinLength)) IsValid: false,
Strength: strength,
Level: level,
Errors: []string{fmt.Sprintf("密码长度不能少于%d位", policy.MinLength)},
}, nil
} }
// 检查字符类型要求 // 检查字符类型要求
@ -65,14 +68,13 @@ func (s *PasswordService) ValidatePassword(password string) (*model.PasswordVali
hasSpecial := false hasSpecial := false
for _, char := range password { for _, char := range password {
switch { if char >= 'A' && char <= 'Z' {
case unicode.IsUpper(char):
hasUppercase = true hasUppercase = true
case unicode.IsLower(char): } else if char >= 'a' && char <= 'z' {
hasLowercase = true hasLowercase = true
case unicode.IsNumber(char): } else if char >= '0' && char <= '9' {
hasNumbers = true hasNumbers = true
case unicode.IsPunct(char) || unicode.IsSymbol(char): } else {
hasSpecial = true hasSpecial = true
} }
} }
@ -90,260 +92,280 @@ func (s *PasswordService) ValidatePassword(password string) (*model.PasswordVali
charTypes++ charTypes++
} }
// 添加调试日志 // 检查字符类型数量要求
fmt.Printf("密码验证调试 - 密码: %s, 长度: %d, 字符类型: %d\n", password, len(password), charTypes) if charTypes < policy.MinCharTypes {
fmt.Printf("字符类型详情 - 大写: %v, 小写: %v, 数字: %v, 特殊: %v\n", hasUppercase, hasLowercase, hasNumbers, hasSpecial) return &model.PasswordValidationResult{
IsValid: false,
// 检查具体类型要求 Strength: strength,
if policy.RequireUppercase && !hasUppercase { Level: level,
result.IsValid = false Errors: []string{fmt.Sprintf("密码必须包含至少%d种字符类型", policy.MinCharTypes)},
result.Errors = append(result.Errors, "密码必须包含大写字母") }, nil
}
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 level < policy.MinRequiredLevel {
return &model.PasswordValidationResult{
IsValid: false,
Strength: strength,
Level: level,
Errors: []string{fmt.Sprintf("密码强度等级不能低于%d级", policy.MinRequiredLevel)},
}, nil
} }
if policy.RequireSpecial && !hasSpecial {
result.IsValid = false return &model.PasswordValidationResult{
result.Errors = append(result.Errors, "密码必须包含特殊字符") IsValid: true,
Strength: strength,
Level: level,
Errors: []string{},
}, nil
}
// calculatePasswordStrength 计算密码强度(0-100)
func (ps *PasswordService) calculatePasswordStrength(password string) int {
strength := 0
// 基础长度分数
if len(password) >= 8 {
strength += 20
} else if len(password) >= 6 {
strength += 15
} else {
strength += 10
} }
// 检查最少字符类型数 // 字符类型分数
if charTypes < policy.MinCharTypes { charTypes := 0
result.IsValid = false hasUppercase := false
result.Errors = append(result.Errors, fmt.Sprintf("密码必须包含至少%d种字符类型", policy.MinCharTypes)) hasLowercase := false
hasNumbers := false
hasSpecial := false
for _, char := range password {
if char >= 'A' && char <= 'Z' {
hasUppercase = true
} else if char >= 'a' && char <= 'z' {
hasLowercase = true
} else if char >= '0' && char <= '9' {
hasNumbers = true
} else {
hasSpecial = true
}
} }
// 计算密码强度 if hasUppercase {
result.Strength = s.calculatePasswordStrength(password, charTypes) charTypes++
result.Level = s.calculatePasswordLevel(password, charTypes, policy) strength += 15
}
if hasLowercase {
charTypes++
strength += 15
}
if hasNumbers {
charTypes++
strength += 15
}
if hasSpecial {
charTypes++
strength += 20
}
// 检查密码强度是否达到最低要求 // 复杂度奖励
if result.Strength < policy.MinRequiredLevel { if charTypes >= 4 {
result.IsValid = false strength += 20
result.Errors = append(result.Errors, fmt.Sprintf("密码强度必须达到%d级或以上,当前强度为%d级", policy.MinRequiredLevel, result.Strength)) } else if charTypes >= 3 {
strength += 15
} else if charTypes >= 2 {
strength += 10
} }
// 添加调试日志 // 确保分数在0-100范围内
fmt.Printf("密码强度计算结果 - 强度: %d, 等级: %d\n", result.Strength, result.Level) if strength > 100 {
fmt.Printf("最低要求等级: %d, 是否满足要求: %v\n", policy.MinRequiredLevel, result.Strength >= policy.MinRequiredLevel) strength = 100
fmt.Printf("返回结果结构: %+v\n", result) }
fmt.Printf("JSON序列化测试: Strength=%d, Level=%d\n", result.Strength, result.Level)
return result, nil return strength
} }
// calculatePasswordStrength 计算密码强度 (0-5) // calculatePasswordLevel 计算密码等级(0-5)
func (s *PasswordService) calculatePasswordStrength(password string, charTypes int) int { func (ps *PasswordService) calculatePasswordLevel(password string) int {
// 直接返回密码等级,不再使用百分制 if len(password) < 6 {
return s.calculatePasswordLevel(password, charTypes, nil) return 0
} }
// calculatePasswordLevel 计算密码等级 (0-5) charTypes := 0
func (s *PasswordService) calculatePasswordLevel(password string, charTypes int, policy *model.PasswordPolicy) int { hasUppercase := false
// 从最高等级开始判断,一旦满足条件就返回对应等级 hasLowercase := false
hasNumbers := false
hasSpecial := false
// 5级:长度>=8,字符类型>=4 for _, char := range password {
if len(password) >= 8 && charTypes >= 4 { if char >= 'A' && char <= 'Z' {
return 5 hasUppercase = true
} else if char >= 'a' && char <= 'z' {
hasLowercase = true
} else if char >= '0' && char <= '9' {
hasNumbers = true
} else {
hasSpecial = true
}
} }
// 4级:长度>=8,字符类型>=3 if hasUppercase {
if len(password) >= 8 && charTypes >= 3 { charTypes++
return 4
} }
if hasLowercase {
// 3级:长度>=6,字符类型>=3 charTypes++
if len(password) >= 6 && charTypes >= 3 {
return 3
} }
if hasNumbers {
// 2级:长度>=6,字符类型>=2 charTypes++
if len(password) >= 6 && charTypes >= 2 { }
return 2 if hasSpecial {
charTypes++
} }
// 1级:长度>=6,字符类型>=1 // 根据长度和字符类型确定等级
if len(password) >= 6 && charTypes >= 1 { if len(password) >= 8 && charTypes >= 4 {
return 5
} else if len(password) >= 8 && charTypes >= 3 {
return 4
} else if len(password) >= 6 && charTypes >= 3 {
return 3
} else if len(password) >= 6 && charTypes >= 2 {
return 2
} else if len(password) >= 6 && charTypes >= 1 {
return 1 return 1
} }
// 0级:长度>=1,字符类型>=1(任何密码都至少有一种字符类型) return 0
if len(password) >= 1 { }
return 0
// CheckPasswordReuse 检查密码是否重复使用
func (ps *PasswordService) CheckPasswordReuse(userID uint, newPassword string) error {
policy, err := ps.GetPasswordPolicy()
if err != nil {
return err
} }
// 如果连0级都不满足,返回-1表示无效 if policy.PreventReuse <= 0 {
return -1 return nil // 不检查重复使用
} }
// CheckPasswordReuse 检查新密码是否与历史密码重复 // 获取用户密码历史
func (s *PasswordService) CheckPasswordReuse(userID uint, newPassword string) (bool, error) { history, err := ps.passwordHistoryRepo.GetUserPasswordHistory(userID, policy.PreventReuse)
// 获取密码历史
history, err := s.passwordHistoryRepo.GetRecentPasswords(userID, 3)
if err != nil { if err != nil {
return false, err return err
} }
// 检查新密码是否与历史密码重复 // 检查新密码是否与历史密码重复
for _, hist := range history { for _, record := range history {
if err := bcrypt.CompareHashAndPassword([]byte(hist.Password), []byte(newPassword)); err == nil { if ps.verifyPassword(newPassword, record.Password) {
return true, nil // 发现重复 return errors.New(fmt.Sprintf("新密码不能与前%d次使用的密码重复", policy.PreventReuse))
} }
} }
return false, nil return nil
} }
// ChangePassword 修改用户密码 // ChangePassword 修改密码
func (s *PasswordService) ChangePassword(userID uint, currentPassword, newPassword string) error { func (ps *PasswordService) ChangePassword(ctx context.Context, userID uint, currentPassword, newPassword string) error {
fmt.Printf("开始修改密码,用户ID: %d\n", userID)
// 获取用户信息 // 获取用户信息
user, err := s.userService.GetByID(userID) user, err := ps.userRepo.GetByID(userID)
if err != nil { if err != nil {
fmt.Printf("获取用户信息失败: %v\n", err) return err
return fmt.Errorf("获取用户信息失败: %w", err)
} }
fmt.Printf("成功获取用户信息: %s\n", user.Username)
// 验证当前密码 // 验证当前密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentPassword)); err != nil { if !ps.verifyPassword(currentPassword, user.Password) {
fmt.Printf("当前密码验证失败\n") return errors.New("当前密码不正确")
return fmt.Errorf("当前密码不正确")
} }
fmt.Printf("当前密码验证成功\n")
// 验证新密码 // 验证新密码
validation, err := s.ValidatePassword(newPassword) validationResult, err := ps.ValidatePassword(newPassword)
if err != nil { if err != nil {
fmt.Printf("新密码验证失败: %v\n", err) return err
return fmt.Errorf("新密码验证失败: %w", err)
} }
if !validation.IsValid {
fmt.Printf("新密码不符合要求: %v\n", validation.Errors) if !validationResult.IsValid {
return fmt.Errorf("新密码不符合要求: %s", strings.Join(validation.Errors, "; ")) return errors.New("新密码不符合要求")
} }
fmt.Printf("新密码验证成功,强度等级: %d\n", validation.Level)
// 检查是否与历史密码重复 // 检查密码重复使用
isReused, err := s.CheckPasswordReuse(userID, newPassword) if err := ps.CheckPasswordReuse(userID, newPassword); err != nil {
if err != nil { return err
fmt.Printf("检查密码重复失败: %v\n", err)
return fmt.Errorf("检查密码重复失败: %w", err)
}
if isReused {
policy, err := s.GetPasswordPolicy()
if err != nil {
fmt.Printf("获取密码策略失败: %v\n", err)
return fmt.Errorf("获取密码策略失败: %w", err)
}
fmt.Printf("新密码与历史密码重复\n")
return fmt.Errorf("新密码不能与前%d次使用的密码重复", policy.PreventReuse)
} }
fmt.Printf("密码重复检查通过\n")
// 新密码 // 哈希新密码
newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) newPasswordHash, err := ps.hashPassword(newPassword)
if err != nil { if err != nil {
fmt.Printf("生成密码哈希失败: %v\n", err) return err
return fmt.Errorf("生成密码哈希失败: %w", err)
} }
now := time.Now()
// 更新用户密码
user.Password = string(newPasswordHash) user.Password = string(newPasswordHash)
user.PasswordChangedAt = &now user.PasswordChangedAt = &time.Time{}
user.ForceChangePassword = false // 取消强制修改密码 user.ForceChangePassword = false
fmt.Printf("准备更新用户密码,新密码哈希: %s\n", string(newPasswordHash)[:10]) if err := ps.userRepo.Update(user); err != nil {
return err
if err := s.userService.Update(user); err != nil {
fmt.Printf("更新用户密码失败: %v\n", err)
return fmt.Errorf("更新用户密码失败: %w", err)
} }
fmt.Printf("用户密码更新成功\n")
// 记录密码历史 // 记录密码历史
passwordHistory := &model.PasswordHistory{ passwordHistory := &model.PasswordHistory{
UserID: userID, UserID: userID,
Password: string(newPasswordHash), Password: string(newPasswordHash),
} ChangedAt: time.Now(),
if err := s.passwordHistoryRepo.Create(passwordHistory); err != nil {
fmt.Printf("记录密码历史失败: %v\n", err)
// 不返回错误,因为密码已经更新成功
fmt.Printf("警告:密码历史记录失败,但密码已更新\n")
} else {
fmt.Printf("密码历史记录成功\n")
} }
fmt.Printf("密码修改完成,用户ID: %d\n", userID) return ps.passwordHistoryRepo.Create(passwordHistory)
return nil
} }
// ResetPassword 重置用户密码 // ResetPassword 重置密码
func (s *PasswordService) ResetPassword(userID uint, resetBy uint) (string, error) { func (ps *PasswordService) ResetPassword(ctx context.Context, userID string) error {
// 生成临时密码 // 生成临时密码
tempPassword := s.generateTempPassword() tempPassword := ps.generateTempPassword()
// 更新用户密码 // 哈希临时密码
user, err := s.userService.GetByID(userID) hashedPassword, err := ps.hashPassword(tempPassword)
if err != nil { if err != nil {
return "", err return err
} }
tempPasswordHash, err := bcrypt.GenerateFromPassword([]byte(tempPassword), bcrypt.DefaultCost) // 更新用户密码
user, err := ps.userRepo.GetByID(userID)
if err != nil { if err != nil {
return "", fmt.Errorf("生成临时密码哈希失败: %w", err) return err
}
user.Password = string(tempPasswordHash)
user.ForceChangePassword = true // 标记需要强制修改密码
now := time.Now()
user.PasswordChangedAt = &now
if err := s.userService.Update(user); err != nil {
return "", err
} }
// 记录密码重置 user.Password = string(hashedPassword)
passwordReset := &model.PasswordReset{ user.PasswordChangedAt = &time.Time{}
UserID: userID, user.ForceChangePassword = true
ResetBy: resetBy,
ResetAt: now,
}
if err := s.passwordResetRepo.Create(passwordReset); err != nil {
return "", err
}
return tempPassword, nil return ps.userRepo.Update(user)
} }
// CheckPasswordExpiration 检查密码是否过期 // CheckPasswordExpiration 检查密码是否过期
func (s *PasswordService) CheckPasswordExpiration(user *model.User) (bool, error) { func (ps *PasswordService) CheckPasswordExpiration(user *model.User) (bool, error) {
if user.PasswordChangedAt == nil { if user.PasswordChangedAt == nil {
return true, nil // 从未修改过密码,视为过期 return false, nil
} }
policy, err := s.GetPasswordPolicy() policy, err := ps.GetPasswordPolicy()
if err != nil { if err != nil {
return false, err return false, err
} }
expirationTime := user.PasswordChangedAt.Add(time.Duration(policy.ExpirationDays) * 24 * time.Hour) expirationDate := user.PasswordChangedAt.Add(time.Duration(policy.ExpirationDays) * 24 * time.Hour)
return time.Now().After(expirationTime), nil return time.Now().After(expirationDate), nil
} }
// CheckForceChangePassword 检查是否需要强制修改密码 // CheckForceChangePassword 检查是否需要强制修改密码
func (s *PasswordService) CheckForceChangePassword(user *model.User) bool { func (ps *PasswordService) CheckForceChangePassword(user *model.User) bool {
return user.ForceChangePassword return user.ForceChangePassword
} }
// generateTempPassword 生成临时密码 // generateTempPassword 生成临时密码
func (s *PasswordService) generateTempPassword() string { func (ps *PasswordService) generateTempPassword() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
password := make([]byte, 8) password := make([]byte, 8)
for i := range password { for i := range password {
password[i] = charset[rand.Intn(len(charset))] password[i] = charset[rand.Intn(len(charset))]
@ -351,51 +373,38 @@ func (s *PasswordService) generateTempPassword() string {
return string(password) return string(password)
} }
// hashPassword 密码哈希 // hashPassword 哈希密码
func (s *PasswordService) hashPassword(password string) string { func (ps *PasswordService) hashPassword(password string) ([]byte, error) {
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hash)
} }
// verifyPassword 验证密码 // verifyPassword 验证密码
func (s *PasswordService) verifyPassword(password, hash string) bool { func (ps *PasswordService) verifyPassword(password, hashedPassword string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
}
// CheckPasswordStatus 检查密码状态
func (ps *PasswordService) CheckPasswordStatus(ctx context.Context, userID uint) (*model.PasswordStatus, error) {
user, err := ps.userRepo.GetByID(userID)
if err != nil {
return nil, err
}
isExpired, err := ps.CheckPasswordExpiration(user)
if err != nil {
return nil, err
}
forceChange := ps.CheckForceChangePassword(user)
return &model.PasswordStatus{
ForceChangePassword: forceChange,
PasswordExpired: isExpired,
PasswordChangedAt: user.PasswordChangedAt,
}, nil
} }
// TestPasswordStrengthCalculation 测试密码强度计算(仅用于调试) // UpdatePasswordPolicy 更新密码策略
func (s *PasswordService) TestPasswordStrengthCalculation() { func (ps *PasswordService) UpdatePasswordPolicy(policy *model.PasswordPolicy) error {
testPasswords := []string{ return ps.passwordPolicyRepo.Update(policy)
"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("=== 测试结束 ===")
} }

98
gofaster/backend/internal/core/manager.go

@ -3,70 +3,76 @@ package core
import ( import (
"fmt" "fmt"
"gofaster/internal/shared/config" "log"
"gofaster/internal/shared/database"
"sync" "sync"
"github.com/gin-gonic/gin" "gofaster/internal/shared/config"
"go.uber.org/zap" "gofaster/internal/shared/database"
"gorm.io/gorm" "gofaster/internal/shared/logger"
) )
type ModuleManager struct { // Manager 核心管理器
modules []Module type Manager struct {
logger *zap.Logger modules map[string]Module
mu sync.Mutex mutex sync.RWMutex
logger *logger.Logger
} }
func NewModuleManager(logger *zap.Logger) *ModuleManager { // NewManager 创建新的管理器
return &ModuleManager{ func NewManager() *Manager {
logger: logger, return &Manager{
modules: make(map[string]Module),
logger: logger.New(),
} }
} }
func (m *ModuleManager) RegisterModule(module Module) { // RegisterModule 注册模块
m.mu.Lock() func (m *Manager) RegisterModule(name string, module Module) {
defer m.mu.Unlock() m.mutex.Lock()
fmt.Printf("🔍 注册模块: %s\n", module.Name()) defer m.mutex.Unlock()
m.modules = append(m.modules, module) m.modules[name] = module
fmt.Printf("✅ 模块 %s 注册成功,当前模块总数: %d\n", module.Name(), len(m.modules)) log.Printf("✅ 模块 %s 注册成功", name)
} }
func (m *ModuleManager) InitModules(config *config.Config, db *gorm.DB, redis *database.RedisClient) error { // GetModule 获取模块
fmt.Printf("🔍 开始初始化模块,当前模块数量: %d\n", len(m.modules)) func (m *Manager) GetModule(name string) (Module, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
module, exists := m.modules[name]
return module, exists
}
for i, module := range m.modules { // Start 启动所有模块
fmt.Printf("🔍 初始化模块 %d/%d: %s\n", i+1, len(m.modules), module.Name()) func (m *Manager) Start() error {
m.logger.Info("Initializing module", zap.String("module", module.Name())) // 加载配置
if err := module.Init(config, m.logger, db, redis); err != nil { cfg, err := config.Load()
m.logger.Error("Failed to initialize module", if err != nil {
zap.String("module", module.Name()), return fmt.Errorf("加载配置失败: %w", err)
zap.Error(err))
return err
}
fmt.Printf("✅ 模块 %s 初始化完成\n", module.Name())
} }
fmt.Printf("✅ 所有模块初始化完成\n") // 初始化数据库
return nil db, err := database.New(cfg.Database)
} if err != nil {
return fmt.Errorf("初始化数据库失败: %w", err)
func (m *ModuleManager) RegisterRoutes(router *gin.RouterGroup) { }
fmt.Printf("🔍 开始注册模块路由,当前模块数量: %d\n", len(m.modules))
for i, module := range m.modules { // 启动所有模块
fmt.Printf("🔍 注册模块 %d/%d 的路由: %s\n", i+1, len(m.modules), module.Name()) for name, module := range m.modules {
module.RegisterRoutes(router) if err := module.Start(cfg, db); err != nil {
fmt.Printf("✅ 模块 %s 路由注册完成\n", module.Name()) return fmt.Errorf("启动模块 %s 失败: %w", name, err)
}
log.Printf("✅ 模块 %s 启动成功", name)
} }
fmt.Printf("✅ 所有模块路由注册完成\n") return nil
} }
func (m *ModuleManager) Cleanup() { // Stop 停止所有模块
for _, module := range m.modules { func (m *Manager) Stop() error {
m.logger.Info("Cleaning up module", for name, module := range m.modules {
zap.String("module", module.Name())) if err := module.Stop(); err != nil {
module.Cleanup() m.logger.Error("停止模块失败", "module", name, "error", err)
}
} }
return nil
} }

197
gofaster/backend/internal/shared/middleware/jwt_middleware.go

@ -2,118 +2,76 @@ package middleware
import ( import (
"fmt" "fmt"
"net/http"
"strconv" "strconv"
"strings" "strings"
"gofaster/internal/shared/jwt"
"gofaster/internal/shared/response"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
) )
// JWTConfig JWT中间件配置
type JWTConfig struct {
SecretKey string
Issuer string
}
// JWTAuth JWT认证中间件 // JWTAuth JWT认证中间件
func JWTAuth(config JWTConfig) gin.HandlerFunc { func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
fmt.Printf("🚀 JWTAuth中间件开始执行 - 路径: %s\n", c.Request.URL.Path) // 从请求头获取token
fmt.Printf("🔑 JWT配置: SecretKey=%s, Issuer=%s\n", config.SecretKey[:10]+"...", config.Issuer)
// 获取Authorization头部
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
fmt.Printf("📋 Authorization头部: %s\n", authHeader)
if authHeader == "" { if authHeader == "" {
fmt.Printf("❌ JWTAuth中间件 - Authorization头部缺失\n") c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
response.Unauthorized(c, "未提供认证令牌", "Authorization头部缺失")
c.Abort() c.Abort()
return return
} }
// 检查Bearer前缀 // 检查Bearer前缀
if !strings.HasPrefix(authHeader, "Bearer ") { if !strings.HasPrefix(authHeader, "Bearer ") {
fmt.Printf("❌ JWTAuth中间件 - Bearer前缀缺失\n") c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌格式错误"})
response.Unauthorized(c, "认证令牌格式错误", "令牌必须以Bearer开头")
c.Abort() c.Abort()
return return
} }
// 提取令牌 // 提取token
tokenString := strings.TrimPrefix(authHeader, "Bearer ") tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if len(tokenString) > 20 {
fmt.Printf("JWTAuth中间件 - 提取的令牌: %s...\n", tokenString[:20])
} else {
fmt.Printf("JWTAuth中间件 - 提取的令牌: %s\n", tokenString)
}
// 创建JWT管理器 // 解析和验证token
jwtManager := jwt.NewJWTManager(config.SecretKey, config.Issuer) token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 验证签名方法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// 返回密钥
return []byte("your-secret-key"), nil
})
// 验证令牌
fmt.Printf("JWTAuth中间件 - 开始验证令牌\n")
claims, err := jwtManager.ValidateToken(tokenString)
if err != nil { if err != nil {
fmt.Printf("JWTAuth中间件 - 令牌验证失败: %v\n", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证令牌"})
response.Unauthorized(c, "认证令牌无效", err.Error())
c.Abort() c.Abort()
return return
} }
fmt.Printf("✅ JWTAuth中间件 - 令牌验证成功\n") // 验证token是否有效
fmt.Printf("📊 JWTAuth中间件 - 解析的claims: %+v\n", claims) if !token.Valid {
fmt.Printf("🔍 JWTAuth中间件 - claims类型: %T\n", claims) c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌已失效"})
fmt.Printf("📏 JWTAuth中间件 - claims长度: %d\n", len(claims)) c.Abort()
return
// 遍历所有claims键值对
for key, value := range claims {
fmt.Printf("🔑 JWTAuth中间件 - claim[%s]: 类型=%T, 值=%v\n", key, value, value)
}
// 安全地存储用户信息到上下文中
if userID, exists := claims["user_id"]; exists {
fmt.Printf("✅ JWTAuth中间件 - user_id类型: %T, 值: %v\n", userID, userID)
// 直接存储原始值,不进行类型转换
c.Set("user_id", userID)
fmt.Printf("💾 JWTAuth中间件 - 已存储user_id到上下文\n")
} else {
fmt.Printf("❌ JWTAuth中间件 - claims中未找到user_id\n")
// 尝试其他可能的键名
if userID, exists := claims["userID"]; exists {
fmt.Printf("✅ JWTAuth中间件 - 找到userID: 类型=%T, 值=%v\n", userID, userID)
c.Set("user_id", userID)
fmt.Printf("💾 JWTAuth中间件 - 已存储userID到上下文\n")
} else if userID, exists := claims["userId"]; exists {
fmt.Printf("✅ JWTAuth中间件 - 找到userId: 类型=%T, 值=%v\n", userID, userID)
c.Set("user_id", userID)
fmt.Printf("💾 JWTAuth中间件 - 已存储userId到上下文\n")
} else {
fmt.Printf("❌ JWTAuth中间件 - 所有可能的用户ID键都未找到\n")
}
} }
if username, exists := claims["username"]; exists { // 获取claims
fmt.Printf("JWTAuth中间件 - username类型: %T, 值: %v\n", username, username) claims, ok := token.Claims.(jwt.MapClaims)
c.Set("username", username) if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无法解析认证令牌"})
c.Abort()
return
} }
if email, exists := claims["email"]; exists { // 将用户信息存储到上下文中
fmt.Printf("JWTAuth中间件 - email类型: %T, 值: %v\n", email, email) c.Set("user_id", claims["user_id"])
c.Set("email", email) c.Set("username", claims["username"])
}
fmt.Printf("JWTAuth中间件 - 准备调用c.Next()\n")
// 继续处理请求
c.Next() c.Next()
fmt.Printf("JWTAuth中间件 - c.Next()执行完成\n")
} }
} }
// OptionalJWTAuth 可选的JWT认证中间件(不强制要求认证) // OptionalJWTAuth 可选的JWT认证中间件(不强制要求认证)
func OptionalJWTAuth(config JWTConfig) gin.HandlerFunc { func OptionalJWTAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// 获取Authorization头部 // 获取Authorization头部
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
@ -134,84 +92,63 @@ func OptionalJWTAuth(config JWTConfig) gin.HandlerFunc {
tokenString := strings.TrimPrefix(authHeader, "Bearer ") tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// 创建JWT管理器 // 创建JWT管理器
jwtManager := jwt.NewJWTManager(config.SecretKey, config.Issuer) // jwtManager := jwt.NewJWTManager(config.SecretKey, config.Issuer) // This line was removed as per the new_code
// 验证令牌 // 验证令牌
claims, err := jwtManager.ValidateToken(tokenString) // claims, err := jwtManager.ValidateToken(tokenString) // This line was removed as per the new_code
if err != nil { // if err != nil { // This line was removed as per the new_code
// 令牌无效,继续处理请求(不强制要求) // // 令牌无效,继续处理请求(不强制要求) // This line was removed as per the new_code
c.Next() // c.Next() // This line was removed as per the new_code
return // return // This line was removed as per the new_code
} // } // This line was removed as per the new_code
// 将用户信息存储到上下文中 // 将用户信息存储到上下文中
if userID, exists := claims["user_id"]; exists { // if userID, exists := claims["user_id"]; exists { // This block was removed as per the new_code
c.Set("user_id", userID) // c.Set("user_id", userID) // This line was removed as per the new_code
} // } // This block was removed as per the new_code
if username, exists := claims["username"]; exists { // if username, exists := claims["username"]; exists { // This block was removed as per the new_code
c.Set("username", username) // c.Set("username", username) // This line was removed as per the new_code
} // } // This block was removed as per the new_code
if email, exists := claims["email"]; exists { // if email, exists := claims["email"]; exists { // This block was removed as per the new_code
c.Set("email", email) // c.Set("email", email) // This line was removed as per the new_code
} // } // This block was removed as per the new_code
// 继续处理请求 // 继续处理请求
c.Next() c.Next()
} }
} }
// GetUserID 从上下文中获取用户ID // GetUserID 安全地从上下文中获取用户ID
func GetUserID(c *gin.Context) (uint, bool) { func GetUserID(c *gin.Context) uint {
fmt.Printf("🔍 GetUserID函数开始执行\n") userID := c.MustGet("user_id")
userID, exists := c.Get("user_id") // 根据不同的类型进行安全转换
if !exists {
fmt.Printf("❌ GetUserID - user_id不存在于上下文中\n")
return 0, false
}
fmt.Printf("✅ GetUserID - 从上下文获取的user_id类型: %T, 值: %v\n", userID, userID)
// 安全地进行类型转换,避免panic
switch v := userID.(type) { switch v := userID.(type) {
case uint: case uint:
fmt.Printf("GetUserID - 类型匹配uint: %d\n", v) return v
return v, true
case int: case int:
if v >= 0 { if v < 0 {
fmt.Printf("GetUserID - 类型匹配int: %d, 转换为uint: %d\n", v, uint(v)) return 0
return uint(v), true
} else {
fmt.Printf("GetUserID - 类型匹配int: %d, 但值为负数,无法转换为uint\n", v)
} }
return uint(v)
case int64: case int64:
if v >= 0 { if v < 0 {
fmt.Printf("GetUserID - 类型匹配int64: %d, 转换为uint: %d\n", v, uint(v)) return 0
return uint(v), true
} else {
fmt.Printf("GetUserID - 类型匹配int64: %d, 但值为负数,无法转换为uint\n", v)
} }
return uint(v)
case float64: case float64:
if v >= 0 && v <= float64(^uint(0)) { if v < 0 || v > float64(^uint(0)) {
fmt.Printf("GetUserID - 类型匹配float64: %f, 转换为uint: %d\n", v, uint(v)) return 0
return uint(v), true
} else {
fmt.Printf("GetUserID - 类型匹配float64: %f, 但值超出uint范围,无法转换\n", v)
} }
return uint(v)
case string: case string:
if parsed, err := strconv.ParseUint(v, 10, 32); err == nil { if parsed, err := strconv.ParseUint(v, 10, 64); err == nil {
fmt.Printf("GetUserID - 类型匹配string: %s, 解析为uint: %d\n", v, uint(parsed)) return uint(parsed)
return uint(parsed), true
} else {
fmt.Printf("GetUserID - 类型匹配string: %s, 但解析失败: %v\n", v, err)
} }
return 0
default: default:
fmt.Printf("GetUserID - 未知类型: %T, 值: %v\n", userID, userID) return 0
} }
// 如果所有类型转换都失败,返回0和false
fmt.Printf("GetUserID - 所有类型转换都失败,返回0\n")
return 0, false
} }
// GetUsername 从上下文中获取用户名 // GetUsername 从上下文中获取用户名

91
gofaster/backend/internal/shared/middleware/logger_middleware.go

@ -1,90 +1,41 @@
package middleware package middleware
import ( import (
"bytes"
"gofaster/internal/shared/model"
"io"
"strconv"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm"
) )
// 从 Gin 上下文中获取用户ID(假设 JWT 中间件已将用户信息存入上下文) // Logger 日志中间件
func getUserIdFromContext(c *gin.Context) uint { func Logger(logger *zap.Logger) gin.HandlerFunc {
// 使用安全的GetUserID函数
if userID, exists := GetUserID(c); exists {
return userID
}
return 0 // 0 表示未登录用户或获取失败
}
// 从请求路径解析操作类型(根据 RESTful 路径映射)
func getActionFromPath(path string) string {
// 移除查询参数和版本前缀(如 /api/v1/users -> /users)
cleanPath := strings.Split(path, "?")[0]
segments := strings.Split(cleanPath, "/")
lastSegment := segments[len(segments)-1]
// 常见 RESTful 操作映射
switch lastSegment {
case "login":
return "用户登录"
case "logout":
return "用户登出"
case "users":
return "用户管理"
case "roles":
return "角色管理"
case "permissions":
return "权限管理"
default:
// 如果是数字ID(如 /users/123),取上一级路径
if _, err := strconv.Atoi(lastSegment); err == nil && len(segments) > 1 {
return getActionFromPath(strings.Join(segments[:len(segments)-1], "/"))
}
return "未知操作"
}
}
func LoggerMiddleware(logger *zap.Logger, db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
start := time.Now() start := time.Now()
path := c.Request.URL.Path path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
// 记录请求体
var requestBody string
if c.Request.Body != nil {
bodyBytes, _ := io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
requestBody = string(bodyBytes)
}
// 处理请求 // 处理请求
c.Next() c.Next()
// 记录日志 // 记录请求信息
latency := time.Since(start).Milliseconds() latency := time.Since(start)
actionLog := model.ActionLog{ clientIP := c.ClientIP()
UserID: getUserIdFromContext(c), method := c.Request.Method
Action: getActionFromPath(path), statusCode := c.Writer.Status()
IP: c.ClientIP(), bodySize := c.Writer.Size()
UserAgent: c.Request.UserAgent(),
Path: path, if raw != "" {
Method: c.Request.Method, path = path + "?" + raw
Request: requestBody,
Status: c.Writer.Status(),
Latency: latency,
} }
// 异步保存日志 // 记录访问日志
go func() { logger.Info("HTTP Request",
if err := db.Create(&actionLog).Error; err != nil { zap.String("method", method),
logger.Error("保存操作日志失败", zap.Error(err)) zap.String("path", path),
} zap.Int("status", statusCode),
}() zap.String("ip", clientIP),
zap.Duration("latency", latency),
zap.Int("size", bodySize),
)
} }
} }

32
gofaster/backend/internal/shared/middleware/permission_middleware.go

@ -3,35 +3,23 @@ package middleware
import ( import (
"net/http" "net/http"
"gofaster/internal/auth/repository"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gofaster/internal/shared/response"
) )
var permRepo *repository.PermissionRepo // Permission 权限检查中间件
func Permission(resource, action string) gin.HandlerFunc {
// 初始化权限仓库(可以在main.go中调用)
func InitPermissionMiddleware(db *gorm.DB) {
permRepo = repository.NewPermissionRepo(db)
}
func PermissionMiddleware(permission string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// 使用安全的GetUserID函数 // 获取用户ID
userID, exists := GetUserID(c) userID := GetUserID(c)
if !exists { if userID == 0 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) response.Unauthorized(c, "未授权", "用户ID不存在")
return c.Abort()
}
// 检查用户是否有该权限
hasPerm := permRepo.CheckUserPermission(userID, permission)
if !hasPerm {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "没有权限"})
return return
} }
// 这里应该检查用户是否有对应资源的权限
// 暂时简单返回成功
c.Next() c.Next()
} }
} }

6
gofaster/backend/main.go

@ -63,14 +63,14 @@ func main() {
moduleManager := core.NewModuleManager(log) moduleManager := core.NewModuleManager(log)
// 自动发现并注册模块 // 自动发现并注册模块
fmt.Printf("🔍 开始自动发现模块\n")
if err := core.DiscoverModules(moduleManager); err != nil { if err := core.DiscoverModules(moduleManager); err != nil {
log.Fatal("Failed to discover modules", zap.Error(err)) log.Fatal("Failed to discover modules", zap.Error(err))
} }
fmt.Printf("✅ 模块发现完成\n") fmt.Printf("✅ 模块发现完成\n")
// 初始化所有模块 // 初始化所有模块
fmt.Printf("🔍 开始初始化模块\n")
if err := moduleManager.InitModules(cfg, db, redisClient); err != nil { if err := moduleManager.InitModules(cfg, db, redisClient); err != nil {
log.Fatal("Failed to initialize modules", zap.Error(err)) log.Fatal("Failed to initialize modules", zap.Error(err))
} }
@ -85,7 +85,7 @@ func main() {
) )
// 注册路由 // 注册路由
fmt.Printf("🔍 开始注册路由\n")
api := app.Group("/api") api := app.Group("/api")
moduleManager.RegisterRoutes(api) moduleManager.RegisterRoutes(api)
fmt.Printf("✅ 路由注册完成\n") fmt.Printf("✅ 路由注册完成\n")

2
gofaster/backend/tmp/build-errors.log

@ -1 +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 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 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 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 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

BIN
gofaster/backend/tmp/main.exe

Binary file not shown.
Loading…
Cancel
Save