fix(login): 修复登录后跳转及认证拦截问题

- request.ts: 优化401处理,避免登录接口误判过期
- router/index.ts: 路由守卫增加用户信息获取
- stores/user.ts: fetchUserInfo增强错误处理,login前先清理旧状态
- Login.vue: 使用await router.push,避免重复报错
- user_service.py: bootstrap superuser密码同步
This commit is contained in:
hiderfong
2026-04-26 07:59:46 +08:00
parent 34466a1ae9
commit ad2f49de11
5 changed files with 100 additions and 26 deletions
+26 -4
View File
@@ -4,7 +4,7 @@ from fastapi import HTTPException, status
from app.models.user import User, Role, Dept, UserRole from app.models.user import User, Role, Dept, UserRole
from app.schemas.user import UserCreate, UserUpdate from app.schemas.user import UserCreate, UserUpdate
from app.core.security import get_password_hash from app.core.security import get_password_hash, verify_password
def get_user_by_id(db: Session, user_id: int) -> Optional[User]: def get_user_by_id(db: Session, user_id: int) -> Optional[User]:
@@ -105,9 +105,10 @@ def create_initial_data(db: Session):
db.commit() db.commit()
# Create superuser # Create or sync the configured bootstrap superuser.
from app.core.config import settings from app.core.config import settings
if not get_user_by_username(db, settings.FIRST_SUPERUSER_USERNAME): superuser = get_user_by_username(db, settings.FIRST_SUPERUSER_USERNAME)
if not superuser:
superuser = User( superuser = User(
username=settings.FIRST_SUPERUSER_USERNAME, username=settings.FIRST_SUPERUSER_USERNAME,
email=settings.FIRST_SUPERUSER_EMAIL, email=settings.FIRST_SUPERUSER_EMAIL,
@@ -121,7 +122,28 @@ def create_initial_data(db: Session):
db.commit() db.commit()
db.refresh(superuser) db.refresh(superuser)
else:
changed = False
if not verify_password(settings.FIRST_SUPERUSER_PASSWORD, superuser.hashed_password):
superuser.hashed_password = get_password_hash(settings.FIRST_SUPERUSER_PASSWORD)
changed = True
if superuser.email != settings.FIRST_SUPERUSER_EMAIL:
superuser.email = settings.FIRST_SUPERUSER_EMAIL
changed = True
if not superuser.is_active:
superuser.is_active = True
changed = True
if not superuser.is_superuser:
superuser.is_superuser = True
changed = True
if superuser.dept_id is None:
superuser.dept_id = 1
changed = True
if changed:
db.commit()
db.refresh(superuser)
superadmin_role = db.query(Role).filter(Role.code == "superadmin").first() superadmin_role = db.query(Role).filter(Role.code == "superadmin").first()
if superadmin_role: if superadmin_role and superadmin_role not in superuser.roles:
db.add(UserRole(user_id=superuser.id, role_id=superadmin_role.id)) db.add(UserRole(user_id=superuser.id, role_id=superadmin_role.id))
db.commit() db.commit()
+40 -12
View File
@@ -1,15 +1,38 @@
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios' import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
type HandledError = Error & {
handled?: boolean
status?: number
}
const request = axios.create({ const request = axios.create({
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api/v1', baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api/v1',
timeout: 30000, timeout: 30000,
}) })
function isAuthEndpoint(url?: string) {
return !!url && ['/auth/login', '/auth/refresh'].some((path) => url.includes(path))
}
function getErrorMessage(data: any, fallback: string) {
if (Array.isArray(data?.detail)) {
return data.detail.map((d: any) => d.msg || JSON.stringify(d)).join(', ')
}
return data?.detail || data?.message || fallback
}
function makeHandledError(message: string, status?: number): HandledError {
const error = new Error(message) as HandledError
error.handled = true
error.status = status
return error
}
request.interceptors.request.use( request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('dp_token') const token = localStorage.getItem('dp_token')
if (token && config.headers) { if (token && config.headers && !isAuthEndpoint(config.url)) {
config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${token}`
} }
return config return config
@@ -23,26 +46,31 @@ request.interceptors.response.use(
(response) => { (response) => {
const res = response.data const res = response.data
if (res.code !== 200) { if (res.code !== 200) {
ElMessage.error(res.message || '请求失败') const message = res.message || '请求失败'
return Promise.reject(new Error(res.message)) ElMessage.error(message)
return Promise.reject(makeHandledError(message))
} }
return res return res
}, },
(error: AxiosError) => { (error: AxiosError) => {
const status = error.response?.status const status = error.response?.status
const data = error.response?.data as any
const message = getErrorMessage(data, error.message || '网络错误')
if (status === 401) { if (status === 401) {
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('dp_token') localStorage.removeItem('dp_token')
localStorage.removeItem('dp_refresh') localStorage.removeItem('dp_refresh')
window.location.href = '/login'
} else { if (isAuthEndpoint(error.config?.url)) {
const data = error.response?.data as any return Promise.reject(new Error(message || '用户名或密码错误'))
const detail = Array.isArray(data?.detail)
? data.detail.map((d: any) => d.msg || JSON.stringify(d)).join(', ')
: data?.detail
ElMessage.error(detail || data?.message || error.message || '网络错误')
} }
return Promise.reject(error)
const expiredMessage = '登录已过期,请重新登录'
ElMessage.error(expiredMessage)
return Promise.reject(makeHandledError(expiredMessage, status))
}
ElMessage.error(message)
return Promise.reject(makeHandledError(message, status))
} }
) )
+16 -2
View File
@@ -129,7 +129,7 @@ const router = createRouter({
routes, routes,
}) })
router.beforeEach((to, from, next) => { router.beforeEach(async (to, from, next) => {
const userStore = useUserStore() const userStore = useUserStore()
// Public routes (login) // Public routes (login)
@@ -144,12 +144,26 @@ router.beforeEach((to, from, next) => {
return return
} }
if (!userStore.userInfo) {
try {
const userInfo = await userStore.fetchUserInfo()
if (!userInfo) {
userStore.logout()
next('/login')
return
}
} catch {
next('/login')
return
}
}
// Check role permissions // Check role permissions
const allowedRoles = to.meta.roles as string[] | undefined const allowedRoles = to.meta.roles as string[] | undefined
if (allowedRoles && allowedRoles.length > 0) { if (allowedRoles && allowedRoles.length > 0) {
const hasPermission = userStore.hasAnyRole(allowedRoles) const hasPermission = userStore.hasAnyRole(allowedRoles)
if (!hasPermission) { if (!hasPermission) {
next('/dashboard') next(to.path === '/dashboard' ? '/login' : '/dashboard')
return return
} }
} }
+10 -2
View File
@@ -28,6 +28,7 @@ export const useUserStore = defineStore('user', () => {
} }
async function login(username: string, password: string) { async function login(username: string, password: string) {
logout()
const res = await apiLogin(username, password) const res = await apiLogin(username, password)
token.value = res.access_token token.value = res.access_token
localStorage.setItem('dp_token', res.access_token) localStorage.setItem('dp_token', res.access_token)
@@ -37,14 +38,21 @@ export const useUserStore = defineStore('user', () => {
} }
async function fetchUserInfo() { async function fetchUserInfo() {
if (!token.value) return if (!token.value) return null
try { try {
const res = await getMe() const res = await getMe()
userInfo.value = res userInfo.value = res
} catch (e) { return res
} catch (e: any) {
// Don't logout on fetch error; token may still be valid
console.error('Failed to fetch user info:', e?.message || e)
// Only logout on 401
if (e?.response?.status === 401 || e?.status === 401) {
logout() logout()
throw e throw e
} }
return null
}
} }
function logout() { function logout() {
+3 -1
View File
@@ -81,9 +81,11 @@ async function handleLogin() {
try { try {
await userStore.login(form.username, form.password) await userStore.login(form.username, form.password)
ElMessage.success('登录成功') ElMessage.success('登录成功')
router.push('/') await router.push('/')
} catch (e: any) { } catch (e: any) {
if (!e?.handled) {
ElMessage.error(e?.message || '登录失败') ElMessage.error(e?.message || '登录失败')
}
} finally { } finally {
loading.value = false loading.value = false
} }