diff --git a/admin/package.json b/admin/package.json index 300b0c1ef..5c5f9cb08 100644 --- a/admin/package.json +++ b/admin/package.json @@ -11,8 +11,10 @@ "start:api": "nodemon server.js" }, "dependencies": { + "@arco-design/web-react": "^2.49.1", "concurrently": "^8.1.0", "cors": "^2.8.5", + "crypto": "^1.0.1", "dayjs": "^1.11.8", "dotenv": "^16.1.4", "express": "^4.18.2", @@ -21,6 +23,7 @@ "react": "^18.2.0", "react-admin": "^4.11.0", "react-dom": "^18.2.0", + "react-i18next": "^12.3.1", "tushan": "^0.2.22" }, "devDependencies": { diff --git a/admin/pnpm-lock.yaml b/admin/pnpm-lock.yaml index 7722f744c..5daa85811 100644 --- a/admin/pnpm-lock.yaml +++ b/admin/pnpm-lock.yaml @@ -5,12 +5,18 @@ settings: excludeLinksFromLockfile: false dependencies: + '@arco-design/web-react': + specifier: ^2.49.1 + version: registry.npmmirror.com/@arco-design/web-react@2.49.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0) concurrently: specifier: ^8.1.0 version: registry.npmmirror.com/concurrently@8.1.0 cors: specifier: ^2.8.5 version: registry.npmmirror.com/cors@2.8.5 + crypto: + specifier: ^1.0.1 + version: registry.npmmirror.com/crypto@1.0.1 dayjs: specifier: ^1.11.8 version: registry.npmmirror.com/dayjs@1.11.8 @@ -35,6 +41,9 @@ dependencies: react-dom: specifier: ^18.2.0 version: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0) + react-i18next: + specifier: ^12.3.1 + version: registry.npmmirror.com/react-i18next@12.3.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0) tushan: specifier: ^0.2.22 version: registry.npmmirror.com/tushan@0.2.22(history@5.3.0)(prop-types@15.8.1)(react-hook-form@7.44.3) @@ -1896,6 +1905,13 @@ packages: - encoding dev: false + registry.npmmirror.com/crypto@1.0.1: + resolution: {integrity: sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz} + name: crypto + version: 1.0.1 + deprecated: This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in. + dev: false + registry.npmmirror.com/css-color-keywords@1.0.0: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz} name: css-color-keywords diff --git a/admin/service/route/app.js b/admin/service/route/app.js index efdd4789c..a89755805 100644 --- a/admin/service/route/app.js +++ b/admin/service/route/app.js @@ -1,8 +1,9 @@ import { User, Model, Kb } from '../schema.js'; +import { auth } from './system.js'; export const useAppRoute = (app) => { // 获取AI助手列表 - app.get('/models', async (req, res) => { + app.get('/models', auth(), async (req, res) => { try { const start = parseInt(req.query._start) || 0; const end = parseInt(req.query._end) || 20; diff --git a/admin/service/route/kb.js b/admin/service/route/kb.js index 6a98c2332..3702e6cc5 100644 --- a/admin/service/route/kb.js +++ b/admin/service/route/kb.js @@ -1,8 +1,9 @@ import { Kb } from '../schema.js'; +import { auth } from './system.js'; export const useKbRoute = (app) => { // 获取用户知识库列表 - app.get('/kbs', async (req, res) => { + app.get('/kbs', auth(), async (req, res) => { try { const start = parseInt(req.query._start) || 0; const end = parseInt(req.query._end) || 20; diff --git a/admin/service/route/user.js b/admin/service/route/user.js index ab0210b81..f5e031e1b 100644 --- a/admin/service/route/user.js +++ b/admin/service/route/user.js @@ -31,7 +31,8 @@ export const useUserRoute = (app) => { return { ...obj, id: obj._id, - createTime: dayjs(obj.createTime).format('YYYY/MM/DD HH:mm') + createTime: dayjs(obj.createTime).format('YYYY/MM/DD HH:mm'), + password: '' }; }); @@ -49,14 +50,7 @@ export const useUserRoute = (app) => { // 创建用户 app.post('/users', auth(), async (req, res) => { try { - const { - username, - password, - balance, - promotion, - openaiKey = '', - avatar = '/icon/human.png' - } = req.body; + const { username, password, balance } = req.body; if (!username || !password || !balance) { return res.status(400).json({ error: 'Invalid user information' }); } @@ -64,19 +58,12 @@ export const useUserRoute = (app) => { if (existingUser) { return res.status(400).json({ error: 'Username already exists' }); } - const user = new User({ - _id: new mongoose.Types.ObjectId(), + + const result = await User.create({ username, password, - balance, - promotion: { - rate: promotion?.rate || 0 - }, - openaiKey, - avatar, - createTime: new Date() + balance }); - const result = await user.save(); res.json(result); } catch (err) { console.log(`Error creating user: ${err}`); @@ -88,15 +75,13 @@ export const useUserRoute = (app) => { app.put('/users/:id', auth(), async (req, res) => { try { const _id = req.params.id; - - // Check if a new password is provided in the request body - if (req.body.password) { - // Hash the new password - const hashedPassword = hashPassword(req.body.password); - req.body.password = hashedPassword; - } - - const result = await User.updateOne({ _id: _id }, { $set: req.body }); + + let { password, balance = 0 } = req.body; + + const result = await User.findByIdAndUpdate(_id, { + ...(password && { password: hashPassword(hashPassword(password)) }), + ...(balance && { balance }) + }); res.json(result); } catch (err) { console.log(`Error updating user: ${err}`); diff --git a/admin/src/App.tsx b/admin/src/App.tsx index c391cf1c9..2781d9e0c 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -8,6 +8,7 @@ import { } from 'tushan'; import { authProvider } from './auth'; import { userFields, payFields, kbFields, ModelFields } from './fields'; +import { Dashboard } from './Dashboard'; const authStorageKey = 'tushan:auth'; @@ -34,6 +35,7 @@ function App() { header={'FastGpt-Admin'} dataProvider={dataProvider} authProvider={authProvider} + dashboard={} > { - - const [userCount, setUserCount] = useState(0); //用户数量 - const [kbCount, setkbCount] = useState(0); - const [modelCount, setmodelCount] = useState(0); - useEffect(() => { - const fetchCounts = async () => { - const userResponse = await fetch('http://localhost:3001/users', { - headers: { 'Content-Type': 'application/json' }, - }); - const kbResponse = await fetch('http://localhost:3001/kbs', { - headers: { 'Content-Type': 'application/json' }, - }); - const modelResponse = await fetch('http://localhost:3001/models', { - headers: { 'Content-Type': 'application/json' }, - }); - - const userTotalCount = userResponse.headers.get('X-Total-Count'); - const kbTotalCount = kbResponse.headers.get('X-Total-Count'); - const modelTotalCount = modelResponse.headers.get('X-Total-Count'); - - if (userTotalCount) { - setUserCount(Number(userTotalCount)); - } - if (kbTotalCount) { - setkbCount(Number(kbTotalCount)); - } - if (modelTotalCount) { - setmodelCount(Number(modelTotalCount)); - } - }; - - fetchCounts(); - }, []); - - return ( -
-
- - - - {'你好,管理员'} - - - - - - - {/* 把 userCount 传递给 DataItem 组件 */} - } - title={'用户'} - count={userCount} - /> - - - - - - } - title={'知识库'} - count={kbCount} - /> - - - - - - } - title={'AI模型'} - count={modelCount} - /> - - - - - - - - -
-
- ); -}); -Dashboard.displayName = 'Dashboard'; - -const DashboardItem: React.FC< - React.PropsWithChildren<{ - title: string; - href?: string; - }> -> = React.memo((props) => { - const { t } = useTranslation(); - - return ( - - {t('tushan.dashboard.more')} - - ) - } - bordered={false} - style={{ overflow: 'hidden' }} - > - {props.children} - - ); -}); -DashboardItem.displayName = 'DashboardItem'; - -const DataItem: React.FC<{ - icon: React.ReactElement; - title: string; - count: number; -}> = React.memo((props) => { - return ( - -
- {props.icon} -
-
-
{props.title}
-
{props.count}
-
-
- ); -}); -DataItem.displayName = 'DataItem'; +import { Card, Link, Space, Grid, Divider, Typography } from '@arco-design/web-react'; +import { IconApps, IconUser, IconUserGroup } from 'tushan/icon'; +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +const authStorageKey = 'tushan:auth'; + +export const Dashboard: React.FC = React.memo(() => { + const [userCount, setUserCount] = useState(0); //用户数量 + const [kbCount, setkbCount] = useState(0); + const [modelCount, setmodelCount] = useState(0); + useEffect(() => { + const fetchCounts = async () => { + const baseUrl = import.meta.env.VITE_PUBLIC_SERVER_URL; + const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}'); + + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }; + const userResponse = await fetch(`${baseUrl}/users?_end=1`, { + headers + }); + const kbResponse = await fetch(`${baseUrl}/kbs?_end=1`, { + headers + }); + const modelResponse = await fetch(`${baseUrl}/models?_end=1`, { + headers + }); + + const userTotalCount = userResponse.headers.get('X-Total-Count'); + const kbTotalCount = kbResponse.headers.get('X-Total-Count'); + const modelTotalCount = modelResponse.headers.get('X-Total-Count'); + console.log(userTotalCount); + + if (userTotalCount) { + setUserCount(Number(userTotalCount)); + } + if (kbTotalCount) { + setkbCount(Number(kbTotalCount)); + } + if (modelTotalCount) { + setmodelCount(Number(modelTotalCount)); + } + }; + + fetchCounts(); + }, []); + + return ( +
+
+ + + FastGpt Admin + + + + + + {/* 把 userCount 传递给 DataItem 组件 */} + } title={'用户'} count={userCount} /> + + + + + + } title={'知识库'} count={kbCount} /> + + + + + + } title={'AI模型'} count={modelCount} /> + + + + + + +
+
+ ); +}); +Dashboard.displayName = 'Dashboard'; + +const DashboardItem: React.FC< + React.PropsWithChildren<{ + title: string; + href?: string; + }> +> = React.memo((props) => { + const { t } = useTranslation(); + + return ( + + {t('tushan.dashboard.more')} + + ) + } + bordered={false} + style={{ overflow: 'hidden' }} + > + {props.children} + + ); +}); +DashboardItem.displayName = 'DashboardItem'; + +const DataItem: React.FC<{ + icon: React.ReactElement; + title: string; + count: number; +}> = React.memo((props) => { + return ( + +
+ {props.icon} +
+
+
{props.title}
+
{props.count}
+
+
+ ); +}); +DataItem.displayName = 'DataItem'; diff --git a/admin/src/auth.ts b/admin/src/auth.ts index 92bc39078..504fbaf6c 100644 --- a/admin/src/auth.ts +++ b/admin/src/auth.ts @@ -1,5 +1,5 @@ import { createAuthProvider, type AuthProvider } from 'tushan'; export const authProvider: AuthProvider = createAuthProvider({ - loginUrl: `${import.meta.env.VITE_PUBLIC_SERVER_URL}api/login` + loginUrl: `${import.meta.env.VITE_PUBLIC_SERVER_URL}/api/login` }); diff --git a/admin/src/fields.ts b/admin/src/fields.ts index 08ec91afc..42d4d437f 100644 --- a/admin/src/fields.ts +++ b/admin/src/fields.ts @@ -1,13 +1,11 @@ -import { - createTextField, - createNumberField, -} from 'tushan'; +import { createTextField, createNumberField } from 'tushan'; export const userFields = [ createTextField('id', { label: 'ID' }), createTextField('username', { label: '用户名' }), createNumberField('balance', { label: '余额', list: { sort: true } }), - createTextField('createTime', { label: 'Create Time', list: { sort: true } }) + createTextField('createTime', { label: 'Create Time', list: { sort: true } }), + createTextField('password', { label: '密码', list: { hidden: true } }) ]; export const payFields = [