fix: 修复会话鉴权「登录后 token 无效」并补齐 NAS 部署流程
- deps.py: _create_redis_client 增加同步 ping 校验,Redis 不可达时正确降级为内存模式 - main.py: lifespan 复用 deps 的 UserStore 单例,避免 admin 与 API 请求实例不一致 - 前端: 强制改密弹窗(must_change_password 用户)、兼容新旧登录返回格式、markPasswordChanged - 新增 NAS SSH 部署脚本与批处理测试;gitignore 前端构建产物
This commit is contained in:
@@ -63,3 +63,15 @@ export function resetPassword(username, newPassword) {
|
||||
export function deleteUser(username) {
|
||||
return http.delete(`/api/v1/auth/users/${encodeURIComponent(username)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改自己的密码(当前登录用户)
|
||||
* @param {string} oldPassword
|
||||
* @param {string} newPassword
|
||||
*/
|
||||
export function changeMyPassword(oldPassword, newPassword) {
|
||||
return http.post('/api/v1/auth/password', {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, h, ref } from 'vue'
|
||||
import { computed, h, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter, RouterView } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import { Modal, message } from 'ant-design-vue'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
FileTextOutlined,
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
TeamOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { changeMyPassword } from '@/api/auth'
|
||||
import { callApi } from '@/api/client'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -66,6 +68,68 @@ function handleLogout() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* ---------- 强制改密弹窗(must_change_password 用户) ---------- */
|
||||
const passwordModalVisible = ref(false)
|
||||
const passwordSubmitting = ref(false)
|
||||
const passwordFormRef = ref(null)
|
||||
const passwordForm = reactive({
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
|
||||
const passwordRules = {
|
||||
old_password: [{ required: true, message: '请输入当前密码', trigger: 'blur' }],
|
||||
new_password: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 8, message: '密码至少 8 位', trigger: 'blur' }
|
||||
],
|
||||
confirm_password: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value) =>
|
||||
value === passwordForm.new_password
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('两次输入的密码不一致')),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// must_change_password 为 true 时自动弹出改密弹窗(不可关闭)
|
||||
watch(
|
||||
() => user.value?.must_change_password,
|
||||
(val) => {
|
||||
passwordModalVisible.value = val === true
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function handleSubmitPassword() {
|
||||
try {
|
||||
await passwordFormRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
passwordSubmitting.value = true
|
||||
try {
|
||||
await callApi(
|
||||
() => changeMyPassword(passwordForm.old_password, passwordForm.new_password),
|
||||
{ errorText: '修改密码失败' }
|
||||
)
|
||||
authStore.markPasswordChanged()
|
||||
message.success('密码修改成功')
|
||||
passwordModalVisible.value = false
|
||||
passwordForm.old_password = ''
|
||||
passwordForm.new_password = ''
|
||||
passwordForm.confirm_password = ''
|
||||
} catch {
|
||||
// 错误已由 callApi 弹出
|
||||
} finally {
|
||||
passwordSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -127,6 +191,55 @@ function handleLogout() {
|
||||
QMDSearch Admin · 当前用户:{{ user?.username || '-' }}
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
|
||||
<!-- 强制改密弹窗(must_change_password 用户,不可关闭) -->
|
||||
<a-modal
|
||||
v-model:open="passwordModalVisible"
|
||||
:mask-closable="false"
|
||||
:closable="false"
|
||||
:keyboard="false"
|
||||
:destroy-on-close="true"
|
||||
title="首次登录须修改密码"
|
||||
:confirm-loading="passwordSubmitting"
|
||||
ok-text="确认修改"
|
||||
:cancel-button-props="{ style: { display: 'none' } }"
|
||||
@ok="handleSubmitPassword"
|
||||
>
|
||||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
message="检测到首次登录或管理员重置密码,请立即修改密码后方可使用系统。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="当前密码" name="old_password">
|
||||
<a-input-password
|
||||
v-model:value="passwordForm.old_password"
|
||||
placeholder="请输入当前密码"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="新密码" name="new_password">
|
||||
<a-input-password
|
||||
v-model:value="passwordForm.new_password"
|
||||
placeholder="至少 8 位"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="确认新密码" name="confirm_password">
|
||||
<a-input-password
|
||||
v-model:value="passwordForm.confirm_password"
|
||||
placeholder="再次输入新密码"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -39,13 +39,27 @@ export const useAuthStore = defineStore('auth', {
|
||||
actions: {
|
||||
/**
|
||||
* 登录成功后保存 token + user
|
||||
* @param {{access_token:string, user:object}} data
|
||||
* 兼容后端两种返回:
|
||||
* - 新格式 { token, username, role, must_change_password }
|
||||
* - 旧格式 { access_token, user: { username, role, ... } }
|
||||
* @param {object} data
|
||||
*/
|
||||
setAuth(data) {
|
||||
this.token = data.access_token
|
||||
this.user = data.user
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, data.access_token)
|
||||
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(data.user))
|
||||
const token = data?.access_token ?? data?.token
|
||||
const user =
|
||||
data?.user ??
|
||||
(data
|
||||
? {
|
||||
username: data.username,
|
||||
role: data.role,
|
||||
must_change_password: data.must_change_password ?? false
|
||||
}
|
||||
: null)
|
||||
if (!token || !user) return
|
||||
this.token = token
|
||||
this.user = user
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user))
|
||||
},
|
||||
|
||||
/** 清除登录态(登出 / 401) */
|
||||
@@ -56,6 +70,13 @@ export const useAuthStore = defineStore('auth', {
|
||||
localStorage.removeItem(USER_STORAGE_KEY)
|
||||
},
|
||||
|
||||
/** 改密成功后更新本地用户状态(清除 must_change_password 标记) */
|
||||
markPasswordChanged() {
|
||||
if (!this.user) return
|
||||
this.user = { ...this.user, must_change_password: false }
|
||||
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(this.user))
|
||||
},
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user