init
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# ---> Rust
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
3957
Cargo.lock
generated
Normal file
3957
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
Cargo.toml
Normal file
39
Cargo.toml
Normal file
@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "uadmin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7.0"
|
||||
tokio = { version = "1.40", features = ["full"] }
|
||||
tower = "0.4.13"
|
||||
tower-http = { version = "0.5.0", features = ["cors"] }
|
||||
|
||||
# ORM - 使用更稳定的版本
|
||||
sea-orm = { version = "0.12.0", features = ["sqlx-mysql", "runtime-tokio-rustls", "macros"] }
|
||||
|
||||
# 序列化
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
serde_yaml = "0.9.0"
|
||||
|
||||
# JWT认证
|
||||
jsonwebtoken = "8.0.1"
|
||||
|
||||
# 密码加密
|
||||
bcrypt = "0.11.0"
|
||||
|
||||
# 环境变量
|
||||
dotenv = "0.15.0"
|
||||
|
||||
# 日志
|
||||
tracing = "0.1.32"
|
||||
tracing-subscriber = "0.3.9"
|
||||
|
||||
# 时间处理
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
|
||||
migration = { path = "migration" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.2"
|
||||
14
config.yml
Normal file
14
config.yml
Normal file
@ -0,0 +1,14 @@
|
||||
# UAdmin 配置文件
|
||||
server:
|
||||
host: "127.0.0.1"
|
||||
port: 3000
|
||||
|
||||
database:
|
||||
url: "mysql://root:123456@127.0.0.1:3306/uadmin"
|
||||
max_connections: 100
|
||||
connect_timeout: 8
|
||||
sqlx_logging: true
|
||||
|
||||
jwt:
|
||||
secret: "your-secret-key-here-please-change-in-production"
|
||||
expires_in: 86400 # 24 hours in seconds
|
||||
BIN
frontend/.DS_Store
vendored
Normal file
BIN
frontend/.DS_Store
vendored
Normal file
Binary file not shown.
46
frontend/README.md
Normal file
46
frontend/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
17346
frontend/package-lock.json
generated
Normal file
17346
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
frontend/package.json
Normal file
49
frontend/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.7.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.126",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"antd": "^5.27.0",
|
||||
"axios": "^1.11.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.8.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
frontend/public/index.html
Normal file
43
frontend/public/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
frontend/public/logo192.png
Normal file
BIN
frontend/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/logo512.png
Normal file
BIN
frontend/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
frontend/public/manifest.json
Normal file
25
frontend/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
frontend/public/robots.txt
Normal file
3
frontend/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
BIN
frontend/src/.DS_Store
vendored
Normal file
BIN
frontend/src/.DS_Store
vendored
Normal file
Binary file not shown.
38
frontend/src/App.css
Normal file
38
frontend/src/App.css
Normal file
@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
9
frontend/src/App.test.tsx
Normal file
9
frontend/src/App.test.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
87
frontend/src/App.tsx
Normal file
87
frontend/src/App.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import { AuthProvider, useAuth } from './hooks/useAuth';
|
||||
import Layout from './components/Layout';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Users from './pages/Users';
|
||||
import Roles from './pages/Roles';
|
||||
import Permissions from './pages/Permissions';
|
||||
import Menus from './pages/Menus';
|
||||
import './App.css';
|
||||
|
||||
// 受保护的路由组件
|
||||
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
// 公共路由组件(已登录用户不能访问)
|
||||
const PublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
{/* 公共路由 */}
|
||||
<Route path="/login" element={
|
||||
<PublicRoute>
|
||||
<Login />
|
||||
</PublicRoute>
|
||||
} />
|
||||
<Route path="/register" element={
|
||||
<PublicRoute>
|
||||
<Register />
|
||||
</PublicRoute>
|
||||
} />
|
||||
|
||||
{/* 受保护的路由 */}
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Layout />
|
||||
</ProtectedRoute>
|
||||
}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="users" element={<Users />} />
|
||||
<Route path="roles" element={<Roles />} />
|
||||
<Route path="permissions" element={<Permissions />} />
|
||||
<Route path="menus" element={<Menus />} />
|
||||
</Route>
|
||||
|
||||
{/* 404 重定向 */}
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
175
frontend/src/components/Layout.tsx
Normal file
175
frontend/src/components/Layout.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Layout as AntLayout,
|
||||
Menu,
|
||||
Button,
|
||||
Avatar,
|
||||
Dropdown,
|
||||
Typography,
|
||||
Space,
|
||||
MenuProps,
|
||||
} from 'antd';
|
||||
import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
SafetyOutlined,
|
||||
AppstoreOutlined,
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
DashboardOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
|
||||
const { Header, Sider, Content } = AntLayout;
|
||||
const { Title } = Typography;
|
||||
|
||||
const Layout: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: '/dashboard',
|
||||
icon: React.createElement(DashboardOutlined),
|
||||
label: '仪表盘',
|
||||
},
|
||||
{
|
||||
key: '/users',
|
||||
icon: React.createElement(UserOutlined),
|
||||
label: '用户管理',
|
||||
},
|
||||
{
|
||||
key: '/roles',
|
||||
icon: React.createElement(TeamOutlined),
|
||||
label: '角色管理',
|
||||
},
|
||||
{
|
||||
key: '/permissions',
|
||||
icon: React.createElement(SafetyOutlined),
|
||||
label: '权限管理',
|
||||
},
|
||||
{
|
||||
key: '/menus',
|
||||
icon: React.createElement(AppstoreOutlined),
|
||||
label: '菜单管理',
|
||||
},
|
||||
];
|
||||
|
||||
const handleMenuClick = ({ key }: { key: string }) => {
|
||||
navigate(key);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const userMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'profile',
|
||||
icon: React.createElement(SettingOutlined),
|
||||
label: '个人设置',
|
||||
onClick: () => navigate('/profile'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
icon: React.createElement(LogoutOutlined),
|
||||
label: '退出登录',
|
||||
onClick: handleLogout,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<AntLayout style={{ minHeight: '100vh' }}>
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
style={{
|
||||
background: '#fff',
|
||||
boxShadow: '2px 0 8px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
{!collapsed ? (
|
||||
<Title level={3} style={{ margin: 0, color: '#1890ff' }}>
|
||||
UAdmin
|
||||
</Title>
|
||||
) : (
|
||||
<Title level={3} style={{ margin: 0, color: '#1890ff' }}>
|
||||
U
|
||||
</Title>
|
||||
)}
|
||||
</div>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
style={{ border: 'none' }}
|
||||
/>
|
||||
</Sider>
|
||||
<AntLayout>
|
||||
<Header
|
||||
style={{
|
||||
padding: '0 16px',
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? React.createElement(MenuUnfoldOutlined) : React.createElement(MenuFoldOutlined)}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
width: 64,
|
||||
height: 64,
|
||||
}}
|
||||
/>
|
||||
<Space>
|
||||
<span>欢迎,{user?.username}</span>
|
||||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||
<Avatar
|
||||
style={{ backgroundColor: '#1890ff', cursor: 'pointer' }}
|
||||
icon={React.createElement(UserOutlined)}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Header>
|
||||
<Content
|
||||
style={{
|
||||
margin: '16px',
|
||||
padding: '24px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
minHeight: 'calc(100vh - 112px)',
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</AntLayout>
|
||||
</AntLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
113
frontend/src/hooks/useAuth.ts
Normal file
113
frontend/src/hooks/useAuth.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { useState, useEffect, createContext, useContext, ReactNode } from 'react';
|
||||
import { User, LoginRequest, RegisterRequest } from '../types';
|
||||
import { apiService } from '../services/api';
|
||||
import { message } from 'antd';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
loading: boolean;
|
||||
login: (data: LoginRequest) => Promise<boolean>;
|
||||
register: (data: RegisterRequest) => Promise<boolean>;
|
||||
logout: () => void;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// 从localStorage恢复认证状态
|
||||
const savedToken = localStorage.getItem('token');
|
||||
const savedUser = localStorage.getItem('user');
|
||||
|
||||
if (savedToken && savedUser) {
|
||||
try {
|
||||
setToken(savedToken);
|
||||
setUser(JSON.parse(savedUser));
|
||||
} catch (error) {
|
||||
console.error('Failed to parse saved user data:', error);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = async (data: LoginRequest): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiService.login(data);
|
||||
|
||||
setToken(response.token);
|
||||
setUser(response.user);
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('token', response.token);
|
||||
localStorage.setItem('user', JSON.stringify(response.user));
|
||||
|
||||
message.success('登录成功');
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error('Login failed:', error);
|
||||
message.error(error.response?.data?.message || '登录失败');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (data: RegisterRequest): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await apiService.register(data);
|
||||
message.success('注册成功,请登录');
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error('Register failed:', error);
|
||||
message.error(error.response?.data?.message || '注册失败');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await apiService.logout();
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
} finally {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
message.success('已退出登录');
|
||||
}
|
||||
};
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
isAuthenticated: !!token && !!user,
|
||||
};
|
||||
|
||||
return React.createElement(AuthContext.Provider, { value }, children);
|
||||
};
|
||||
13
frontend/src/index.css
Normal file
13
frontend/src/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
19
frontend/src/index.tsx
Normal file
19
frontend/src/index.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
1
frontend/src/logo.svg
Normal file
1
frontend/src/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
342
frontend/src/pages/Dashboard.tsx
Normal file
342
frontend/src/pages/Dashboard.tsx
Normal file
@ -0,0 +1,342 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Avatar,
|
||||
List,
|
||||
Typography,
|
||||
Progress,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
SafetyOutlined,
|
||||
MenuOutlined,
|
||||
TrophyOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface DashboardStats {
|
||||
totalUsers: number;
|
||||
totalRoles: number;
|
||||
totalPermissions: number;
|
||||
totalMenus: number;
|
||||
}
|
||||
|
||||
interface RecentActivity {
|
||||
id: number;
|
||||
action: string;
|
||||
target: string;
|
||||
user: string;
|
||||
timestamp: string;
|
||||
type: 'create' | 'update' | 'delete';
|
||||
}
|
||||
|
||||
interface SystemInfo {
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
diskUsage: number;
|
||||
uptime: string;
|
||||
}
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
totalUsers: 0,
|
||||
totalRoles: 0,
|
||||
totalPermissions: 0,
|
||||
totalMenus: 0,
|
||||
});
|
||||
const [recentActivities, setRecentActivities] = useState<RecentActivity[]>([]);
|
||||
const [systemInfo, setSystemInfo] = useState<SystemInfo>({
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0,
|
||||
uptime: '0天',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 模拟数据加载
|
||||
useEffect(() => {
|
||||
const loadDashboardData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 模拟统计数据
|
||||
setStats({
|
||||
totalUsers: 156,
|
||||
totalRoles: 8,
|
||||
totalPermissions: 24,
|
||||
totalMenus: 12,
|
||||
});
|
||||
|
||||
// 模拟最近活动
|
||||
setRecentActivities([
|
||||
{
|
||||
id: 1,
|
||||
action: '创建用户',
|
||||
target: 'john_doe',
|
||||
user: 'admin',
|
||||
timestamp: '2024-01-15 10:30:00',
|
||||
type: 'create',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
action: '更新角色',
|
||||
target: '管理员',
|
||||
user: 'admin',
|
||||
timestamp: '2024-01-15 09:45:00',
|
||||
type: 'update',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
action: '删除权限',
|
||||
target: 'users:delete',
|
||||
user: 'admin',
|
||||
timestamp: '2024-01-15 09:15:00',
|
||||
type: 'delete',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
action: '创建菜单',
|
||||
target: '系统设置',
|
||||
user: 'admin',
|
||||
timestamp: '2024-01-15 08:30:00',
|
||||
type: 'create',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
action: '更新用户',
|
||||
target: 'jane_smith',
|
||||
user: 'admin',
|
||||
timestamp: '2024-01-14 16:20:00',
|
||||
type: 'update',
|
||||
},
|
||||
]);
|
||||
|
||||
// 模拟系统信息
|
||||
setSystemInfo({
|
||||
cpuUsage: Math.floor(Math.random() * 100),
|
||||
memoryUsage: Math.floor(Math.random() * 100),
|
||||
diskUsage: Math.floor(Math.random() * 100),
|
||||
uptime: '15天 8小时 32分钟',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载仪表板数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
const getActivityTypeColor = (type: RecentActivity['type']) => {
|
||||
switch (type) {
|
||||
case 'create':
|
||||
return 'green';
|
||||
case 'update':
|
||||
return 'blue';
|
||||
case 'delete':
|
||||
return 'red';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getActivityTypeIcon = (type: RecentActivity['type']) => {
|
||||
switch (type) {
|
||||
case 'create':
|
||||
return React.createElement(TrophyOutlined);
|
||||
case 'update':
|
||||
return React.createElement(ClockCircleOutlined);
|
||||
case 'delete':
|
||||
return React.createElement(ClockCircleOutlined);
|
||||
default:
|
||||
return React.createElement(ClockCircleOutlined);
|
||||
}
|
||||
};
|
||||
|
||||
const activityColumns: ColumnsType<RecentActivity> = [
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
render: (action: string, record: RecentActivity) => (
|
||||
<Space>
|
||||
<Tag color={getActivityTypeColor(record.type)}>
|
||||
{action}
|
||||
</Tag>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '目标',
|
||||
dataIndex: 'target',
|
||||
key: 'target',
|
||||
render: (target: string) => (
|
||||
<Text code>{target}</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'user',
|
||||
key: 'user',
|
||||
render: (user: string) => (
|
||||
<Space>
|
||||
<Avatar size="small" icon={React.createElement(UserOutlined)} />
|
||||
{user}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 欢迎信息 */}
|
||||
<Card style={{ marginBottom: 24 }}>
|
||||
<Row align="middle">
|
||||
<Col>
|
||||
<Avatar size={64} icon={React.createElement(UserOutlined)} />
|
||||
</Col>
|
||||
<Col style={{ marginLeft: 16 }}>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
欢迎回来,{user?.username}!
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
今天是 {new Date().toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
})}
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="用户总数"
|
||||
value={stats.totalUsers}
|
||||
prefix={React.createElement(UserOutlined)}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="角色总数"
|
||||
value={stats.totalRoles}
|
||||
prefix={React.createElement(TeamOutlined)}
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="权限总数"
|
||||
value={stats.totalPermissions}
|
||||
prefix={React.createElement(SafetyOutlined)}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="菜单总数"
|
||||
value={stats.totalMenus}
|
||||
prefix={React.createElement(MenuOutlined)}
|
||||
valueStyle={{ color: '#722ed1' }}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
{/* 最近活动 */}
|
||||
<Col xs={24} lg={16}>
|
||||
<Card title="最近活动" style={{ height: 500 }}>
|
||||
<Table
|
||||
columns={activityColumns}
|
||||
dataSource={recentActivities}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 系统信息 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card title="系统信息" style={{ height: 500 }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
<div>
|
||||
<Text strong>CPU 使用率</Text>
|
||||
<Progress
|
||||
percent={systemInfo.cpuUsage}
|
||||
status={systemInfo.cpuUsage > 80 ? 'exception' : 'active'}
|
||||
strokeColor={systemInfo.cpuUsage > 80 ? '#ff4d4f' : '#52c41a'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>内存使用率</Text>
|
||||
<Progress
|
||||
percent={systemInfo.memoryUsage}
|
||||
status={systemInfo.memoryUsage > 80 ? 'exception' : 'active'}
|
||||
strokeColor={systemInfo.memoryUsage > 80 ? '#ff4d4f' : '#1890ff'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>磁盘使用率</Text>
|
||||
<Progress
|
||||
percent={systemInfo.diskUsage}
|
||||
status={systemInfo.diskUsage > 80 ? 'exception' : 'active'}
|
||||
strokeColor={systemInfo.diskUsage > 80 ? '#ff4d4f' : '#faad14'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong>系统运行时间</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text code>{systemInfo.uptime}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
107
frontend/src/pages/Login.tsx
Normal file
107
frontend/src/pages/Login.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Input, Button, Card, Typography, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { LoginRequest } from '../types';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = async (values: LoginRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const success = await login(values);
|
||||
if (success) {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
}}>
|
||||
<Card
|
||||
style={{
|
||||
width: 400,
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
<Title level={2} style={{ color: '#1890ff', marginBottom: 8 }}>
|
||||
UAdmin
|
||||
</Title>
|
||||
<Typography.Text type="secondary">
|
||||
用户权限管理系统
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名!' },
|
||||
{ min: 3, message: '用户名至少3个字符!' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码!' },
|
||||
{ min: 6, message: '密码至少6个字符!' }
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Text type="secondary">
|
||||
还没有账号? <Link to="/register">立即注册</Link>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
421
frontend/src/pages/Menus.tsx
Normal file
421
frontend/src/pages/Menus.tsx
Normal file
@ -0,0 +1,421 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Popconfirm,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
InputNumber,
|
||||
TreeSelect,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Menu, CreateMenuRequest, UpdateMenuRequest, QueryParams } from '../types';
|
||||
import { apiService } from '../services/api';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface TreeNode {
|
||||
title: string;
|
||||
value: number;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
const Menus: React.FC = () => {
|
||||
const [menus, setMenus] = useState<Menu[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingMenu, setEditingMenu] = useState<Menu | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [searchForm] = Form.useForm();
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchMenus = async (params?: QueryParams) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiService.getMenus({
|
||||
page: pagination.current,
|
||||
per_page: pagination.pageSize,
|
||||
...params,
|
||||
});
|
||||
setMenus(response.data || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.total,
|
||||
current: response.page,
|
||||
}));
|
||||
} catch (error) {
|
||||
message.error('获取菜单列表失败');
|
||||
setMenus([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMenus();
|
||||
}, [pagination.current, pagination.pageSize]);
|
||||
|
||||
const handleSearch = (values: any) => {
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchMenus(values);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.resetFields();
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchMenus();
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingMenu(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (menu: Menu) => {
|
||||
setEditingMenu(menu);
|
||||
form.setFieldsValue({
|
||||
name: menu.name,
|
||||
path: menu.path,
|
||||
icon: menu.icon,
|
||||
parent_id: menu.parent_id,
|
||||
sort_order: menu.sort_order,
|
||||
status: menu.status,
|
||||
});
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await apiService.deleteMenu(id);
|
||||
message.success('删除成功');
|
||||
fetchMenus();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: CreateMenuRequest | UpdateMenuRequest) => {
|
||||
try {
|
||||
if (editingMenu) {
|
||||
await apiService.updateMenu(editingMenu.id, values as UpdateMenuRequest);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await apiService.createMenu(values as CreateMenuRequest);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setModalVisible(false);
|
||||
fetchMenus();
|
||||
} catch (error) {
|
||||
message.error(editingMenu ? '更新失败' : '创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 构建树形结构数据
|
||||
const buildTreeData = (menus: Menu[]): TreeNode[] => {
|
||||
if (!menus || !Array.isArray(menus)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const menuMap = new Map<number, Menu>();
|
||||
menus.forEach(menu => menuMap.set(menu.id, menu));
|
||||
|
||||
const buildTree = (parentId: number | null = null): TreeNode[] => {
|
||||
return menus
|
||||
.filter(menu => menu.parent_id === parentId)
|
||||
.map(menu => ({
|
||||
title: menu.name,
|
||||
value: menu.id,
|
||||
children: buildTree(menu.id),
|
||||
}));
|
||||
};
|
||||
|
||||
return buildTree();
|
||||
};
|
||||
|
||||
const treeData = buildTreeData(menus);
|
||||
|
||||
const columns: ColumnsType<Menu> = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '菜单名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '路径',
|
||||
dataIndex: 'path',
|
||||
key: 'path',
|
||||
render: (path: string) => (
|
||||
<code style={{ background: '#f5f5f5', padding: '2px 4px', borderRadius: '3px' }}>
|
||||
{path}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
dataIndex: 'icon',
|
||||
key: 'icon',
|
||||
render: (icon: string) => icon ? (
|
||||
<Tag color="blue">{icon}</Tag>
|
||||
) : '-',
|
||||
},
|
||||
{
|
||||
title: '父级菜单',
|
||||
dataIndex: 'parent_id',
|
||||
key: 'parent_id',
|
||||
render: (parentId: number | null) => {
|
||||
if (!parentId) return '-';
|
||||
const parentMenu = menus.find(m => m.id === parentId);
|
||||
return parentMenu ? parentMenu.name : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort_order',
|
||||
key: 'sort_order',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={status === 'active' ? 'green' : 'red'}>
|
||||
{status === 'active' ? '激活' : '禁用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="link"
|
||||
icon={React.createElement(EditOutlined)}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个菜单吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={React.createElement(DeleteOutlined)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Form
|
||||
form={searchForm}
|
||||
layout="inline"
|
||||
onFinish={handleSearch}
|
||||
>
|
||||
<Row gutter={16} style={{ width: '100%' }}>
|
||||
<Col span={6}>
|
||||
<Form.Item name="search" label="搜索">
|
||||
<Input placeholder="菜单名称或路径" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select placeholder="选择状态" allowClear>
|
||||
<Option value="active">激活</Option>
|
||||
<Option value="inactive">禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={React.createElement(SearchOutlined)}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={React.createElement(PlusOutlined)}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
新增菜单
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={menus}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize || 10,
|
||||
}));
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingMenu ? '编辑菜单' : '新增菜单'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="菜单名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入菜单名称' },
|
||||
{ min: 2, message: '菜单名称至少2个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入菜单名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="path"
|
||||
label="菜单路径"
|
||||
rules={[
|
||||
{ required: true, message: '请输入菜单路径' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入菜单路径,如:/users, /roles" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="icon"
|
||||
label="图标"
|
||||
>
|
||||
<Input placeholder="请输入图标名称,如:UserOutlined" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="parent_id"
|
||||
label="父级菜单"
|
||||
>
|
||||
<TreeSelect
|
||||
placeholder="请选择父级菜单(可选)"
|
||||
allowClear
|
||||
treeData={treeData}
|
||||
treeDefaultExpandAll
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="sort_order"
|
||||
label="排序"
|
||||
rules={[
|
||||
{ required: true, message: '请输入排序值' },
|
||||
]}
|
||||
initialValue={0}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={9999}
|
||||
placeholder="请输入排序值"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{editingMenu && (
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[{ required: true, message: '请选择状态' }]}
|
||||
>
|
||||
<Select placeholder="请选择状态">
|
||||
<Option value="active">激活</Option>
|
||||
<Option value="inactive">禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item style={{ textAlign: 'right', marginBottom: 0 }}>
|
||||
<Space>
|
||||
<Button onClick={() => setModalVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{editingMenu ? '更新' : '创建'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menus;
|
||||
311
frontend/src/pages/Permissions.tsx
Normal file
311
frontend/src/pages/Permissions.tsx
Normal file
@ -0,0 +1,311 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Popconfirm,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Permission, CreatePermissionRequest, UpdatePermissionRequest, QueryParams } from '../types';
|
||||
import { apiService } from '../services/api';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const Permissions: React.FC = () => {
|
||||
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingPermission, setEditingPermission] = useState<Permission | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [searchForm] = Form.useForm();
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchPermissions = async (params?: QueryParams) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiService.getPermissions({
|
||||
page: pagination.current,
|
||||
per_page: pagination.pageSize,
|
||||
...params,
|
||||
});
|
||||
setPermissions(response.data || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.total,
|
||||
current: response.page,
|
||||
}));
|
||||
} catch (error) {
|
||||
message.error('获取权限列表失败');
|
||||
setPermissions([]); } finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPermissions();
|
||||
}, [pagination.current, pagination.pageSize]);
|
||||
|
||||
const handleSearch = (values: any) => {
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchPermissions(values);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.resetFields();
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchPermissions();
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingPermission(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (permission: Permission) => {
|
||||
setEditingPermission(permission);
|
||||
form.setFieldsValue({
|
||||
name: permission.name,
|
||||
key: permission.key,
|
||||
description: permission.description,
|
||||
});
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await apiService.deletePermission(id);
|
||||
message.success('删除成功');
|
||||
fetchPermissions();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: CreatePermissionRequest | UpdatePermissionRequest) => {
|
||||
try {
|
||||
if (editingPermission) {
|
||||
await apiService.updatePermission(editingPermission.id, values as UpdatePermissionRequest);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await apiService.createPermission(values as CreatePermissionRequest);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setModalVisible(false);
|
||||
fetchPermissions();
|
||||
} catch (error) {
|
||||
message.error(editingPermission ? '更新失败' : '创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<Permission> = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '权限名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '权限标识',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
render: (key: string) => (
|
||||
<Tag color="blue">{key}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action_buttons',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="link"
|
||||
icon={React.createElement(EditOutlined)}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个权限吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={React.createElement(DeleteOutlined)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Form
|
||||
form={searchForm}
|
||||
layout="inline"
|
||||
onFinish={handleSearch}
|
||||
>
|
||||
<Row gutter={16} style={{ width: '100%' }}>
|
||||
<Col span={6}>
|
||||
<Form.Item name="search" label="搜索">
|
||||
<Input placeholder="权限名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="key" label="权限标识">
|
||||
<Input placeholder="权限标识" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={React.createElement(SearchOutlined)}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={React.createElement(PlusOutlined)}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
新增权限
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={permissions}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize || 10,
|
||||
}));
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingPermission ? '编辑权限' : '新增权限'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="权限名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入权限名称' },
|
||||
{ min: 2, message: '权限名称至少2个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入权限名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="key"
|
||||
label="权限标识"
|
||||
rules={[
|
||||
{ required: true, message: '请输入权限标识' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入权限标识,如:users:create, roles:read" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
>
|
||||
<TextArea rows={4} placeholder="请输入权限描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ textAlign: 'right', marginBottom: 0 }}>
|
||||
<Space>
|
||||
<Button onClick={() => setModalVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{editingPermission ? '更新' : '创建'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Permissions;
|
||||
143
frontend/src/pages/Register.tsx
Normal file
143
frontend/src/pages/Register.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Input, Button, Card, Typography, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { RegisterRequest } from '../types';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const Register: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFinish = async (values: RegisterRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const success = await register(values);
|
||||
if (success) {
|
||||
navigate('/login');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Register error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
}}>
|
||||
<Card
|
||||
style={{
|
||||
width: 400,
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
<Title level={2} style={{ color: '#1890ff', marginBottom: 8 }}>
|
||||
UAdmin
|
||||
</Title>
|
||||
<Typography.Text type="secondary">
|
||||
用户权限管理系统 - 注册
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
name="register"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名!' },
|
||||
{ min: 3, message: '用户名至少3个字符!' },
|
||||
{ max: 20, message: '用户名最多20个字符!' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱!' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址!' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined />}
|
||||
placeholder="邮箱"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码!' },
|
||||
{ min: 6, message: '密码至少6个字符!' },
|
||||
{ max: 20, message: '密码最多20个字符!' }
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认密码!' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致!'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="确认密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Text type="secondary">
|
||||
已有账号? <Link to="/login">立即登录</Link>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
413
frontend/src/pages/Roles.tsx
Normal file
413
frontend/src/pages/Roles.tsx
Normal file
@ -0,0 +1,413 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Popconfirm,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
Transfer,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Role, Permission, CreateRoleRequest, UpdateRoleRequest, QueryParams } from '../types';
|
||||
import { apiService } from '../services/api';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { TransferDirection } from 'antd/es/transfer';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface TransferItem {
|
||||
key: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const Roles: React.FC = () => {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<Role | null>(null);
|
||||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [searchForm] = Form.useForm();
|
||||
const [targetKeys, setTargetKeys] = useState<string[]>([]);
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchRoles = async (params?: QueryParams) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiService.getRoles({
|
||||
page: pagination.current,
|
||||
per_page: pagination.pageSize,
|
||||
...params,
|
||||
});
|
||||
setRoles(response.data || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.total,
|
||||
current: response.page,
|
||||
}));
|
||||
} catch (error) {
|
||||
message.error('获取角色列表失败');
|
||||
setRoles([]); } finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPermissions = async () => {
|
||||
try {
|
||||
const response = await apiService.getPermissions({ page: 1, per_page: 1000 });
|
||||
setPermissions(response.data || []);
|
||||
} catch (error) {
|
||||
message.error('获取权限列表失败');
|
||||
setPermissions([]); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
fetchPermissions();
|
||||
}, [pagination.current, pagination.pageSize]);
|
||||
|
||||
const handleSearch = (values: any) => {
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchRoles(values);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.resetFields();
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingRole(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (role: Role) => {
|
||||
setEditingRole(role);
|
||||
form.setFieldsValue({
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
status: role.status,
|
||||
});
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await apiService.deleteRole(id);
|
||||
message.success('删除成功');
|
||||
fetchRoles();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: CreateRoleRequest | UpdateRoleRequest) => {
|
||||
try {
|
||||
if (editingRole) {
|
||||
await apiService.updateRole(editingRole.id, values as UpdateRoleRequest);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await apiService.createRole(values as CreateRoleRequest);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setModalVisible(false);
|
||||
fetchRoles();
|
||||
} catch (error) {
|
||||
message.error(editingRole ? '更新失败' : '创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleManagePermissions = (role: Role) => {
|
||||
setSelectedRole(role);
|
||||
const rolePermissionIds: string[] = [];
|
||||
setTargetKeys(rolePermissionIds);
|
||||
setPermissionModalVisible(true);
|
||||
};
|
||||
|
||||
const handlePermissionChange = (newTargetKeys: React.Key[], direction: TransferDirection, moveKeys: React.Key[]) => {
|
||||
setTargetKeys(newTargetKeys.map(key => key.toString()));
|
||||
};
|
||||
|
||||
const handleSavePermissions = async () => {
|
||||
if (!selectedRole) return;
|
||||
|
||||
try {
|
||||
const permissionIds = targetKeys.map(key => parseInt(key));
|
||||
// TODO: Implement assignRolePermissions API method
|
||||
// await apiService.assignRolePermissions(selectedRole.id, permissionIds);
|
||||
message.success('权限分配成功');
|
||||
setPermissionModalVisible(false);
|
||||
fetchRoles();
|
||||
} catch (error) {
|
||||
message.error('权限分配失败');
|
||||
}
|
||||
};
|
||||
|
||||
const transferData: TransferItem[] = permissions.map(permission => ({
|
||||
key: permission.id.toString(),
|
||||
title: permission.name,
|
||||
description: permission.description,
|
||||
}));
|
||||
|
||||
const columns: ColumnsType<Role> = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '权限数量',
|
||||
key: 'permissions_count',
|
||||
render: (_, record) => (
|
||||
<Tag color="blue">
|
||||
0 个权限
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={status === 'active' ? 'green' : 'red'}>
|
||||
{status === 'active' ? '激活' : '禁用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handleManagePermissions(record)}
|
||||
>
|
||||
权限管理
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
icon={React.createElement(EditOutlined)}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个角色吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={React.createElement(DeleteOutlined)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Form
|
||||
form={searchForm}
|
||||
layout="inline"
|
||||
onFinish={handleSearch}
|
||||
>
|
||||
<Row gutter={16} style={{ width: '100%' }}>
|
||||
<Col span={6}>
|
||||
<Form.Item name="search" label="搜索">
|
||||
<Input placeholder="角色名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select placeholder="选择状态" allowClear>
|
||||
<Option value="active">激活</Option>
|
||||
<Option value="inactive">禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={React.createElement(SearchOutlined)}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={React.createElement(PlusOutlined)}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
新增角色
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={roles}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize || 10,
|
||||
}));
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 角色编辑模态框 */}
|
||||
<Modal
|
||||
title={editingRole ? '编辑角色' : '新增角色'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="角色名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入角色名称' },
|
||||
{ min: 2, message: '角色名称至少2个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入角色名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
>
|
||||
<TextArea rows={4} placeholder="请输入角色描述" />
|
||||
</Form.Item>
|
||||
|
||||
{editingRole && (
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[{ required: true, message: '请选择状态' }]}
|
||||
>
|
||||
<Select placeholder="请选择状态">
|
||||
<Option value="active">激活</Option>
|
||||
<Option value="inactive">禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item style={{ textAlign: 'right', marginBottom: 0 }}>
|
||||
<Space>
|
||||
<Button onClick={() => setModalVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{editingRole ? '更新' : '创建'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 权限管理模态框 */}
|
||||
<Modal
|
||||
title={`管理角色权限 - ${selectedRole?.name}`}
|
||||
open={permissionModalVisible}
|
||||
onCancel={() => setPermissionModalVisible(false)}
|
||||
onOk={handleSavePermissions}
|
||||
width={800}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Transfer
|
||||
dataSource={transferData}
|
||||
titles={['可用权限', '已分配权限']}
|
||||
targetKeys={targetKeys}
|
||||
onChange={handlePermissionChange}
|
||||
render={item => item.title}
|
||||
showSearch
|
||||
listStyle={{
|
||||
width: 350,
|
||||
height: 400,
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Roles;
|
||||
347
frontend/src/pages/Users.tsx
Normal file
347
frontend/src/pages/Users.tsx
Normal file
@ -0,0 +1,347 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Popconfirm,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { User, CreateUserRequest, UpdateUserRequest, QueryParams } from '../types';
|
||||
import { apiService } from '../services/api';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const Users: React.FC = () => {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [searchForm] = Form.useForm();
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchUsers = async (params?: QueryParams) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiService.getUsers({
|
||||
page: pagination.current,
|
||||
per_page: pagination.pageSize,
|
||||
...params,
|
||||
});
|
||||
setUsers(response.data || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: response.total,
|
||||
current: response.page,
|
||||
}));
|
||||
} catch (error) {
|
||||
message.error('获取用户列表失败');
|
||||
setUsers([]); } finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, [pagination.current, pagination.pageSize]);
|
||||
|
||||
const handleSearch = (values: any) => {
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchUsers(values);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.resetFields();
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingUser(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
setEditingUser(user);
|
||||
form.setFieldsValue({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
status: user.status,
|
||||
});
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await apiService.deleteUser(id);
|
||||
message.success('删除成功');
|
||||
fetchUsers();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: CreateUserRequest | UpdateUserRequest) => {
|
||||
try {
|
||||
if (editingUser) {
|
||||
await apiService.updateUser(editingUser.id, values as UpdateUserRequest);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await apiService.createUser(values as CreateUserRequest);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setModalVisible(false);
|
||||
fetchUsers();
|
||||
} catch (error) {
|
||||
message.error(editingUser ? '更新失败' : '创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<User> = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={status === 'active' ? 'green' : 'red'}>
|
||||
{status === 'active' ? '激活' : '禁用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="link"
|
||||
icon={React.createElement(EditOutlined)}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个用户吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={React.createElement(DeleteOutlined)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Form
|
||||
form={searchForm}
|
||||
layout="inline"
|
||||
onFinish={handleSearch}
|
||||
>
|
||||
<Row gutter={16} style={{ width: '100%' }}>
|
||||
<Col span={6}>
|
||||
<Form.Item name="search" label="搜索">
|
||||
<Input placeholder="用户名或邮箱" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select placeholder="选择状态" allowClear>
|
||||
<Option value="active">激活</Option>
|
||||
<Option value="inactive">禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={React.createElement(SearchOutlined)}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={React.createElement(PlusOutlined)}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
新增用户
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: page,
|
||||
pageSize: pageSize || 10,
|
||||
}));
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingUser ? '编辑用户' : '新增用户'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少3个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="邮箱"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
|
||||
{!editingUser && (
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label="手机号"
|
||||
>
|
||||
<Input placeholder="请输入手机号" />
|
||||
</Form.Item>
|
||||
|
||||
{editingUser && (
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
rules={[{ required: true, message: '请选择状态' }]}
|
||||
>
|
||||
<Select placeholder="请选择状态">
|
||||
<Option value="active">激活</Option>
|
||||
<Option value="inactive">禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item style={{ textAlign: 'right', marginBottom: 0 }}>
|
||||
<Space>
|
||||
<Button onClick={() => setModalVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{editingUser ? '更新' : '创建'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
||||
1
frontend/src/react-app-env.d.ts
vendored
Normal file
1
frontend/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
15
frontend/src/reportWebVitals.ts
Normal file
15
frontend/src/reportWebVitals.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
229
frontend/src/services/api.ts
Normal file
229
frontend/src/services/api.ts
Normal file
@ -0,0 +1,229 @@
|
||||
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import {
|
||||
User,
|
||||
Role,
|
||||
Permission,
|
||||
Menu,
|
||||
CreateUserRequest,
|
||||
UpdateUserRequest,
|
||||
CreateRoleRequest,
|
||||
UpdateRoleRequest,
|
||||
CreatePermissionRequest,
|
||||
UpdatePermissionRequest,
|
||||
CreateMenuRequest,
|
||||
UpdateMenuRequest,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest,
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
QueryParams,
|
||||
} from '../types';
|
||||
|
||||
class ApiService {
|
||||
private api: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.api = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器 - 添加认证token
|
||||
this.api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器 - 处理错误
|
||||
this.api.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token过期或无效,清除本地存储并跳转到登录页
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 认证相关API
|
||||
async login(data: LoginRequest): Promise<LoginResponse> {
|
||||
const response = await this.api.post<ApiResponse<LoginResponse>>('/auth/login', data);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async register(data: RegisterRequest): Promise<User> {
|
||||
const response = await this.api.post<ApiResponse<User>>('/auth/register', data);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.api.post('/auth/logout');
|
||||
}
|
||||
|
||||
// 用户管理API
|
||||
async getUsers(params?: QueryParams): Promise<PaginatedResponse<User>> {
|
||||
const response = await this.api.get('/users', { params });
|
||||
const backendData = response.data.data;
|
||||
// 转换后端数据结构到前端期望的结构
|
||||
return {
|
||||
data: backendData.items.map((user: any) => ({
|
||||
...user,
|
||||
status: user.status === 1 ? 'active' : 'inactive'
|
||||
})),
|
||||
total: backendData.total,
|
||||
page: backendData.page,
|
||||
per_page: backendData.page_size,
|
||||
total_pages: Math.ceil(backendData.total / backendData.page_size)
|
||||
};
|
||||
}
|
||||
|
||||
async getUser(id: number): Promise<User> {
|
||||
const response = await this.api.get<ApiResponse<User>>(`/users/${id}`);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async createUser(data: CreateUserRequest): Promise<User> {
|
||||
const response = await this.api.post<ApiResponse<User>>('/users', data);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async updateUser(id: number, data: UpdateUserRequest): Promise<User> {
|
||||
// 转换前端的字符串状态为后端期望的数字格式
|
||||
const backendData = {
|
||||
...data,
|
||||
status: data.status ? (data.status === 'active' ? 1 : 0) : undefined
|
||||
};
|
||||
const response = await this.api.put<ApiResponse<User>>(`/users/${id}`, backendData);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async deleteUser(id: number): Promise<void> {
|
||||
await this.api.delete(`/users/${id}`);
|
||||
}
|
||||
|
||||
// 角色管理API
|
||||
async getRoles(params?: QueryParams): Promise<PaginatedResponse<Role>> {
|
||||
const response = await this.api.get<ApiResponse<PaginatedResponse<Role>>>('/roles', { params });
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async getAllRoles(): Promise<Role[]> {
|
||||
const response = await this.api.get<ApiResponse<Role[]>>('/roles/all');
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async getRole(id: number): Promise<Role> {
|
||||
const response = await this.api.get<ApiResponse<Role>>(`/roles/${id}`);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async createRole(data: CreateRoleRequest): Promise<Role> {
|
||||
const response = await this.api.post<ApiResponse<Role>>('/roles', data);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async updateRole(id: number, data: UpdateRoleRequest): Promise<Role> {
|
||||
// 转换前端的字符串状态为后端期望的数字格式
|
||||
const backendData = {
|
||||
...data,
|
||||
status: data.status ? (data.status === 'active' ? 1 : 0) : undefined
|
||||
};
|
||||
const response = await this.api.put<ApiResponse<Role>>(`/roles/${id}`, backendData);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async deleteRole(id: number): Promise<void> {
|
||||
await this.api.delete(`/roles/${id}`);
|
||||
}
|
||||
|
||||
// 权限管理API
|
||||
async getPermissions(params?: QueryParams): Promise<PaginatedResponse<Permission>> {
|
||||
const response = await this.api.get<ApiResponse<PaginatedResponse<Permission>>>('/permissions', { params });
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async getAllPermissions(): Promise<Permission[]> {
|
||||
const response = await this.api.get<ApiResponse<Permission[]>>('/permissions/all');
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async getPermission(id: number): Promise<Permission> {
|
||||
const response = await this.api.get<ApiResponse<Permission>>(`/permissions/${id}`);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async createPermission(data: CreatePermissionRequest): Promise<Permission> {
|
||||
const response = await this.api.post<ApiResponse<Permission>>('/permissions', data);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async updatePermission(id: number, data: UpdatePermissionRequest): Promise<Permission> {
|
||||
// 转换前端的字符串状态为后端期望的数字格式
|
||||
const backendData = {
|
||||
...data,
|
||||
status: data.status ? (data.status === 'active' ? 1 : 0) : undefined
|
||||
};
|
||||
const response = await this.api.put<ApiResponse<Permission>>(`/permissions/${id}`, backendData);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async deletePermission(id: number): Promise<void> {
|
||||
await this.api.delete(`/permissions/${id}`);
|
||||
}
|
||||
|
||||
// 菜单管理API
|
||||
async getMenus(params?: QueryParams): Promise<PaginatedResponse<Menu>> {
|
||||
const response = await this.api.get<ApiResponse<PaginatedResponse<Menu>>>('/menus', { params });
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async getAllMenus(): Promise<Menu[]> {
|
||||
const response = await this.api.get<ApiResponse<Menu[]>>('/menus/all');
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async getMenu(id: number): Promise<Menu> {
|
||||
const response = await this.api.get<ApiResponse<Menu>>(`/menus/${id}`);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async createMenu(data: CreateMenuRequest): Promise<Menu> {
|
||||
const response = await this.api.post<ApiResponse<Menu>>('/menus', data);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async updateMenu(id: number, data: UpdateMenuRequest): Promise<Menu> {
|
||||
// 转换前端的字符串状态为后端期望的数字格式
|
||||
const backendData = {
|
||||
...data,
|
||||
status: data.status ? (data.status === 'active' ? 1 : 0) : undefined
|
||||
};
|
||||
const response = await this.api.put<ApiResponse<Menu>>(`/menus/${id}`, backendData);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
async deleteMenu(id: number): Promise<void> {
|
||||
await this.api.delete(`/menus/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const apiService = new ApiService();
|
||||
export default apiService;
|
||||
5
frontend/src/setupTests.ts
Normal file
5
frontend/src/setupTests.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
143
frontend/src/types/index.ts
Normal file
143
frontend/src/types/index.ts
Normal file
@ -0,0 +1,143 @@
|
||||
// 用户相关类型
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
status: 'active' | 'inactive';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
username?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
status?: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
// 角色相关类型
|
||||
export interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
status: 'active' | 'inactive';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateRoleRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateRoleRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
status?: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
// 权限相关类型
|
||||
export interface Permission {
|
||||
id: number;
|
||||
name: string;
|
||||
key: string;
|
||||
description?: string;
|
||||
status: 'active' | 'inactive';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreatePermissionRequest {
|
||||
name: string;
|
||||
key: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdatePermissionRequest {
|
||||
name?: string;
|
||||
key?: string;
|
||||
description?: string;
|
||||
status?: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
// 菜单相关类型
|
||||
export interface Menu {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
icon?: string;
|
||||
parent_id?: number;
|
||||
sort_order: number;
|
||||
status: 'active' | 'inactive';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
children?: Menu[];
|
||||
}
|
||||
|
||||
export interface CreateMenuRequest {
|
||||
name: string;
|
||||
path: string;
|
||||
icon?: string;
|
||||
parent_id?: number;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
export interface UpdateMenuRequest {
|
||||
name?: string;
|
||||
path?: string;
|
||||
icon?: string;
|
||||
parent_id?: number;
|
||||
sort_order?: number;
|
||||
status?: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
// 认证相关类型
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
// 表格查询参数
|
||||
export interface QueryParams {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
status?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
26
frontend/tsconfig.json
Normal file
26
frontend/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
BIN
migration/.DS_Store
vendored
Normal file
BIN
migration/.DS_Store
vendored
Normal file
Binary file not shown.
2935
migration/Cargo.lock
generated
Normal file
2935
migration/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
migration/Cargo.toml
Normal file
18
migration/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "migration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "migration"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "migration"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
sea-orm-migration = { version = "0.12.0", features = ["sqlx-mysql", "runtime-tokio-rustls"] }
|
||||
async-std = { version = "1.13.0", features = ["attributes", "tokio1"] }
|
||||
bcrypt = "0.17.1"
|
||||
26
migration/src/lib.rs
Normal file
26
migration/src/lib.rs
Normal file
@ -0,0 +1,26 @@
|
||||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20240101_000001_create_users_table;
|
||||
mod m20240101_000002_create_roles_table;
|
||||
mod m20240101_000003_create_permissions_table;
|
||||
mod m20240101_000004_create_menus_table;
|
||||
mod m20240101_000005_create_user_roles_table;
|
||||
mod m20240101_000006_create_role_permissions_table;
|
||||
mod m20240101_000007_insert_admin_user;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20240101_000001_create_users_table::Migration),
|
||||
Box::new(m20240101_000002_create_roles_table::Migration),
|
||||
Box::new(m20240101_000003_create_permissions_table::Migration),
|
||||
Box::new(m20240101_000004_create_menus_table::Migration),
|
||||
Box::new(m20240101_000005_create_user_roles_table::Migration),
|
||||
Box::new(m20240101_000006_create_role_permissions_table::Migration),
|
||||
Box::new(m20240101_000007_insert_admin_user::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
84
migration/src/m20240101_000001_create_users_table.rs
Normal file
84
migration/src/m20240101_000001_create_users_table.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Users::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Users::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::Username)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::Email)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::PasswordHash)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(Users::Nickname).string())
|
||||
.col(ColumnDef::new(Users::Avatar).string())
|
||||
.col(ColumnDef::new(Users::Phone).string())
|
||||
.col(
|
||||
ColumnDef::new(Users::Status)
|
||||
.tiny_integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::CreatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Users::UpdatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Users::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
Username,
|
||||
Email,
|
||||
PasswordHash,
|
||||
Nickname,
|
||||
Avatar,
|
||||
Phone,
|
||||
Status,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
67
migration/src/m20240101_000002_create_roles_table.rs
Normal file
67
migration/src/m20240101_000002_create_roles_table.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Roles::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Roles::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Roles::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Roles::Description).text())
|
||||
.col(
|
||||
ColumnDef::new(Roles::Status)
|
||||
.tiny_integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Roles::CreatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Roles::UpdatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Roles::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Roles {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Description,
|
||||
Status,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
86
migration/src/m20240101_000003_create_permissions_table.rs
Normal file
86
migration/src/m20240101_000003_create_permissions_table.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Permissions::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Permissions::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Permissions::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Permissions::Key)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Permissions::Description).text())
|
||||
.col(
|
||||
ColumnDef::new(Permissions::Resource)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Permissions::Action)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Permissions::Status)
|
||||
.tiny_integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Permissions::CreatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Permissions::UpdatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Permissions::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Permissions {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Key,
|
||||
Description,
|
||||
Resource,
|
||||
Action,
|
||||
Status,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
103
migration/src/m20240101_000004_create_menus_table.rs
Normal file
103
migration/src/m20240101_000004_create_menus_table.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Menus::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Menus::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Menus::ParentId).integer())
|
||||
.col(
|
||||
ColumnDef::new(Menus::Name)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(Menus::Path).string())
|
||||
.col(ColumnDef::new(Menus::Component).string())
|
||||
.col(ColumnDef::new(Menus::Icon).string())
|
||||
.col(
|
||||
ColumnDef::new(Menus::SortOrder)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Menus::MenuType)
|
||||
.tiny_integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Menus::Visible)
|
||||
.tiny_integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Menus::Status)
|
||||
.tiny_integer()
|
||||
.not_null()
|
||||
.default(1),
|
||||
)
|
||||
.col(ColumnDef::new(Menus::PermissionKey).string())
|
||||
.col(
|
||||
ColumnDef::new(Menus::CreatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Menus::UpdatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_menus_parent_id")
|
||||
.from(Menus::Table, Menus::ParentId)
|
||||
.to(Menus::Table, Menus::Id)
|
||||
.on_delete(ForeignKeyAction::SetNull)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Menus::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Menus {
|
||||
Table,
|
||||
Id,
|
||||
ParentId,
|
||||
Name,
|
||||
Path,
|
||||
Component,
|
||||
Icon,
|
||||
SortOrder,
|
||||
MenuType,
|
||||
Visible,
|
||||
Status,
|
||||
PermissionKey,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
91
migration/src/m20240101_000005_create_user_roles_table.rs
Normal file
91
migration/src/m20240101_000005_create_user_roles_table.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(UserRoles::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(UserRoles::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserRoles::UserId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserRoles::RoleId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(UserRoles::CreatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_user_roles_user_id")
|
||||
.from(UserRoles::Table, UserRoles::UserId)
|
||||
.to(Users::Table, Users::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_user_roles_role_id")
|
||||
.from(UserRoles::Table, UserRoles::RoleId)
|
||||
.to(Roles::Table, Roles::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.index(
|
||||
Index::create()
|
||||
.name("idx_user_roles_unique")
|
||||
.col(UserRoles::UserId)
|
||||
.col(UserRoles::RoleId)
|
||||
.unique(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(UserRoles::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum UserRoles {
|
||||
Table,
|
||||
Id,
|
||||
UserId,
|
||||
RoleId,
|
||||
CreatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Roles {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(RolePermissions::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(RolePermissions::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(RolePermissions::RoleId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(RolePermissions::PermissionId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(RolePermissions::CreatedAt)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_role_permissions_role_id")
|
||||
.from(RolePermissions::Table, RolePermissions::RoleId)
|
||||
.to(Roles::Table, Roles::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_role_permissions_permission_id")
|
||||
.from(RolePermissions::Table, RolePermissions::PermissionId)
|
||||
.to(Permissions::Table, Permissions::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.index(
|
||||
Index::create()
|
||||
.name("idx_role_permissions_unique")
|
||||
.col(RolePermissions::RoleId)
|
||||
.col(RolePermissions::PermissionId)
|
||||
.unique(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(RolePermissions::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum RolePermissions {
|
||||
Table,
|
||||
Id,
|
||||
RoleId,
|
||||
PermissionId,
|
||||
CreatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Roles {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Permissions {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
63
migration/src/m20240101_000007_insert_admin_user.rs
Normal file
63
migration/src/m20240101_000007_insert_admin_user.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 对密码进行哈希加密
|
||||
let password_hash = hash("123456", DEFAULT_COST)
|
||||
.map_err(|e| DbErr::Custom(format!("Failed to hash password: {}", e)))?;
|
||||
|
||||
// 插入admin用户
|
||||
let insert_stmt = Query::insert()
|
||||
.into_table(Users::Table)
|
||||
.columns([
|
||||
Users::Username,
|
||||
Users::Email,
|
||||
Users::PasswordHash,
|
||||
Users::Status,
|
||||
Users::CreatedAt,
|
||||
Users::UpdatedAt,
|
||||
])
|
||||
.values_panic([
|
||||
"admin".into(),
|
||||
"admin@example.com".into(),
|
||||
password_hash.into(),
|
||||
1.into(),
|
||||
Expr::current_timestamp().into(),
|
||||
Expr::current_timestamp().into(),
|
||||
])
|
||||
.to_owned();
|
||||
|
||||
manager.exec_stmt(insert_stmt).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 删除admin用户
|
||||
let delete_stmt = Query::delete()
|
||||
.from_table(Users::Table)
|
||||
.and_where(Expr::col(Users::Username).eq("admin"))
|
||||
.to_owned();
|
||||
|
||||
manager.exec_stmt(delete_stmt).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
Username,
|
||||
Email,
|
||||
PasswordHash,
|
||||
Status,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
cli::run_cli(migration::Migrator).await;
|
||||
}
|
||||
62
src/config/config.rs
Normal file
62
src/config/config.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
pub database: DatabaseConfig,
|
||||
pub jwt: JwtConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct DatabaseConfig {
|
||||
pub url: String,
|
||||
pub max_connections: u32,
|
||||
pub connect_timeout: u64,
|
||||
pub sqlx_logging: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct JwtConfig {
|
||||
pub secret: String,
|
||||
pub expires_in: u64,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let config: Config = serde_yaml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
// 尝试从config.yml加载配置
|
||||
if let Ok(config) = Self::from_file("config.yml") {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// 如果config.yml不存在,使用默认配置
|
||||
Ok(Config {
|
||||
server: ServerConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 3000,
|
||||
},
|
||||
database: DatabaseConfig {
|
||||
url: "mysql://root:123456@127.0.0.1:3306/uadmin".to_string(),
|
||||
max_connections: 100,
|
||||
connect_timeout: 8,
|
||||
sqlx_logging: true,
|
||||
},
|
||||
jwt: JwtConfig {
|
||||
secret: "your-secret-key-here-please-change-in-production".to_string(),
|
||||
expires_in: 86400,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
21
src/config/database.rs
Normal file
21
src/config/database.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use sea_orm::{
|
||||
ConnectOptions, Database, DatabaseConnection, DbErr,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn establish_connection() -> Result<DatabaseConnection, DbErr> {
|
||||
let config = Config::load().expect("Failed to load configuration");
|
||||
|
||||
let mut opt = ConnectOptions::new(config.database.url);
|
||||
opt.max_connections(config.database.max_connections)
|
||||
.min_connections(5)
|
||||
.connect_timeout(Duration::from_secs(config.database.connect_timeout))
|
||||
.acquire_timeout(Duration::from_secs(8))
|
||||
.idle_timeout(Duration::from_secs(8))
|
||||
.max_lifetime(Duration::from_secs(8))
|
||||
.sqlx_logging(config.database.sqlx_logging)
|
||||
.sqlx_logging_level(tracing::log::LevelFilter::Info);
|
||||
|
||||
Database::connect(opt).await
|
||||
}
|
||||
4
src/config/mod.rs
Normal file
4
src/config/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod database;
|
||||
pub mod config;
|
||||
|
||||
pub use config::Config;
|
||||
14
src/database.rs
Normal file
14
src/database.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use sea_orm::{Database, DatabaseConnection, DbErr};
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn establish_connection() -> Result<DatabaseConnection, DbErr> {
|
||||
let config = Config::load().expect("Failed to load configuration");
|
||||
|
||||
Database::connect(&config.database.url).await
|
||||
}
|
||||
|
||||
pub async fn run_migrations(db: &DatabaseConnection) -> Result<(), DbErr> {
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
|
||||
Migrator::up(db, None).await
|
||||
}
|
||||
172
src/handlers/auth.rs
Normal file
172
src/handlers/auth.rs
Normal file
@ -0,0 +1,172 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::{
|
||||
models::{user, ApiResponse},
|
||||
utils::{jwt::generate_token, password::{hash_password, verify_password}},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub nickname: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AuthResponse {
|
||||
pub token: String,
|
||||
pub user: UserInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserInfo {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub nickname: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> Result<Json<ApiResponse<AuthResponse>>, StatusCode> {
|
||||
// 查找用户
|
||||
let user = user::Entity::find()
|
||||
.filter(user::Column::Username.eq(&payload.username))
|
||||
.filter(user::Column::Status.eq(1))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let user = match user {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
401,
|
||||
"用户名或密码错误".to_string(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// 验证密码
|
||||
let is_valid = verify_password(&payload.password, &user.password_hash)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if !is_valid {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
401,
|
||||
"用户名或密码错误".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
let token = generate_token(user.id, user.username.clone())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let auth_response = AuthResponse {
|
||||
token,
|
||||
user: UserInfo {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
phone: user.phone,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(auth_response)))
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<RegisterRequest>,
|
||||
) -> Result<Json<ApiResponse<AuthResponse>>, StatusCode> {
|
||||
// 检查用户名是否已存在
|
||||
let existing_user = user::Entity::find()
|
||||
.filter(user::Column::Username.eq(&payload.username))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existing_user.is_some() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"用户名已存在".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
let existing_email = user::Entity::find()
|
||||
.filter(user::Column::Email.eq(&payload.email))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existing_email.is_some() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"邮箱已存在".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
let password_hash = hash_password(&payload.password)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 创建用户
|
||||
let now = Utc::now();
|
||||
let new_user = user::ActiveModel {
|
||||
username: Set(payload.username.clone()),
|
||||
email: Set(payload.email.clone()),
|
||||
password_hash: Set(password_hash),
|
||||
nickname: Set(payload.nickname.clone()),
|
||||
phone: Set(payload.phone.clone()),
|
||||
status: Set(1),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let user = new_user
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 生成JWT token
|
||||
let token = generate_token(user.id, user.username.clone())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let auth_response = AuthResponse {
|
||||
token,
|
||||
user: UserInfo {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
phone: user.phone,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(auth_response)))
|
||||
}
|
||||
279
src/handlers/menu.rs
Normal file
279
src/handlers/menu.rs
Normal file
@ -0,0 +1,279 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use chrono::Utc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
menu::{self, CreateMenuRequest, UpdateMenuRequest, MenuTreeResponse},
|
||||
ApiResponse,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MenuQuery {
|
||||
pub name: Option<String>,
|
||||
pub status: Option<i8>,
|
||||
pub menu_type: Option<i8>,
|
||||
}
|
||||
|
||||
pub async fn get_menus(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Query(params): Query<MenuQuery>,
|
||||
) -> Result<Json<ApiResponse<Vec<MenuTreeResponse>>>, StatusCode> {
|
||||
let mut query = menu::Entity::find();
|
||||
|
||||
// 添加过滤条件
|
||||
if let Some(name) = params.name {
|
||||
query = query.filter(menu::Column::Name.contains(&name));
|
||||
}
|
||||
if let Some(status) = params.status {
|
||||
query = query.filter(menu::Column::Status.eq(status));
|
||||
}
|
||||
if let Some(menu_type) = params.menu_type {
|
||||
query = query.filter(menu::Column::MenuType.eq(menu_type));
|
||||
}
|
||||
|
||||
let menus = query
|
||||
.order_by_asc(menu::Column::SortOrder)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 构建菜单树
|
||||
let menu_tree = build_menu_tree(menus);
|
||||
|
||||
Ok(Json(ApiResponse::success(menu_tree)))
|
||||
}
|
||||
|
||||
pub async fn create_menu(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<CreateMenuRequest>,
|
||||
) -> Result<Json<ApiResponse<menu::Model>>, StatusCode> {
|
||||
// 如果有父菜单,检查父菜单是否存在
|
||||
if let Some(parent_id) = payload.parent_id {
|
||||
let parent_menu = menu::Entity::find_by_id(parent_id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if parent_menu.is_none() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"父菜单不存在".to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// 创建菜单
|
||||
let now = Utc::now();
|
||||
let new_menu = menu::ActiveModel {
|
||||
parent_id: Set(payload.parent_id),
|
||||
name: Set(payload.name),
|
||||
path: Set(payload.path),
|
||||
component: Set(payload.component),
|
||||
icon: Set(payload.icon),
|
||||
sort_order: Set(payload.sort_order),
|
||||
menu_type: Set(payload.menu_type),
|
||||
visible: Set(payload.visible),
|
||||
status: Set(1),
|
||||
permission_key: Set(payload.permission_key),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let menu = new_menu
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ApiResponse::success(menu)))
|
||||
}
|
||||
|
||||
pub async fn update_menu(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
Json(payload): Json<UpdateMenuRequest>,
|
||||
) -> Result<Json<ApiResponse<menu::Model>>, StatusCode> {
|
||||
// 查找菜单
|
||||
let menu = menu::Entity::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let menu = match menu {
|
||||
Some(menu) => menu,
|
||||
None => {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
404,
|
||||
"菜单不存在".to_string(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// 如果更新父菜单,检查是否会形成循环引用
|
||||
if let Some(parent_id) = payload.parent_id {
|
||||
if parent_id == id {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"不能将自己设为父菜单".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 检查父菜单是否存在
|
||||
let parent_menu = menu::Entity::find_by_id(parent_id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if parent_menu.is_none() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"父菜单不存在".to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let mut menu: menu::ActiveModel = menu.into();
|
||||
|
||||
// 更新字段
|
||||
if let Some(parent_id) = payload.parent_id {
|
||||
menu.parent_id = Set(Some(parent_id));
|
||||
}
|
||||
if let Some(name) = payload.name {
|
||||
menu.name = Set(name);
|
||||
}
|
||||
if let Some(path) = payload.path {
|
||||
menu.path = Set(Some(path));
|
||||
}
|
||||
if let Some(component) = payload.component {
|
||||
menu.component = Set(Some(component));
|
||||
}
|
||||
if let Some(icon) = payload.icon {
|
||||
menu.icon = Set(Some(icon));
|
||||
}
|
||||
if let Some(sort_order) = payload.sort_order {
|
||||
menu.sort_order = Set(sort_order);
|
||||
}
|
||||
if let Some(menu_type) = payload.menu_type {
|
||||
menu.menu_type = Set(menu_type);
|
||||
}
|
||||
if let Some(visible) = payload.visible {
|
||||
menu.visible = Set(visible);
|
||||
}
|
||||
if let Some(status) = payload.status {
|
||||
menu.status = Set(status);
|
||||
}
|
||||
if let Some(permission_key) = payload.permission_key {
|
||||
menu.permission_key = Set(Some(permission_key));
|
||||
}
|
||||
menu.updated_at = Set(Utc::now());
|
||||
|
||||
let updated_menu = menu
|
||||
.update(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ApiResponse::success(updated_menu)))
|
||||
}
|
||||
|
||||
pub async fn delete_menu(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<ApiResponse<()>>, StatusCode> {
|
||||
// 查找菜单
|
||||
let menu = menu::Entity::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
match menu {
|
||||
Some(menu) => {
|
||||
// 检查是否有子菜单
|
||||
let children = menu::Entity::find()
|
||||
.filter(menu::Column::ParentId.eq(id))
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if !children.is_empty() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"请先删除子菜单".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 删除菜单
|
||||
menu.delete(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ApiResponse::success(())))
|
||||
}
|
||||
None => Ok(Json(ApiResponse::error(
|
||||
404,
|
||||
"菜单不存在".to_string(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单树的辅助函数
|
||||
fn build_menu_tree(menus: Vec<menu::Model>) -> Vec<MenuTreeResponse> {
|
||||
let mut menu_map: HashMap<i32, MenuTreeResponse> = HashMap::new();
|
||||
let mut root_menus: Vec<MenuTreeResponse> = Vec::new();
|
||||
|
||||
// 将所有菜单转换为 MenuTreeResponse
|
||||
for menu in menus {
|
||||
let menu_response = MenuTreeResponse {
|
||||
id: menu.id,
|
||||
parent_id: menu.parent_id,
|
||||
name: menu.name,
|
||||
path: menu.path,
|
||||
component: menu.component,
|
||||
icon: menu.icon,
|
||||
sort_order: menu.sort_order,
|
||||
menu_type: menu.menu_type,
|
||||
visible: menu.visible,
|
||||
status: menu.status,
|
||||
permission_key: menu.permission_key,
|
||||
children: Vec::new(),
|
||||
created_at: menu.created_at,
|
||||
updated_at: menu.updated_at,
|
||||
};
|
||||
menu_map.insert(menu.id, menu_response);
|
||||
}
|
||||
|
||||
// 构建树结构
|
||||
let mut menu_map_clone = menu_map.clone();
|
||||
for (_id, menu) in menu_map.iter() {
|
||||
if let Some(parent_id) = menu.parent_id {
|
||||
if let Some(parent) = menu_map_clone.get_mut(&parent_id) {
|
||||
parent.children.push(menu.clone());
|
||||
}
|
||||
} else {
|
||||
root_menus.push(menu.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// 对每个层级的菜单按 sort_order 排序
|
||||
sort_menu_tree(&mut root_menus);
|
||||
|
||||
root_menus
|
||||
}
|
||||
|
||||
// 递归排序菜单树
|
||||
fn sort_menu_tree(menus: &mut Vec<MenuTreeResponse>) {
|
||||
menus.sort_by(|a, b| a.sort_order.cmp(&b.sort_order));
|
||||
for menu in menus.iter_mut() {
|
||||
sort_menu_tree(&mut menu.children);
|
||||
}
|
||||
}
|
||||
5
src/handlers/mod.rs
Normal file
5
src/handlers/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod user;
|
||||
pub mod role;
|
||||
pub mod menu;
|
||||
pub mod permission;
|
||||
275
src/handlers/permission.rs
Normal file
275
src/handlers/permission.rs
Normal file
@ -0,0 +1,275 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
permission::{self, CreatePermissionRequest, UpdatePermissionRequest, PermissionResponse},
|
||||
ApiResponse, PageResponse,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PermissionQuery {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub name: Option<String>,
|
||||
pub resource: Option<String>,
|
||||
pub action: Option<String>,
|
||||
pub status: Option<i8>,
|
||||
}
|
||||
|
||||
pub async fn get_permissions(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Query(params): Query<PermissionQuery>,
|
||||
) -> Result<Json<ApiResponse<PageResponse<PermissionResponse>>>, StatusCode> {
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(10);
|
||||
|
||||
let mut query = permission::Entity::find();
|
||||
|
||||
// 添加过滤条件
|
||||
if let Some(name) = params.name {
|
||||
query = query.filter(permission::Column::Name.contains(&name));
|
||||
}
|
||||
if let Some(resource) = params.resource {
|
||||
query = query.filter(permission::Column::Resource.contains(&resource));
|
||||
}
|
||||
if let Some(action) = params.action {
|
||||
query = query.filter(permission::Column::Action.contains(&action));
|
||||
}
|
||||
if let Some(status) = params.status {
|
||||
query = query.filter(permission::Column::Status.eq(status));
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
let total = query
|
||||
.clone()
|
||||
.count(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 分页查询
|
||||
let permissions = query
|
||||
.order_by_desc(permission::Column::CreatedAt)
|
||||
.paginate(&db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let permission_responses: Vec<PermissionResponse> = permissions
|
||||
.into_iter()
|
||||
.map(|p| PermissionResponse {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
key: p.key,
|
||||
description: p.description,
|
||||
resource: p.resource,
|
||||
action: p.action,
|
||||
status: p.status,
|
||||
created_at: p.created_at,
|
||||
updated_at: p.updated_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let page_response = PageResponse::new(permission_responses, total, page, page_size);
|
||||
Ok(Json(ApiResponse::success(page_response)))
|
||||
}
|
||||
|
||||
pub async fn get_all_permissions(
|
||||
State(db): State<DatabaseConnection>,
|
||||
) -> Result<Json<ApiResponse<Vec<PermissionResponse>>>, StatusCode> {
|
||||
let permissions = permission::Entity::find()
|
||||
.filter(permission::Column::Status.eq(1))
|
||||
.order_by_asc(permission::Column::Resource)
|
||||
.order_by_asc(permission::Column::Action)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let permission_responses: Vec<PermissionResponse> = permissions
|
||||
.into_iter()
|
||||
.map(|p| PermissionResponse {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
key: p.key,
|
||||
description: p.description,
|
||||
resource: p.resource,
|
||||
action: p.action,
|
||||
status: p.status,
|
||||
created_at: p.created_at,
|
||||
updated_at: p.updated_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(ApiResponse::success(permission_responses)))
|
||||
}
|
||||
|
||||
pub async fn create_permission(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<CreatePermissionRequest>,
|
||||
) -> Result<Json<ApiResponse<PermissionResponse>>, StatusCode> {
|
||||
// 检查权限键是否已存在
|
||||
let existing_permission = permission::Entity::find()
|
||||
.filter(permission::Column::Key.eq(&payload.key))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existing_permission.is_some() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"权限键已存在".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 创建权限
|
||||
let now = Utc::now();
|
||||
let new_permission = permission::ActiveModel {
|
||||
name: Set(payload.name),
|
||||
key: Set(payload.key),
|
||||
description: Set(payload.description),
|
||||
resource: Set(payload.resource),
|
||||
action: Set(payload.action),
|
||||
status: Set(1),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let permission = new_permission
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let permission_response = PermissionResponse {
|
||||
id: permission.id,
|
||||
name: permission.name,
|
||||
key: permission.key,
|
||||
description: permission.description,
|
||||
resource: permission.resource,
|
||||
action: permission.action,
|
||||
status: permission.status,
|
||||
created_at: permission.created_at,
|
||||
updated_at: permission.updated_at,
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(permission_response)))
|
||||
}
|
||||
|
||||
pub async fn update_permission(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
Json(payload): Json<UpdatePermissionRequest>,
|
||||
) -> Result<Json<ApiResponse<PermissionResponse>>, StatusCode> {
|
||||
// 查找权限
|
||||
let permission = permission::Entity::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let permission = match permission {
|
||||
Some(permission) => permission,
|
||||
None => {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
404,
|
||||
"权限不存在".to_string(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// 如果更新权限键,检查是否已存在
|
||||
if let Some(ref key) = payload.key {
|
||||
if key != &permission.key {
|
||||
let existing_permission = permission::Entity::find()
|
||||
.filter(permission::Column::Key.eq(key))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existing_permission.is_some() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"权限键已存在".to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut permission: permission::ActiveModel = permission.into();
|
||||
|
||||
// 更新字段
|
||||
if let Some(name) = payload.name {
|
||||
permission.name = Set(name);
|
||||
}
|
||||
if let Some(key) = payload.key {
|
||||
permission.key = Set(key);
|
||||
}
|
||||
if let Some(description) = payload.description {
|
||||
permission.description = Set(Some(description));
|
||||
}
|
||||
if let Some(resource) = payload.resource {
|
||||
permission.resource = Set(resource);
|
||||
}
|
||||
if let Some(action) = payload.action {
|
||||
permission.action = Set(action);
|
||||
}
|
||||
if let Some(status) = payload.status {
|
||||
permission.status = Set(status);
|
||||
}
|
||||
permission.updated_at = Set(Utc::now());
|
||||
|
||||
let updated_permission = permission
|
||||
.update(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let permission_response = PermissionResponse {
|
||||
id: updated_permission.id,
|
||||
name: updated_permission.name,
|
||||
key: updated_permission.key,
|
||||
description: updated_permission.description,
|
||||
resource: updated_permission.resource,
|
||||
action: updated_permission.action,
|
||||
status: updated_permission.status,
|
||||
created_at: updated_permission.created_at,
|
||||
updated_at: updated_permission.updated_at,
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(permission_response)))
|
||||
}
|
||||
|
||||
pub async fn delete_permission(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<ApiResponse<()>>, StatusCode> {
|
||||
// 查找权限
|
||||
let permission = permission::Entity::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
match permission {
|
||||
Some(permission) => {
|
||||
// 删除权限(级联删除会自动处理关联表)
|
||||
permission
|
||||
.delete(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ApiResponse::success(())))
|
||||
}
|
||||
None => Ok(Json(ApiResponse::error(
|
||||
404,
|
||||
"权限不存在".to_string(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
263
src/handlers/role.rs
Normal file
263
src/handlers/role.rs
Normal file
@ -0,0 +1,263 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
role::{self, CreateRoleRequest, UpdateRoleRequest, RoleResponse},
|
||||
role_permission, permission, ApiResponse, PageResponse,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RoleQuery {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub name: Option<String>,
|
||||
pub status: Option<i8>,
|
||||
}
|
||||
|
||||
pub async fn get_roles(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Query(params): Query<RoleQuery>,
|
||||
) -> Result<Json<ApiResponse<PageResponse<RoleResponse>>>, StatusCode> {
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(10);
|
||||
|
||||
let mut query = role::Entity::find();
|
||||
|
||||
// 添加过滤条件
|
||||
if let Some(name) = params.name {
|
||||
query = query.filter(role::Column::Name.contains(&name));
|
||||
}
|
||||
if let Some(status) = params.status {
|
||||
query = query.filter(role::Column::Status.eq(status));
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
let total = query
|
||||
.clone()
|
||||
.count(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 分页查询
|
||||
let roles = query
|
||||
.order_by_desc(role::Column::CreatedAt)
|
||||
.paginate(&db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 获取角色权限信息
|
||||
let mut role_responses = Vec::new();
|
||||
for role in roles {
|
||||
let permissions = role
|
||||
.find_related(permission::Entity)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
role_responses.push(RoleResponse {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
status: role.status,
|
||||
permissions,
|
||||
created_at: role.created_at,
|
||||
updated_at: role.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
let page_response = PageResponse::new(role_responses, total, page, page_size);
|
||||
Ok(Json(ApiResponse::success(page_response)))
|
||||
}
|
||||
|
||||
pub async fn create_role(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<CreateRoleRequest>,
|
||||
) -> Result<Json<ApiResponse<RoleResponse>>, StatusCode> {
|
||||
// 检查角色名是否已存在
|
||||
let existing_role = role::Entity::find()
|
||||
.filter(role::Column::Name.eq(&payload.name))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existing_role.is_some() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"角色名已存在".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
let now = Utc::now();
|
||||
let new_role = role::ActiveModel {
|
||||
name: Set(payload.name),
|
||||
description: Set(payload.description),
|
||||
status: Set(1),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let role = new_role
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 分配权限
|
||||
if let Some(permission_ids) = payload.permission_ids {
|
||||
for permission_id in permission_ids {
|
||||
let role_permission = role_permission::ActiveModel {
|
||||
role_id: Set(role.id),
|
||||
permission_id: Set(permission_id),
|
||||
created_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
role_permission
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色权限信息
|
||||
let permissions = role
|
||||
.find_related(permission::Entity)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let role_response = RoleResponse {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
status: role.status,
|
||||
permissions,
|
||||
created_at: role.created_at,
|
||||
updated_at: role.updated_at,
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(role_response)))
|
||||
}
|
||||
|
||||
pub async fn update_role(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
Json(payload): Json<UpdateRoleRequest>,
|
||||
) -> Result<Json<ApiResponse<RoleResponse>>, StatusCode> {
|
||||
// 查找角色
|
||||
let role = role::Entity::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let role = match role {
|
||||
Some(role) => role,
|
||||
None => {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
404,
|
||||
"角色不存在".to_string(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let mut role: role::ActiveModel = role.into();
|
||||
|
||||
// 更新字段
|
||||
if let Some(name) = payload.name {
|
||||
role.name = Set(name);
|
||||
}
|
||||
if let Some(description) = payload.description {
|
||||
role.description = Set(Some(description));
|
||||
}
|
||||
if let Some(status) = payload.status {
|
||||
role.status = Set(status);
|
||||
}
|
||||
role.updated_at = Set(Utc::now());
|
||||
|
||||
let updated_role = role
|
||||
.update(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 更新权限关联
|
||||
if let Some(permission_ids) = payload.permission_ids {
|
||||
// 删除现有权限关联
|
||||
role_permission::Entity::delete_many()
|
||||
.filter(role_permission::Column::RoleId.eq(id))
|
||||
.exec(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 添加新的权限关联
|
||||
let now = Utc::now();
|
||||
for permission_id in permission_ids {
|
||||
let role_permission = role_permission::ActiveModel {
|
||||
role_id: Set(id),
|
||||
permission_id: Set(permission_id),
|
||||
created_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
role_permission
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色权限信息
|
||||
let permissions = updated_role
|
||||
.find_related(permission::Entity)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let role_response = RoleResponse {
|
||||
id: updated_role.id,
|
||||
name: updated_role.name,
|
||||
description: updated_role.description,
|
||||
status: updated_role.status,
|
||||
permissions,
|
||||
created_at: updated_role.created_at,
|
||||
updated_at: updated_role.updated_at,
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(role_response)))
|
||||
}
|
||||
|
||||
pub async fn delete_role(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<ApiResponse<()>>, StatusCode> {
|
||||
// 查找角色
|
||||
let role = role::Entity::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
match role {
|
||||
Some(role) => {
|
||||
// 删除角色(级联删除会自动处理关联表)
|
||||
role.delete(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ApiResponse::success(())))
|
||||
}
|
||||
None => Ok(Json(ApiResponse::error(
|
||||
404,
|
||||
"角色不存在".to_string(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
309
src/handlers/user.rs
Normal file
309
src/handlers/user.rs
Normal file
@ -0,0 +1,309 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
user::{self, CreateUserRequest, UpdateUserRequest, UserResponse},
|
||||
user_role, role, ApiResponse, PageResponse,
|
||||
},
|
||||
utils::password::hash_password,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserQuery {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
pub username: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub status: Option<i8>,
|
||||
}
|
||||
|
||||
pub async fn get_users(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Query(params): Query<UserQuery>,
|
||||
) -> Result<Json<ApiResponse<PageResponse<UserResponse>>>, StatusCode> {
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(10);
|
||||
|
||||
let mut query = user::Entity::find();
|
||||
|
||||
// 添加过滤条件
|
||||
if let Some(username) = params.username {
|
||||
query = query.filter(user::Column::Username.contains(&username));
|
||||
}
|
||||
if let Some(email) = params.email {
|
||||
query = query.filter(user::Column::Email.contains(&email));
|
||||
}
|
||||
if let Some(status) = params.status {
|
||||
query = query.filter(user::Column::Status.eq(status));
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
let total = query
|
||||
.clone()
|
||||
.count(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 分页查询
|
||||
let users = query
|
||||
.order_by_desc(user::Column::CreatedAt)
|
||||
.paginate(&db, page_size)
|
||||
.fetch_page(page - 1)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 获取用户角色信息
|
||||
let mut user_responses = Vec::new();
|
||||
for user in users {
|
||||
let roles = user
|
||||
.find_related(role::Entity)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
user_responses.push(UserResponse {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
phone: user.phone,
|
||||
status: user.status,
|
||||
roles,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
let page_response = PageResponse::new(user_responses, total, page, page_size);
|
||||
Ok(Json(ApiResponse::success(page_response)))
|
||||
}
|
||||
|
||||
pub async fn create_user(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Json(payload): Json<CreateUserRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>, StatusCode> {
|
||||
// 检查用户名是否已存在
|
||||
let existing_user = user::Entity::find()
|
||||
.filter(user::Column::Username.eq(&payload.username))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existing_user.is_some() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"用户名已存在".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
let existing_email = user::Entity::find()
|
||||
.filter(user::Column::Email.eq(&payload.email))
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if existing_email.is_some() {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
400,
|
||||
"邮箱已存在".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
let password_hash = hash_password(&payload.password)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 创建用户
|
||||
let now = Utc::now();
|
||||
let new_user = user::ActiveModel {
|
||||
username: Set(payload.username),
|
||||
email: Set(payload.email),
|
||||
password_hash: Set(password_hash),
|
||||
nickname: Set(payload.nickname),
|
||||
phone: Set(payload.phone),
|
||||
status: Set(1),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let user = new_user
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 分配角色
|
||||
if let Some(role_ids) = payload.role_ids {
|
||||
for role_id in role_ids {
|
||||
let user_role = user_role::ActiveModel {
|
||||
user_id: Set(user.id),
|
||||
role_id: Set(role_id),
|
||||
created_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
user_role
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户角色信息
|
||||
let roles = user
|
||||
.find_related(role::Entity)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let user_response = UserResponse {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
phone: user.phone,
|
||||
status: user.status,
|
||||
roles,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(user_response)))
|
||||
}
|
||||
|
||||
pub async fn update_user(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
Json(payload): Json<UpdateUserRequest>,
|
||||
) -> Result<Json<ApiResponse<UserResponse>>, StatusCode> {
|
||||
// 查找用户
|
||||
let user = user::Entity::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let user = match user {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
return Ok(Json(ApiResponse::error(
|
||||
404,
|
||||
"用户不存在".to_string(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let mut user: user::ActiveModel = user.into();
|
||||
|
||||
// 更新字段
|
||||
if let Some(username) = payload.username {
|
||||
user.username = Set(username);
|
||||
}
|
||||
if let Some(email) = payload.email {
|
||||
user.email = Set(email);
|
||||
}
|
||||
if let Some(password) = payload.password {
|
||||
let password_hash = hash_password(&password)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
user.password_hash = Set(password_hash);
|
||||
}
|
||||
if let Some(nickname) = payload.nickname {
|
||||
user.nickname = Set(Some(nickname));
|
||||
}
|
||||
if let Some(phone) = payload.phone {
|
||||
user.phone = Set(Some(phone));
|
||||
}
|
||||
if let Some(status) = payload.status {
|
||||
user.status = Set(status);
|
||||
}
|
||||
user.updated_at = Set(Utc::now());
|
||||
|
||||
let updated_user = user
|
||||
.update(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 更新角色关联
|
||||
if let Some(role_ids) = payload.role_ids {
|
||||
// 删除现有角色关联
|
||||
user_role::Entity::delete_many()
|
||||
.filter(user_role::Column::UserId.eq(id))
|
||||
.exec(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 添加新的角色关联
|
||||
let now = Utc::now();
|
||||
for role_id in role_ids {
|
||||
let user_role = user_role::ActiveModel {
|
||||
user_id: Set(id),
|
||||
role_id: Set(role_id),
|
||||
created_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
user_role
|
||||
.insert(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户角色信息
|
||||
let roles = updated_user
|
||||
.find_related(role::Entity)
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let user_response = UserResponse {
|
||||
id: updated_user.id,
|
||||
username: updated_user.username,
|
||||
email: updated_user.email,
|
||||
nickname: updated_user.nickname,
|
||||
avatar: updated_user.avatar,
|
||||
phone: updated_user.phone,
|
||||
status: updated_user.status,
|
||||
roles,
|
||||
created_at: updated_user.created_at,
|
||||
updated_at: updated_user.updated_at,
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(user_response)))
|
||||
}
|
||||
|
||||
pub async fn delete_user(
|
||||
State(db): State<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<ApiResponse<()>>, StatusCode> {
|
||||
// 查找用户
|
||||
let user = user::Entity::find_by_id(id)
|
||||
.one(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
match user {
|
||||
Some(user) => {
|
||||
// 删除用户(级联删除会自动处理关联表)
|
||||
user.delete(&db)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ApiResponse::success(())))
|
||||
}
|
||||
None => Ok(Json(ApiResponse::error(
|
||||
404,
|
||||
"用户不存在".to_string(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
53
src/main.rs
Normal file
53
src/main.rs
Normal file
@ -0,0 +1,53 @@
|
||||
mod config;
|
||||
mod database;
|
||||
mod handlers;
|
||||
mod middleware;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod utils;
|
||||
|
||||
use std::env;
|
||||
use tracing::{info, Level};
|
||||
use tracing_subscriber;
|
||||
use config::Config;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 加载环境变量
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
// 初始化日志
|
||||
let log_level = env::var("RUST_LOG")
|
||||
.unwrap_or_else(|_| "info".to_string())
|
||||
.parse::<Level>()
|
||||
.unwrap_or(Level::INFO);
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(log_level)
|
||||
.init();
|
||||
|
||||
info!("Starting UAdmin API Server...");
|
||||
|
||||
// 建立数据库连接
|
||||
let db = database::establish_connection().await?;
|
||||
info!("Database connected successfully");
|
||||
|
||||
// 运行数据库迁移
|
||||
database::run_migrations(&db).await?;
|
||||
info!("Database migrations completed");
|
||||
|
||||
// 创建路由
|
||||
let app = routes::create_routes(db);
|
||||
|
||||
// 加载配置
|
||||
let config = Config::load().expect("Failed to load configuration");
|
||||
let addr = format!("{}:{}", config.server.host, config.server.port);
|
||||
|
||||
info!("Server running on http://{}", addr);
|
||||
|
||||
// 启动服务器
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
57
src/middleware/auth.rs
Normal file
57
src/middleware/auth.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use crate::utils::jwt::{extract_token_from_header, verify_token};
|
||||
|
||||
pub async fn auth_middleware(
|
||||
State(_db): State<DatabaseConnection>,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let headers = request.headers().clone();
|
||||
|
||||
// 跳过OPTIONS请求(CORS预检请求)
|
||||
if request.method() == axum::http::Method::OPTIONS {
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
// 跳过认证的路径
|
||||
let path = request.uri().path();
|
||||
let skip_auth_paths = [
|
||||
"/api/auth/login",
|
||||
"/api/auth/register",
|
||||
"/",
|
||||
"/health",
|
||||
];
|
||||
|
||||
if skip_auth_paths.contains(&path) {
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
// 获取Authorization头
|
||||
let auth_header = headers
|
||||
.get("authorization")
|
||||
.and_then(|header| header.to_str().ok());
|
||||
|
||||
let token = match auth_header {
|
||||
Some(header) => match extract_token_from_header(header) {
|
||||
Some(token) => token,
|
||||
None => return Err(StatusCode::UNAUTHORIZED),
|
||||
},
|
||||
None => return Err(StatusCode::UNAUTHORIZED),
|
||||
};
|
||||
|
||||
// 验证JWT token
|
||||
match verify_token(token) {
|
||||
Ok(claims) => {
|
||||
// 将用户信息添加到请求扩展中
|
||||
request.extensions_mut().insert(claims);
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
Err(_) => Err(StatusCode::UNAUTHORIZED),
|
||||
}
|
||||
}
|
||||
1
src/middleware/mod.rs
Normal file
1
src/middleware/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod auth;
|
||||
94
src/models/menu.rs
Normal file
94
src/models/menu.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "menus")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub parent_id: Option<i32>,
|
||||
pub name: String,
|
||||
pub path: Option<String>,
|
||||
pub component: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub sort_order: i32,
|
||||
pub menu_type: i8, // 0: 目录, 1: 菜单, 2: 按钮
|
||||
pub visible: i8, // 0: 隐藏, 1: 显示
|
||||
pub status: i8, // 0: 禁用, 1: 启用
|
||||
pub permission_key: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "Entity",
|
||||
from = "Column::ParentId",
|
||||
to = "Column::Id"
|
||||
)]
|
||||
Parent,
|
||||
#[sea_orm(
|
||||
has_many = "Entity",
|
||||
from = "Column::Id",
|
||||
to = "Column::ParentId"
|
||||
)]
|
||||
Children,
|
||||
}
|
||||
|
||||
impl Related<Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Parent.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
// 菜单创建请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateMenuRequest {
|
||||
pub parent_id: Option<i32>,
|
||||
pub name: String,
|
||||
pub path: Option<String>,
|
||||
pub component: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub sort_order: i32,
|
||||
pub menu_type: i8,
|
||||
pub visible: i8,
|
||||
pub permission_key: Option<String>,
|
||||
}
|
||||
|
||||
// 菜单更新请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateMenuRequest {
|
||||
pub parent_id: Option<i32>,
|
||||
pub name: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub component: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
pub menu_type: Option<i8>,
|
||||
pub visible: Option<i8>,
|
||||
pub status: Option<i8>,
|
||||
pub permission_key: Option<String>,
|
||||
}
|
||||
|
||||
// 菜单树响应
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct MenuTreeResponse {
|
||||
pub id: i32,
|
||||
pub parent_id: Option<i32>,
|
||||
pub name: String,
|
||||
pub path: Option<String>,
|
||||
pub component: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub sort_order: i32,
|
||||
pub menu_type: i8,
|
||||
pub visible: i8,
|
||||
pub status: i8,
|
||||
pub permission_key: Option<String>,
|
||||
pub children: Vec<MenuTreeResponse>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
61
src/models/mod.rs
Normal file
61
src/models/mod.rs
Normal file
@ -0,0 +1,61 @@
|
||||
pub mod user;
|
||||
pub mod role;
|
||||
pub mod menu;
|
||||
pub mod permission;
|
||||
pub mod user_role;
|
||||
pub mod role_permission;
|
||||
|
||||
// 重新导出所有实体
|
||||
pub use user::Entity as User;
|
||||
pub use role::Entity as Role;
|
||||
pub use menu::Entity as Menu;
|
||||
pub use permission::Entity as Permission;
|
||||
pub use user_role::Entity as UserRole;
|
||||
pub use role_permission::Entity as RolePermission;
|
||||
|
||||
// 通用响应结构
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub code: i32,
|
||||
pub message: String,
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
code: 200,
|
||||
message: "Success".to_string(),
|
||||
data: Some(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(code: i32, message: String) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PageResponse<T> {
|
||||
pub items: Vec<T>,
|
||||
pub total: u64,
|
||||
pub page: u64,
|
||||
pub page_size: u64,
|
||||
}
|
||||
|
||||
impl<T> PageResponse<T> {
|
||||
pub fn new(items: Vec<T>, total: u64, page: u64, page_size: u64) -> Self {
|
||||
Self {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/models/permission.rs
Normal file
78
src/models/permission.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "permissions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
#[sea_orm(unique)]
|
||||
pub key: String,
|
||||
pub description: Option<String>,
|
||||
pub resource: String, // 资源标识
|
||||
pub action: String, // 操作类型:create, read, update, delete
|
||||
pub status: i8, // 0: 禁用, 1: 启用
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::role_permission::Entity")]
|
||||
RolePermissions,
|
||||
}
|
||||
|
||||
impl Related<super::role_permission::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::RolePermissions.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::role::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
super::role_permission::Relation::Role.def()
|
||||
}
|
||||
fn via() -> Option<RelationDef> {
|
||||
Some(super::role_permission::Relation::Permission.def().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
// 权限创建请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreatePermissionRequest {
|
||||
pub name: String,
|
||||
pub key: String,
|
||||
pub description: Option<String>,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
// 权限更新请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdatePermissionRequest {
|
||||
pub name: Option<String>,
|
||||
pub key: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub resource: Option<String>,
|
||||
pub action: Option<String>,
|
||||
pub status: Option<i8>,
|
||||
}
|
||||
|
||||
// 权限响应
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PermissionResponse {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub key: String,
|
||||
pub description: Option<String>,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
pub status: i8,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
85
src/models/role.rs
Normal file
85
src/models/role.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "roles")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub status: i8, // 0: 禁用, 1: 启用
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::user_role::Entity")]
|
||||
UserRoles,
|
||||
#[sea_orm(has_many = "super::role_permission::Entity")]
|
||||
RolePermissions,
|
||||
}
|
||||
|
||||
impl Related<super::user_role::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::UserRoles.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::role_permission::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::RolePermissions.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
super::user_role::Relation::User.def()
|
||||
}
|
||||
fn via() -> Option<RelationDef> {
|
||||
Some(super::user_role::Relation::Role.def().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::permission::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
super::role_permission::Relation::Permission.def()
|
||||
}
|
||||
fn via() -> Option<RelationDef> {
|
||||
Some(super::role_permission::Relation::Role.def().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
// 角色创建请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateRoleRequest {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub permission_ids: Option<Vec<i32>>,
|
||||
}
|
||||
|
||||
// 角色更新请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateRoleRequest {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub status: Option<i8>,
|
||||
pub permission_ids: Option<Vec<i32>>,
|
||||
}
|
||||
|
||||
// 角色响应
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RoleResponse {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub status: i8,
|
||||
pub permissions: Vec<super::permission::Model>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
47
src/models/role_permission.rs
Normal file
47
src/models/role_permission.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "role_permissions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub role_id: i32,
|
||||
pub permission_id: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::role::Entity",
|
||||
from = "Column::RoleId",
|
||||
to = "super::role::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Role,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::permission::Entity",
|
||||
from = "Column::PermissionId",
|
||||
to = "super::permission::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Permission,
|
||||
}
|
||||
|
||||
impl Related<super::role::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Role.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::permission::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Permission.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
82
src/models/user.rs
Normal file
82
src/models/user.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(unique)]
|
||||
pub username: String,
|
||||
#[sea_orm(unique)]
|
||||
pub email: String,
|
||||
pub password_hash: String,
|
||||
pub nickname: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub status: i8, // 0: 禁用, 1: 启用
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::user_role::Entity")]
|
||||
UserRoles,
|
||||
}
|
||||
|
||||
impl Related<super::user_role::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::UserRoles.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::role::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
super::user_role::Relation::Role.def()
|
||||
}
|
||||
fn via() -> Option<RelationDef> {
|
||||
Some(super::user_role::Relation::User.def().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
// 用户创建请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateUserRequest {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub nickname: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub role_ids: Option<Vec<i32>>,
|
||||
}
|
||||
|
||||
// 用户更新请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateUserRequest {
|
||||
pub username: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub status: Option<i8>,
|
||||
pub role_ids: Option<Vec<i32>>,
|
||||
}
|
||||
|
||||
// 用户响应
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserResponse {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub nickname: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub status: i8,
|
||||
pub roles: Vec<super::role::Model>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
47
src/models/user_role.rs
Normal file
47
src/models/user_role.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user_roles")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub user_id: i32,
|
||||
pub role_id: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::role::Entity",
|
||||
from = "Column::RoleId",
|
||||
to = "super::role::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Role,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::role::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Role.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
58
src/routes.rs
Normal file
58
src/routes.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use axum::{
|
||||
routing::{delete, get, post, put},
|
||||
Router,
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
use crate::{
|
||||
handlers::{auth, user, role, menu, permission},
|
||||
middleware::auth::auth_middleware,
|
||||
};
|
||||
|
||||
pub fn create_routes(db: DatabaseConnection) -> Router<()> {
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
Router::new()
|
||||
// 认证路由(无需认证)
|
||||
.route("/api/auth/login", post(auth::login))
|
||||
.route("/api/auth/register", post(auth::register))
|
||||
|
||||
// 用户管理路由
|
||||
.route("/api/users", get(user::get_users))
|
||||
.route("/api/users", post(user::create_user))
|
||||
.route("/api/users/:id", put(user::update_user))
|
||||
.route("/api/users/:id", delete(user::delete_user))
|
||||
|
||||
// 角色管理路由
|
||||
.route("/api/roles", get(role::get_roles))
|
||||
.route("/api/roles", post(role::create_role))
|
||||
.route("/api/roles/:id", put(role::update_role))
|
||||
.route("/api/roles/:id", delete(role::delete_role))
|
||||
|
||||
// 菜单管理路由
|
||||
.route("/api/menus", get(menu::get_menus))
|
||||
.route("/api/menus", post(menu::create_menu))
|
||||
.route("/api/menus/:id", put(menu::update_menu))
|
||||
.route("/api/menus/:id", delete(menu::delete_menu))
|
||||
|
||||
// 权限管理路由
|
||||
.route("/api/permissions", get(permission::get_permissions))
|
||||
.route("/api/permissions/all", get(permission::get_all_permissions))
|
||||
.route("/api/permissions", post(permission::create_permission))
|
||||
.route("/api/permissions/:id", put(permission::update_permission))
|
||||
.route("/api/permissions/:id", delete(permission::delete_permission))
|
||||
|
||||
// 健康检查
|
||||
.route("/", get(|| async { "UAdmin API Server is running!" }))
|
||||
.route("/health", get(|| async { "OK" }))
|
||||
|
||||
// 添加CORS中间件
|
||||
.layer(cors)
|
||||
// 添加认证中间件(除了登录、注册和健康检查路由)
|
||||
.layer(axum::middleware::from_fn_with_state(db.clone(), auth_middleware))
|
||||
.with_state(db)
|
||||
}
|
||||
50
src/utils/jwt.rs
Normal file
50
src/utils/jwt.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use chrono::{Duration, Utc};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub sub: String, // 用户ID
|
||||
pub username: String,
|
||||
pub exp: usize, // 过期时间
|
||||
pub iat: usize, // 签发时间
|
||||
}
|
||||
|
||||
impl Claims {
|
||||
pub fn new(user_id: i32, username: String) -> Self {
|
||||
let now = Utc::now();
|
||||
let exp = now + Duration::hours(24); // 24小时过期
|
||||
|
||||
Self {
|
||||
sub: user_id.to_string(),
|
||||
username,
|
||||
exp: exp.timestamp() as usize,
|
||||
iat: now.timestamp() as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_token(user_id: i32, username: String) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let claims = Claims::new(user_id, username);
|
||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
|
||||
encode(&Header::default(), &claims, &key)
|
||||
}
|
||||
|
||||
pub fn verify_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
let key = DecodingKey::from_secret(secret.as_ref());
|
||||
let validation = Validation::default();
|
||||
|
||||
decode::<Claims>(token, &key, &validation).map(|data| data.claims)
|
||||
}
|
||||
|
||||
pub fn extract_token_from_header(auth_header: &str) -> Option<&str> {
|
||||
if auth_header.starts_with("Bearer ") {
|
||||
Some(&auth_header[7..])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
2
src/utils/mod.rs
Normal file
2
src/utils/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod jwt;
|
||||
pub mod password;
|
||||
9
src/utils/password.rs
Normal file
9
src/utils/password.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
|
||||
pub fn hash_password(password: &str) -> Result<String, bcrypt::BcryptError> {
|
||||
hash(password, DEFAULT_COST)
|
||||
}
|
||||
|
||||
pub fn verify_password(password: &str, hash: &str) -> Result<bool, bcrypt::BcryptError> {
|
||||
verify(password, hash)
|
||||
}
|
||||
Reference in New Issue
Block a user