diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index 812549de..6bd7ca2c 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -4,7 +4,7 @@ from fastapi import HTTPException, status from app.models.user import User, Role, Dept, UserRole 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]: @@ -105,9 +105,10 @@ def create_initial_data(db: Session): db.commit() - # Create superuser + # Create or sync the configured bootstrap superuser. 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( username=settings.FIRST_SUPERUSER_USERNAME, email=settings.FIRST_SUPERUSER_EMAIL, @@ -121,7 +122,28 @@ def create_initial_data(db: Session): db.commit() db.refresh(superuser) - superadmin_role = db.query(Role).filter(Role.code == "superadmin").first() - if superadmin_role: - db.add(UserRole(user_id=superuser.id, role_id=superadmin_role.id)) + 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() + if superadmin_role and superadmin_role not in superuser.roles: + db.add(UserRole(user_id=superuser.id, role_id=superadmin_role.id)) + db.commit() diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts index 93e51c3d..d9df89e6 100644 --- a/frontend/src/api/request.ts +++ b/frontend/src/api/request.ts @@ -1,15 +1,38 @@ import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios' import { ElMessage } from 'element-plus' +type HandledError = Error & { + handled?: boolean + status?: number +} + const request = axios.create({ baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api/v1', 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( (config: InternalAxiosRequestConfig) => { const token = localStorage.getItem('dp_token') - if (token && config.headers) { + if (token && config.headers && !isAuthEndpoint(config.url)) { config.headers.Authorization = `Bearer ${token}` } return config @@ -23,26 +46,31 @@ request.interceptors.response.use( (response) => { const res = response.data if (res.code !== 200) { - ElMessage.error(res.message || '请求失败') - return Promise.reject(new Error(res.message)) + const message = res.message || '请求失败' + ElMessage.error(message) + return Promise.reject(makeHandledError(message)) } return res }, (error: AxiosError) => { const status = error.response?.status + const data = error.response?.data as any + const message = getErrorMessage(data, error.message || '网络错误') if (status === 401) { - ElMessage.error('登录已过期,请重新登录') localStorage.removeItem('dp_token') localStorage.removeItem('dp_refresh') - window.location.href = '/login' - } else { - const data = error.response?.data as any - 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 || '网络错误') + + if (isAuthEndpoint(error.config?.url)) { + return Promise.reject(new Error(message || '用户名或密码错误')) + } + + const expiredMessage = '登录已过期,请重新登录' + ElMessage.error(expiredMessage) + return Promise.reject(makeHandledError(expiredMessage, status)) } - return Promise.reject(error) + + ElMessage.error(message) + return Promise.reject(makeHandledError(message, status)) } ) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 2244cca8..9985dde6 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -129,7 +129,7 @@ const router = createRouter({ routes, }) -router.beforeEach((to, from, next) => { +router.beforeEach(async (to, from, next) => { const userStore = useUserStore() // Public routes (login) @@ -143,13 +143,27 @@ router.beforeEach((to, from, next) => { next('/login') return } + + if (!userStore.userInfo) { + try { + const userInfo = await userStore.fetchUserInfo() + if (!userInfo) { + userStore.logout() + next('/login') + return + } + } catch { + next('/login') + return + } + } // Check role permissions const allowedRoles = to.meta.roles as string[] | undefined if (allowedRoles && allowedRoles.length > 0) { const hasPermission = userStore.hasAnyRole(allowedRoles) if (!hasPermission) { - next('/dashboard') + next(to.path === '/dashboard' ? '/login' : '/dashboard') return } } diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index fe3b6130..dc386c1c 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -28,6 +28,7 @@ export const useUserStore = defineStore('user', () => { } async function login(username: string, password: string) { + logout() const res = await apiLogin(username, password) token.value = res.access_token localStorage.setItem('dp_token', res.access_token) @@ -37,13 +38,20 @@ export const useUserStore = defineStore('user', () => { } async function fetchUserInfo() { - if (!token.value) return + if (!token.value) return null try { const res = await getMe() userInfo.value = res - } catch (e) { - logout() - throw 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() + throw e + } + return null } } diff --git a/frontend/src/views/auth/Login.vue b/frontend/src/views/auth/Login.vue index 68923849..45099e15 100644 --- a/frontend/src/views/auth/Login.vue +++ b/frontend/src/views/auth/Login.vue @@ -81,9 +81,11 @@ async function handleLogin() { try { await userStore.login(form.username, form.password) ElMessage.success('登录成功') - router.push('/') + await router.push('/') } catch (e: any) { - ElMessage.error(e?.message || '登录失败') + if (!e?.handled) { + ElMessage.error(e?.message || '登录失败') + } } finally { loading.value = false }