feat: 添加JWT登录鉴权和用户管理功能
- 后端功能: - 实现JWT登录鉴权,token有效期7天,超过1天自动刷新 - 添加用户管理功能,支持创建、删除、修改密码 - 首次创建的用户为默认账户,拥有管理权限 - 实现密码重置功能,支持重置密钥和频率限制(1分钟最多3次) - 所有API接口需要JWT鉴权 - WebSocket连接需要JWT鉴权,绑定用户身份 - 前端功能: - 添加登录页面和密码重置页面 - 添加用户管理页面,默认账户可管理所有用户,普通用户只能查看和修改自己 - 添加退出登录功能,带二次确认 - 未登录时不建立WebSocket连接 - API请求自动携带JWT token,认证失败自动跳转登录页 - 安全特性: - 密码使用BCrypt加密存储 - 重置密码错误信息统一处理,避免信息泄露 - 用户操作严格绑定JWT,防止数据篡改和越权
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user