feat: 添加JWT登录鉴权和用户管理功能

- 后端功能:
  - 实现JWT登录鉴权,token有效期7天,超过1天自动刷新
  - 添加用户管理功能,支持创建、删除、修改密码
  - 首次创建的用户为默认账户,拥有管理权限
  - 实现密码重置功能,支持重置密钥和频率限制(1分钟最多3次)
  - 所有API接口需要JWT鉴权
  - WebSocket连接需要JWT鉴权,绑定用户身份

- 前端功能:
  - 添加登录页面和密码重置页面
  - 添加用户管理页面,默认账户可管理所有用户,普通用户只能查看和修改自己
  - 添加退出登录功能,带二次确认
  - 未登录时不建立WebSocket连接
  - API请求自动携带JWT token,认证失败自动跳转登录页

- 安全特性:
  - 密码使用BCrypt加密存储
  - 重置密码错误信息统一处理,避免信息泄露
  - 用户操作严格绑定JWT,防止数据篡改和越权
This commit is contained in:
WrBug
2025-12-03 00:24:46 +08:00
parent f1c9b34488
commit dd553ae160
30 changed files with 2402 additions and 47 deletions
+51 -5
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { Layout as AntLayout, Menu, Drawer, Button } from 'antd'
import { Layout as AntLayout, Menu, Drawer, Button, Modal } from 'antd'
import { useMediaQuery } from 'react-responsive'
import {
WalletOutlined,
@@ -10,10 +10,14 @@ import {
MenuOutlined,
FileTextOutlined,
LinkOutlined,
AppstoreOutlined
AppstoreOutlined,
TeamOutlined,
LogoutOutlined
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import type { ReactNode } from 'react'
import { removeToken } from '../utils'
import { wsManager } from '../services/websocket'
const { Header, Content, Sider } = AntLayout
@@ -35,10 +39,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
// 获取当前应该打开的父菜单
const getInitialOpenKeys = (): string[] => {
const path = location.pathname
const keys: string[] = []
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
return ['/copy-trading-management']
keys.push('/copy-trading-management')
}
return []
return keys
}
const [openKeys, setOpenKeys] = useState<string[]>(getInitialOpenKeys())
@@ -46,9 +51,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
// 当路径变化时,自动打开对应的父菜单
useEffect(() => {
const path = location.pathname
const keys: string[] = []
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
setOpenKeys(['/copy-trading-management'])
keys.push('/copy-trading-management')
}
setOpenKeys(keys)
}, [location.pathname])
const menuItems: MenuProps['items'] = [
@@ -88,14 +95,53 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
key: '/statistics',
icon: <BarChartOutlined />,
label: '统计信息'
},
{
key: '/users',
icon: <TeamOutlined />,
label: '用户管理'
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录'
}
]
const handleLogout = () => {
removeToken()
// 断开 WebSocket 连接
wsManager.disconnect()
navigate('/login', { replace: true })
}
const handleLogoutConfirm = () => {
Modal.confirm({
title: '确认退出',
content: '确定要退出登录吗?',
okText: '确定',
cancelText: '取消',
onOk: () => {
handleLogout()
if (isMobile) {
setMobileMenuOpen(false)
}
}
})
}
const handleMenuClick = ({ key }: { key: string }) => {
// 如果是父菜单,不导航
if (key === '/copy-trading-management') {
return
}
// 处理退出登录
if (key === 'logout') {
handleLogoutConfirm()
return
}
navigate(key)
if (isMobile) {
setMobileMenuOpen(false)