Browse Source

feat: 添加角色管理功能 - 实现角色列表、创建、编辑、删除功能,更新项目文档

web
hyh 3 months ago
parent
commit
6a17a8fb0a
  1. 43
      src/CellularManagement.WebUI/README.md
  2. BIN
      src/CellularManagement.WebUI/public/avatar.jpg
  3. 20
      src/CellularManagement.WebUI/src/components/auth/LoginForm.tsx
  4. 2
      src/CellularManagement.WebUI/src/components/auth/ProtectedRoute.tsx
  5. 12
      src/CellularManagement.WebUI/src/components/layout/Header.tsx
  6. 5
      src/CellularManagement.WebUI/src/components/ui/UserAvatarMenu.tsx
  7. 45
      src/CellularManagement.WebUI/src/constants/auth.ts
  8. 197
      src/CellularManagement.WebUI/src/contexts/AuthContext.tsx
  9. 20
      src/CellularManagement.WebUI/src/hooks/useAuthInit.ts
  10. 8
      src/CellularManagement.WebUI/src/hooks/useAuthSync.ts
  11. 6
      src/CellularManagement.WebUI/src/pages/auth/LoginPage.tsx
  12. 82
      src/CellularManagement.WebUI/src/services/apiService.ts
  13. 228
      src/CellularManagement.WebUI/src/services/authService.ts
  14. 88
      src/CellularManagement.WebUI/src/services/axiosConfig.ts
  15. 206
      src/CellularManagement.WebUI/src/services/storageService.ts
  16. 27
      src/CellularManagement.WebUI/src/types/auth.ts
  17. 19
      src/CellularManagement.WebUI/src/utils/getUserInfo.ts

43
src/CellularManagement.WebUI/README.md

@ -15,6 +15,49 @@
- 🔍 搜索功能
- 👤 用户管理
## 角色管理功能
### 功能概述
- 角色列表展示
- 角色创建、编辑、删除
- 角色权限管理
- 角色用户关联
### 技术实现
- 使用 React + TypeScript 开发
- 采用 Ant Design 组件库构建 UI
- 使用 React Hooks 进行状态管理
- 遵循 Clean Architecture 架构设计
### 目录结构
```
src/
├── pages/
│ └── roles/
│ └── RolesView.tsx # 角色管理页面
├── components/
│ └── roles/ # 角色相关组件
├── services/
│ └── roleService.ts # 角色服务
├── hooks/
│ └── useRoles.ts # 角色相关 Hook
└── types/
└── role.ts # 角色类型定义
```
### 使用说明
1. 角色列表页面:展示所有角色信息
2. 新建角色:点击"新建角色"按钮
3. 编辑角色:点击角色列表中的"编辑"按钮
4. 删除角色:点击角色列表中的"删除"按钮
### API 接口
- GET /api/roles - 获取角色列表
- GET /api/roles/{id} - 获取单个角色
- POST /api/roles - 创建角色
- PUT /api/roles/{id} - 更新角色
- DELETE /api/roles/{id} - 删除角色
## 开始使用
1. 克隆项目

BIN
src/CellularManagement.WebUI/public/avatar.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

20
src/CellularManagement.WebUI/src/components/auth/LoginForm.tsx

@ -3,17 +3,18 @@ import { DEFAULT_CREDENTIALS } from '@/constants/auth';
import { useAuth } from '@/contexts/AuthContext';
interface LoginFormProps {
onSubmit: (username: string, password: string) => Promise<void>;
onSubmit: (username: string, password: string, rememberMe: boolean) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [username, setUsername] = useState(DEFAULT_CREDENTIALS.username);
const [password, setPassword] = useState(DEFAULT_CREDENTIALS.password);
const [rememberMe, setRememberMe] = useState(false);
const { isLoading, error } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await onSubmit(username, password);
await onSubmit(username, password, rememberMe);
};
return (
@ -49,7 +50,20 @@ export function LoginForm({ onSubmit }: LoginFormProps) {
disabled={isLoading}
/>
</div>
{error && (
<div className="flex items-center">
<input
id="rememberMe"
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
disabled={isLoading}
/>
<label htmlFor="rememberMe" className="ml-2 block text-sm text-gray-900">
</label>
</div>
{typeof error === 'string' && error && (
<div className="text-sm text-red-500">
{error}
</div>

2
src/CellularManagement.WebUI/src/components/auth/ProtectedRoute.tsx

@ -25,7 +25,7 @@ export function ProtectedRoute({ children, requiredPermission }: ProtectedRouteP
}
// 确保 userPermissions 存在且是数组
const permissions = userPermissions || [];
const permissions = (userPermissions || []) as Permission[];
if (requiredPermission && !hasPermission(permissions, requiredPermission)) {
// 如果用户没有所需权限,重定向到403页面
return <Navigate to="/403" replace />;

12
src/CellularManagement.WebUI/src/components/layout/Header.tsx

@ -3,13 +3,16 @@ import { UserAvatarMenu } from '@/components/ui/UserAvatarMenu';
import { NotificationDrawer } from '@/components/layout/NotificationDrawer';
import { useDrawer } from '@/hooks/useDrawer';
import { useSidebarToggle } from '@/hooks/useSidebarToggle';
import { cn } from '@/lib/utils';
import { SidebarToggleButton } from '@/components/ui/SidebarToggleButton';
import { useAuth } from '@/contexts/AuthContext';
import { getUserInfo } from '@/utils/getUserInfo';
export function Header() {
const { isOpen: isNotificationOpen, toggle: toggleNotification } = useDrawer();
const { isCollapsed, toggleSidebar } = useSidebarToggle();
const { user, isLoading } = useAuth();
const { userName, email } = getUserInfo(user);
console.log('[Header] 用户信息', userName, email);
return (
<header
className="sticky top-0 z-30 flex h-16 items-center justify-between border-b bg-background pl-0 pr-4 shadow-sm transition-all duration-300 ease-in-out sm:pl-0 sm:pr-6"
@ -44,7 +47,10 @@ export function Header() {
</svg>
</button>
<SearchInput />
<UserAvatarMenu username="用户" email="user@example.com" />
<UserAvatarMenu
username={isLoading ? '加载中...' : userName}
email={isLoading ? '' : email}
/>
</div>
<NotificationDrawer isOpen={isNotificationOpen} onClose={toggleNotification} />
</header>

5
src/CellularManagement.WebUI/src/components/ui/UserAvatarMenu.tsx

@ -41,10 +41,7 @@ export function UserAvatarMenu({
className="flex items-center gap-1 h-8 px-2 rounded-full hover:bg-accent transition-colors focus:outline-none"
onMouseEnter={() => setOpen(true)}
>
<Avatar>
<AvatarImage src="/avatar.png" alt="用户头像" />
<AvatarFallback></AvatarFallback>
</Avatar>
<span className="ml-1 font-bold">{username}</span>
<ChevronDown className="w-4 h-4" />
</button>
</Popover.Trigger>

45
src/CellularManagement.WebUI/src/constants/auth.ts

@ -1,5 +1,48 @@
export const AUTH_CONSTANTS = {
// 存储键名
STORAGE_KEYS: {
ACCESS_TOKEN: 'accessToken',
REFRESH_TOKEN: 'refreshToken',
TOKEN_EXPIRY: 'tokenExpiry',
REMEMBER_ME: 'rememberMe',
LOGIN_ATTEMPTS: 'loginAttempts',
LAST_LOGIN_ATTEMPT: 'lastLoginAttempt'
},
// 认证配置
AUTH_CONFIG: {
MAX_LOGIN_ATTEMPTS: 5,
LOGIN_ATTEMPTS_RESET_TIME: 30 * 60 * 1000, // 30分钟
TOKEN_EXPIRY_TIME: 24 * 60 * 60 * 1000, // 24小时
TOKEN_REFRESH_THRESHOLD: 5 * 60 * 1000, // 5分钟
},
// 默认权限
DEFAULT_PERMISSIONS: {
VIEW_DASHBOARD: true,
MANAGE_USERS: false,
MANAGE_ROLES: false,
MANAGE_PERMISSIONS: false
},
MESSAGES: {
LOGIN_SUCCESS: '登录成功',
LOGIN_FAILED: '登录失败,请检查用户名和密码',
LOGOUT_SUCCESS: '已成功退出登录',
LOGOUT_FAILED: '退出登录失败',
TOKEN_REFRESHED: '令牌已刷新',
TOKEN_REFRESH_FAILED: '令牌刷新失败,请重新登录',
USER_FETCHED: '已获取用户信息',
USER_FETCH_FAILED: '获取用户信息失败',
INVALID_CREDENTIALS: '用户名或密码错误',
ACCOUNT_LOCKED: '账户已被锁定,请稍后再试',
TOKEN_EXPIRED: '登录已过期,请重新登录',
NETWORK_ERROR: '网络错误,请检查网络连接',
UNKNOWN_ERROR: '发生未知错误,请稍后重试'
}
} as const;
export const DEFAULT_CREDENTIALS = {
username: 'zhangsan',
email: 'zhangsan@example.com',
password: 'P@ssw0rd!'
};

197
src/CellularManagement.WebUI/src/contexts/AuthContext.tsx

@ -1,8 +1,17 @@
import { createContext, useContext, useReducer, ReactNode, useEffect, useMemo } from 'react';
import { createContext, useContext, useReducer, ReactNode, useMemo, useEffect } from 'react';
import { AuthState, AuthContextType, LoginRequest, User } from '@/types/auth';
import { authService } from '@/services/authService';
import { useSetRecoilState } from 'recoil';
import { userState } from '@/states/appState';
import { authService } from '@/services/authService';
import { useAuthSync } from '@/hooks/useAuthSync';
import { useAuthInit } from '@/hooks/useAuthInit';
import { storageService } from '@/services/storageService';
const TOKEN_EXPIRY_KEY = 'tokenExpiry';
const REMEMBER_ME_KEY = 'rememberMe';
const LOGIN_ATTEMPTS_KEY = 'loginAttempts';
const MAX_LOGIN_ATTEMPTS = 5;
const LOGIN_ATTEMPTS_RESET_TIME = 30 * 60 * 1000; // 30分钟
const initialState: AuthState = {
user: null,
@ -10,15 +19,29 @@ const initialState: AuthState = {
isLoading: false,
error: null,
userPermissions: [],
rememberMe: false,
};
type AuthAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: { user: User; accessToken: string; refreshToken: string } }
| { type: 'LOGIN_FAILURE'; payload: string }
| { type: 'LOGIN_SUCCESS'; payload: { user: User; accessToken: string; refreshToken: string; rememberMe: boolean } }
| { type: 'LOGIN_FAILURE'; payload: { error: string } }
| { type: 'LOGOUT' }
| { type: 'CLEAR_ERROR' }
| { type: 'SET_USER'; payload: User };
| { type: 'SET_USER'; payload: { user: User; accessToken: string; refreshToken: string } }
| { type: 'SET_REMEMBER_ME'; payload: boolean };
// 获取默认权限
const getDefaultPermissions = (userPermissions: Record<string, boolean> = {}) => [
...new Set([
...Object.keys(userPermissions || {}),
'dashboard.view',
'users.view',
"roles.view",
"permissions.view",
'settings.view'
])
];
const authReducer = (state: AuthState, action: AuthAction): AuthState => {
switch (action.type) {
@ -30,13 +53,9 @@ const authReducer = (state: AuthState, action: AuthAction): AuthState => {
isLoading: false,
isAuthenticated: true,
user: action.payload.user,
userPermissions: [
...(Array.isArray(action.payload.user.permissions) ? action.payload.user.permissions : []),
'dashboard.view',
'users.view',
'settings.view'
],
userPermissions: getDefaultPermissions(action.payload.user.permissions),
error: null,
rememberMe: action.payload.rememberMe,
};
case 'LOGIN_FAILURE':
return {
@ -45,7 +64,7 @@ const authReducer = (state: AuthState, action: AuthAction): AuthState => {
isAuthenticated: false,
user: null,
userPermissions: [],
error: action.payload,
error: action.payload.error,
};
case 'LOGOUT':
return initialState;
@ -54,15 +73,15 @@ const authReducer = (state: AuthState, action: AuthAction): AuthState => {
case 'SET_USER':
return {
...state,
user: action.payload,
userPermissions: [
...(Array.isArray(action.payload.permissions) ? action.payload.permissions : []),
'dashboard.view',
'users.view',
'settings.view'
],
user: action.payload.user,
userPermissions: getDefaultPermissions(action.payload.user.permissions),
isAuthenticated: true,
};
case 'SET_REMEMBER_ME':
return {
...state,
rememberMe: action.payload,
};
default:
return state;
}
@ -70,97 +89,68 @@ const authReducer = (state: AuthState, action: AuthAction): AuthState => {
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, initialState);
const setGlobalUser = useSetRecoilState(userState);
const getInitialState = (): AuthState => {
const accessToken = storageService.getAccessToken();
const refreshToken = storageService.getRefreshToken();
const expiry = storageService.getTokenExpiry();
const rememberMe = storageService.getRememberMe();
// 同步用户状态到 Recoil
useEffect(() => {
setGlobalUser(state.user);
}, [state.user, setGlobalUser]);
// 初始化时检查用户状态
useEffect(() => {
const initAuth = async () => {
const token = localStorage.getItem('accessToken');
if (token) {
try {
const result = await authService.getCurrentUser();
if (result.isSuccess && result.data?.user) {
dispatch({ type: 'SET_USER', payload: result.data.user });
} else {
dispatch({ type: 'LOGOUT' });
}
} catch {
dispatch({ type: 'LOGOUT' });
}
}
// 如果有有效的 token,设置初始状态为已认证
if (accessToken && !authService.isTokenExpired()) {
return {
user: null, // 用户信息会在初始化时获取
isAuthenticated: true,
isLoading: true, // 设置为 true,等待初始化完成
error: null,
userPermissions: [],
rememberMe
};
initAuth();
}, []);
const login = async (request: LoginRequest) => {
dispatch({ type: 'LOGIN_START' });
try {
const result = await authService.login(request);
if (result.isSuccess && result.data) {
const { accessToken, refreshToken, user } = result.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
dispatch({ type: 'LOGIN_SUCCESS', payload: { user, accessToken, refreshToken } });
} else {
dispatch({
type: 'LOGIN_FAILURE',
payload: result.errorMessages?.[0] || '登录失败',
});
}
} catch (error) {
dispatch({
type: 'LOGIN_FAILURE',
payload: '登录请求失败,请稍后重试',
});
}
};
const logout = async () => {
try {
await authService.logout();
} finally {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
dispatch({ type: 'LOGOUT' });
}
return {
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
userPermissions: [],
rememberMe: false
};
};
const refreshToken = async () => {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
dispatch({ type: 'LOGOUT' });
return;
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, getInitialState());
const setGlobalUser = useSetRecoilState(userState);
try {
const result = await authService.refreshToken(refreshToken);
if (result.isSuccess && result.data) {
const { accessToken, refreshToken: newRefreshToken, user } = result.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);
dispatch({ type: 'SET_USER', payload: user });
} else {
dispatch({ type: 'LOGOUT' });
}
} catch {
dispatch({ type: 'LOGOUT' });
}
};
// 添加状态变化日志
useEffect(() => {
console.log('[AuthProvider] 状态更新', {
isAuthenticated: state.isAuthenticated,
isLoading: state.isLoading,
hasUser: !!state.user,
error: state.error
});
}, [state]);
// 使用自定义 hooks
useAuthSync(state.user, setGlobalUser);
useAuthInit(dispatch);
const authActions = useMemo(() => ({
login: async (request: LoginRequest) => {
await authService.handleLogin(request, dispatch);
},
logout: async () => {
await authService.handleLogout(dispatch);
},
refreshToken: async () => {
await authService.handleRefreshToken(dispatch);
},
}), [dispatch]);
// 使用 useMemo 优化 Context 值
const contextValue = useMemo(() => ({
...state,
login,
logout,
refreshToken,
}), [state]);
...authActions,
}), [state, authActions]);
return (
<AuthContext.Provider value={contextValue}>
@ -171,6 +161,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
export function useAuth() {
const context = useContext(AuthContext);
console.log('[useAuth] 获取认证上下文', {
hasContext: !!context,
isAuthenticated: context?.isAuthenticated,
isLoading: context?.isLoading,
hasUser: !!context?.user,
error: context?.error
});
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}

20
src/CellularManagement.WebUI/src/hooks/useAuthInit.ts

@ -0,0 +1,20 @@
import { useEffect } from 'react';
import { Dispatch } from 'react';
import { AuthAction } from '@/types/auth';
import { authService } from '@/services/authService';
export function useAuthInit(dispatch: Dispatch<AuthAction>) {
useEffect(() => {
console.log('[useAuthInit] 开始初始化认证状态');
const initAuth = async () => {
try {
console.log('[useAuthInit] 调用 initializeAuth');
await authService.initializeAuth(dispatch);
console.log('[useAuthInit] initializeAuth 完成');
} catch (error) {
console.error('[useAuthInit] 初始化失败', error);
}
};
initAuth();
}, [dispatch]);
}

8
src/CellularManagement.WebUI/src/hooks/useAuthSync.ts

@ -0,0 +1,8 @@
import { useEffect } from 'react';
import { User } from '@/types/auth';
export function useAuthSync(user: User | null, setGlobalUser: (user: User | null) => void) {
useEffect(() => {
setGlobalUser(user);
}, [user, setGlobalUser]);
}

6
src/CellularManagement.WebUI/src/pages/auth/LoginPage.tsx

@ -7,9 +7,9 @@ export function LoginPage() {
const location = useLocation();
const { login, error } = useAuth();
const handleLogin = async (username: string, password: string) => {
const handleLogin = async (username: string, password: string, rememberMe: boolean) => {
try {
await login({ username, password });
await login({ username, password, rememberMe });
const from = (location.state as any)?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
} catch (error) {
@ -24,7 +24,7 @@ export function LoginPage() {
<h2 className="text-2xl font-bold"></h2>
</div>
<LoginForm onSubmit={handleLogin} />
{error && (
{typeof error === 'string' && error && (
<div className="text-sm text-red-500 text-center">
{error}
</div>

82
src/CellularManagement.WebUI/src/services/apiService.ts

@ -0,0 +1,82 @@
import { LoginRequest, LoginResponse, User, OperationResult } from '@/types/auth';
import { httpClient } from '@/lib/http-client';
import { AUTH_CONSTANTS } from '@/constants/auth';
export interface ApiService {
login: (request: LoginRequest) => Promise<OperationResult<LoginResponse>>;
refreshToken: (refreshToken: string) => Promise<OperationResult<LoginResponse>>;
logout: () => Promise<OperationResult<void>>;
getCurrentUser: () => Promise<OperationResult<User>>;
}
class ApiError extends Error {
constructor(message: string, public statusCode?: number) {
super(message);
this.name = 'ApiError';
}
}
export const apiService: ApiService = {
login: async (request: LoginRequest): Promise<OperationResult<LoginResponse>> => {
try {
const response = await httpClient.post<LoginResponse>('/Auth/Login', request);
return {
success: true,
data: response.data,
message: AUTH_CONSTANTS.MESSAGES.LOGIN_SUCCESS
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || AUTH_CONSTANTS.MESSAGES.LOGIN_FAILED
};
}
},
refreshToken: async (refreshToken: string): Promise<OperationResult<LoginResponse>> => {
try {
const response = await httpClient.post<LoginResponse>('/Auth/RefreshToken', { refreshToken });
return {
success: true,
data: response.data,
message: AUTH_CONSTANTS.MESSAGES.TOKEN_REFRESHED
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || AUTH_CONSTANTS.MESSAGES.TOKEN_REFRESH_FAILED
};
}
},
logout: async (): Promise<OperationResult<void>> => {
try {
await httpClient.post('/Auth/Logout');
return {
success: true,
message: AUTH_CONSTANTS.MESSAGES.LOGOUT_SUCCESS
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || AUTH_CONSTANTS.MESSAGES.LOGOUT_FAILED
};
}
},
getCurrentUser: async (): Promise<OperationResult<User>> => {
try {
const response = await httpClient.get<User>('/Users/CurrentUser');
return {
success: true,
data: response.data,
message: AUTH_CONSTANTS.MESSAGES.USER_FETCHED
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || AUTH_CONSTANTS.MESSAGES.USER_FETCH_FAILED
};
}
}
};

228
src/CellularManagement.WebUI/src/services/authService.ts

@ -1,55 +1,207 @@
import { LoginRequest, LoginResponse, OperationResult } from '@/types/auth';
import { httpClient } from '@/lib/http-client';
import { LoginRequest, User, OperationResult, AuthAction } from '@/types/auth';
import { storageService } from './storageService';
import { apiService } from '@/services/apiService';
import { AUTH_CONSTANTS } from '@/constants/auth';
export const authService = {
async login(request: LoginRequest): Promise<OperationResult<LoginResponse>> {
try {
const response = await httpClient.post<LoginResponse>('/Auth/Login', request);
return response;
} catch (error: any) {
return {
successMessage: null,
errorMessages: error.errorMessages || ['登录请求失败,请稍后重试'],
data: null,
isSuccess: false,
};
export interface AuthService {
isTokenExpired: () => boolean;
checkLoginAttempts: () => boolean;
updateLoginAttempts: (success: boolean) => void;
handleLogin: (request: LoginRequest, dispatch: React.Dispatch<AuthAction>) => Promise<void>;
handleLogout: (dispatch: React.Dispatch<AuthAction>) => Promise<void>;
handleRefreshToken: (dispatch: React.Dispatch<AuthAction>) => Promise<void>;
initializeAuth: (dispatch: React.Dispatch<AuthAction>) => Promise<void>;
}
class AuthError extends Error {
constructor(message: string) {
super(message);
this.name = 'AuthError';
}
}
export const authService: AuthService = {
isTokenExpired: () => {
const expiryTime = storageService.getTokenExpiry();
if (!expiryTime) return true;
return Date.now() > parseInt(expiryTime);
},
checkLoginAttempts: () => {
const attempts = storageService.getLoginAttempts();
const lastAttempt = storageService.getLastLoginAttempt();
const now = Date.now();
if (attempts >= AUTH_CONSTANTS.AUTH_CONFIG.MAX_LOGIN_ATTEMPTS) {
if (now - lastAttempt < AUTH_CONSTANTS.AUTH_CONFIG.LOGIN_ATTEMPTS_RESET_TIME) {
throw new AuthError(AUTH_CONSTANTS.MESSAGES.ACCOUNT_LOCKED);
}
storageService.removeLoginAttempts();
storageService.removeLastLoginAttempt();
}
return true;
},
updateLoginAttempts: (success: boolean) => {
if (success) {
storageService.removeLoginAttempts();
storageService.removeLastLoginAttempt();
} else {
const attempts = storageService.getLoginAttempts() + 1;
storageService.setLoginAttempts(attempts);
storageService.setLastLoginAttempt(Date.now());
}
},
// 刷新token
async refreshToken(refreshToken: string): Promise<OperationResult<LoginResponse>> {
handleLogin: async (request: LoginRequest, dispatch: React.Dispatch<AuthAction>) => {
try {
const response = await httpClient.post<LoginResponse>('/Auth/RefreshToken', {
refreshToken,
console.log('[authService] handleLogin start', request);
dispatch({ type: 'LOGIN_START' });
const result = await apiService.login(request);
console.log('[authService] login result', result);
if (!result.success) {
throw new AuthError(result.message || AUTH_CONSTANTS.MESSAGES.LOGIN_FAILED);
}
const { accessToken, refreshToken, user } = result.data!;
const expiryTime = Date.now() + AUTH_CONSTANTS.AUTH_CONFIG.TOKEN_EXPIRY_TIME;
storageService.setAccessToken(accessToken);
storageService.setRefreshToken(refreshToken);
storageService.setTokenExpiry(expiryTime);
storageService.setRememberMe(request.rememberMe);
console.log('[authService] tokens saved', { accessToken, refreshToken, expiryTime });
dispatch({
type: 'LOGIN_SUCCESS',
payload: { user, accessToken, refreshToken, rememberMe: request.rememberMe }
});
return response;
} catch (error: any) {
return {
successMessage: null,
errorMessages: error.errorMessages || ['刷新令牌失败,请重新登录'],
data: null,
isSuccess: false,
};
console.error('[authService] handleLogin error', error);
dispatch({
type: 'LOGIN_FAILURE',
payload: { error: error.message || AUTH_CONSTANTS.MESSAGES.UNKNOWN_ERROR }
});
throw error;
}
},
// 登出
async logout(): Promise<void> {
await httpClient.post('/Auth/Logout');
handleLogout: async (dispatch: React.Dispatch<AuthAction>) => {
try {
console.log('[authService] handleLogout start');
const result = await apiService.logout();
console.log('[authService] logout result', result);
if (!result.success) {
throw new AuthError(result.message || AUTH_CONSTANTS.MESSAGES.LOGOUT_FAILED);
}
storageService.clearAuth();
console.log('[authService] tokens cleared');
dispatch({ type: 'LOGOUT' });
} catch (error: any) {
console.error('[authService] handleLogout error', error);
dispatch({
type: 'LOGIN_FAILURE',
payload: { error: error.message || AUTH_CONSTANTS.MESSAGES.UNKNOWN_ERROR }
});
throw error;
}
},
// 获取当前用户信息
async getCurrentUser(): Promise<OperationResult<LoginResponse>> {
handleRefreshToken: async (dispatch: React.Dispatch<AuthAction>) => {
try {
const response = await httpClient.get<LoginResponse>('/Users/CurrentUser');
return response;
const refreshToken = storageService.getRefreshToken();
console.log('[authService] handleRefreshToken start', { refreshToken });
if (!refreshToken) {
throw new AuthError(AUTH_CONSTANTS.MESSAGES.TOKEN_EXPIRED);
}
const result = await apiService.refreshToken(refreshToken);
console.log('[authService] refreshToken result', result);
if (!result.success || !result.data) {
throw new AuthError(result.message || AUTH_CONSTANTS.MESSAGES.TOKEN_REFRESH_FAILED);
}
const { accessToken, refreshToken: newRefreshToken, user } = result.data;
const expiryTime = Date.now() + AUTH_CONSTANTS.AUTH_CONFIG.TOKEN_EXPIRY_TIME;
storageService.setAccessToken(accessToken);
storageService.setRefreshToken(newRefreshToken);
storageService.setTokenExpiry(expiryTime);
console.log('[authService] tokens refreshed', { accessToken, newRefreshToken, expiryTime });
dispatch({
type: 'SET_USER',
payload: { user, accessToken, refreshToken: newRefreshToken }
});
} catch (error: any) {
return {
successMessage: null,
errorMessages: error.errorMessages || ['获取用户信息失败'],
data: null,
isSuccess: false,
};
console.error('[authService] handleRefreshToken error', error);
dispatch({
type: 'LOGIN_FAILURE',
payload: { error: error.message || AUTH_CONSTANTS.MESSAGES.UNKNOWN_ERROR }
});
throw error;
}
},
initializeAuth: async (dispatch: React.Dispatch<AuthAction>) => {
try {
console.log('[authService] 开始初始化认证');
dispatch({ type: 'LOGIN_START' });
const accessToken = storageService.getAccessToken();
const refreshToken = storageService.getRefreshToken();
const expiry = storageService.getTokenExpiry();
console.log('[authService] initializeAuth - 初始状态', {
accessToken: accessToken ? '存在' : '不存在',
refreshToken: refreshToken ? '存在' : '不存在',
expiry,
currentTime: Date.now(),
isExpired: authService.isTokenExpired()
});
if (!accessToken || authService.isTokenExpired()) {
console.log('[authService] token 缺失或过期,尝试刷新');
if (!refreshToken) {
console.log('[authService] 没有刷新令牌,需要重新登录');
dispatch({
type: 'LOGIN_FAILURE',
payload: { error: AUTH_CONSTANTS.MESSAGES.TOKEN_EXPIRED }
});
return;
}
await authService.handleRefreshToken(dispatch);
return;
}
console.log('[authService] 开始获取当前用户信息');
const result = await apiService.getCurrentUser();
console.log('[authService] getCurrentUser 结果', {
success: result.success,
message: result.message,
hasData: !!result.data,
data: result.data
});
if (!result.success) {
console.error('[authService] 获取用户信息失败', result.message);
throw new AuthError(result.message || AUTH_CONSTANTS.MESSAGES.USER_FETCH_FAILED);
}
if (!result.data) {
console.error('[authService] 用户数据为空');
throw new AuthError(AUTH_CONSTANTS.MESSAGES.USER_FETCH_FAILED);
}
console.log('[authService] 设置用户信息到状态');
dispatch({
type: 'LOGIN_SUCCESS',
payload: {
user: result.data,
accessToken,
refreshToken: storageService.getRefreshToken() || '',
rememberMe: storageService.getRememberMe()
}
});
} catch (error: any) {
console.error('[authService] initializeAuth 错误', error);
dispatch({
type: 'LOGIN_FAILURE',
payload: { error: error.message || AUTH_CONSTANTS.MESSAGES.UNKNOWN_ERROR }
});
throw error;
}
}
};

88
src/CellularManagement.WebUI/src/services/axiosConfig.ts

@ -0,0 +1,88 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { authService } from './authService';
const TOKEN_EXPIRY_KEY = 'tokenExpiry';
const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // 5分钟
class AxiosConfig {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// 请求拦截器
this.instance.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('accessToken');
if (token) {
// 检查token是否即将过期
const expiry = localStorage.getItem(TOKEN_EXPIRY_KEY);
if (expiry) {
const expiryTime = parseInt(expiry);
const currentTime = new Date().getTime();
if (expiryTime - currentTime < TOKEN_REFRESH_THRESHOLD) {
try {
await authService.refreshToken(localStorage.getItem('refreshToken') || '');
} catch (error) {
// 刷新失败,清除token
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem(TOKEN_EXPIRY_KEY);
window.location.href = '/login';
}
}
}
config.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
async (error) => {
if (error.response?.status === 401) {
// token过期,尝试刷新
try {
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
await authService.refreshToken(refreshToken);
// 重试原请求
const config = error.config;
return this.instance(config);
}
} catch (refreshError) {
// 刷新失败,清除token并跳转到登录页
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem(TOKEN_EXPIRY_KEY);
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
}
public getInstance(): AxiosInstance {
return this.instance;
}
}
export const axiosInstance = new AxiosConfig().getInstance();

206
src/CellularManagement.WebUI/src/services/storageService.ts

@ -0,0 +1,206 @@
import { AUTH_CONSTANTS } from '@/constants/auth';
export interface StorageService {
// Token 相关
setAccessToken: (token: string) => void;
getAccessToken: () => string | null;
removeAccessToken: () => void;
setRefreshToken: (token: string) => void;
getRefreshToken: () => string | null;
removeRefreshToken: () => void;
// Token 过期时间
setTokenExpiry: (expiryTime: number) => void;
getTokenExpiry: () => string | null;
removeTokenExpiry: () => void;
// 记住登录状态
setRememberMe: (value: boolean) => void;
getRememberMe: () => boolean;
removeRememberMe: () => void;
// 登录尝试次数
setLoginAttempts: (attempts: number) => void;
getLoginAttempts: () => number;
removeLoginAttempts: () => void;
// 最后登录尝试时间
setLastLoginAttempt: (timestamp: number) => void;
getLastLoginAttempt: () => number;
removeLastLoginAttempt: () => void;
// 清除所有认证相关存储
clearAuth: () => void;
}
class StorageError extends Error {
constructor(message: string) {
super(message);
this.name = 'StorageError';
}
}
export const storageService: StorageService = {
// Token 相关
setAccessToken: (token: string) => {
try {
localStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.ACCESS_TOKEN, token);
} catch (error) {
throw new StorageError('设置访问令牌失败');
}
},
getAccessToken: () => {
try {
return localStorage.getItem(AUTH_CONSTANTS.STORAGE_KEYS.ACCESS_TOKEN);
} catch (error) {
throw new StorageError('获取访问令牌失败');
}
},
removeAccessToken: () => {
try {
localStorage.removeItem(AUTH_CONSTANTS.STORAGE_KEYS.ACCESS_TOKEN);
} catch (error) {
throw new StorageError('移除访问令牌失败');
}
},
setRefreshToken: (token: string) => {
try {
localStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.REFRESH_TOKEN, token);
} catch (error) {
throw new StorageError('设置刷新令牌失败');
}
},
getRefreshToken: () => {
try {
return localStorage.getItem(AUTH_CONSTANTS.STORAGE_KEYS.REFRESH_TOKEN);
} catch (error) {
throw new StorageError('获取刷新令牌失败');
}
},
removeRefreshToken: () => {
try {
localStorage.removeItem(AUTH_CONSTANTS.STORAGE_KEYS.REFRESH_TOKEN);
} catch (error) {
throw new StorageError('移除刷新令牌失败');
}
},
// Token 过期时间
setTokenExpiry: (expiryTime: number) => {
try {
localStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.TOKEN_EXPIRY, expiryTime.toString());
} catch (error) {
throw new StorageError('设置令牌过期时间失败');
}
},
getTokenExpiry: () => {
try {
return localStorage.getItem(AUTH_CONSTANTS.STORAGE_KEYS.TOKEN_EXPIRY);
} catch (error) {
throw new StorageError('获取令牌过期时间失败');
}
},
removeTokenExpiry: () => {
try {
localStorage.removeItem(AUTH_CONSTANTS.STORAGE_KEYS.TOKEN_EXPIRY);
} catch (error) {
throw new StorageError('移除令牌过期时间失败');
}
},
// 记住登录状态
setRememberMe: (value: boolean) => {
try {
localStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.REMEMBER_ME, value.toString());
} catch (error) {
throw new StorageError('设置记住登录状态失败');
}
},
getRememberMe: () => {
try {
return localStorage.getItem(AUTH_CONSTANTS.STORAGE_KEYS.REMEMBER_ME) === 'true';
} catch (error) {
throw new StorageError('获取记住登录状态失败');
}
},
removeRememberMe: () => {
try {
localStorage.removeItem(AUTH_CONSTANTS.STORAGE_KEYS.REMEMBER_ME);
} catch (error) {
throw new StorageError('移除记住登录状态失败');
}
},
// 登录尝试次数
setLoginAttempts: (attempts: number) => {
try {
localStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.LOGIN_ATTEMPTS, attempts.toString());
} catch (error) {
throw new StorageError('设置登录尝试次数失败');
}
},
getLoginAttempts: () => {
try {
const attempts = localStorage.getItem(AUTH_CONSTANTS.STORAGE_KEYS.LOGIN_ATTEMPTS);
return attempts ? parseInt(attempts) : 0;
} catch (error) {
throw new StorageError('获取登录尝试次数失败');
}
},
removeLoginAttempts: () => {
try {
localStorage.removeItem(AUTH_CONSTANTS.STORAGE_KEYS.LOGIN_ATTEMPTS);
} catch (error) {
throw new StorageError('移除登录尝试次数失败');
}
},
// 最后登录尝试时间
setLastLoginAttempt: (timestamp: number) => {
try {
localStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.LAST_LOGIN_ATTEMPT, timestamp.toString());
} catch (error) {
throw new StorageError('设置最后登录尝试时间失败');
}
},
getLastLoginAttempt: () => {
try {
const timestamp = localStorage.getItem(AUTH_CONSTANTS.STORAGE_KEYS.LAST_LOGIN_ATTEMPT);
return timestamp ? parseInt(timestamp) : 0;
} catch (error) {
throw new StorageError('获取最后登录尝试时间失败');
}
},
removeLastLoginAttempt: () => {
try {
localStorage.removeItem(AUTH_CONSTANTS.STORAGE_KEYS.LAST_LOGIN_ATTEMPT);
} catch (error) {
throw new StorageError('移除最后登录尝试时间失败');
}
},
// 清除所有认证相关存储
clearAuth: () => {
try {
Object.values(AUTH_CONSTANTS.STORAGE_KEYS).forEach(key => {
localStorage.removeItem(key);
});
} catch (error) {
throw new StorageError('清除认证存储失败');
}
}
};

27
src/CellularManagement.WebUI/src/types/auth.ts

@ -2,14 +2,15 @@ import { Permission } from '@/constants/menuConfig';
export interface User {
id: string;
name: string;
userName: string;
email: string;
permissions: Permission[];
permissions: Record<string, boolean>;
}
export interface LoginRequest {
username: string;
password: string;
rememberMe: boolean;
}
export interface LoginResponse {
@ -23,18 +24,28 @@ export interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
userPermissions: Permission[];
userPermissions: string[];
rememberMe: boolean;
}
export type AuthAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: { user: User; accessToken: string; refreshToken: string; rememberMe: boolean } }
| { type: 'LOGIN_FAILURE'; payload: { error: string } }
| { type: 'LOGOUT' }
| { type: 'CLEAR_ERROR' }
| { type: 'SET_USER'; payload: { user: User; accessToken: string; refreshToken: string } }
| { type: 'SET_REMEMBER_ME'; payload: boolean };
export interface AuthContextType extends AuthState {
login: (request: LoginRequest) => Promise<void>;
logout: () => Promise<void>;
refreshToken: () => Promise<void>;
}
export interface OperationResult<T> {
successMessage: string | null;
errorMessages: string[] | null;
data: T | null;
isSuccess: boolean;
export interface OperationResult<T = void> {
success: boolean;
data?: T;
message?: string;
errorMessages?: string[];
}

19
src/CellularManagement.WebUI/src/utils/getUserInfo.ts

@ -0,0 +1,19 @@
interface User {
userId?: string;
userName?: string;
email?: string;
phoneNumber?: string;
}
export function getUserInfo(user: User | { user?: User } | null) {
let realUser: User | undefined;
if (user && typeof user === 'object' && 'user' in user) {
realUser = user.user;
} else {
realUser = user as User | undefined;
}
return {
userName: realUser?.userName || '未登录',
email: realUser?.email || ''
};
}
Loading…
Cancel
Save