refactor: move backend logic to admin-next

pull/90/head
moonrailgun 2 years ago
parent 6640397b7a
commit 9e6912fa91

@ -1542,25 +1542,64 @@ importers:
server/admin-next: server/admin-next:
dependencies: dependencies:
'@fastify/busboy':
specifier: ^1.1.0
version: 1.1.0
body-parser:
specifier: ^1.20.1
version: 1.20.1
compression:
specifier: ^1.7.4
version: 1.7.4
express: express:
specifier: ^4.18.2 specifier: ^4.18.2
version: 4.18.2 version: 4.18.2
express-mongoose-ra-json-server:
specifier: ^0.1.0
version: 0.1.0(express@4.18.2)(mongoose@6.1.1)
jsonwebtoken:
specifier: ^8.5.1
version: 8.5.1
lodash:
specifier: ^4.17.21
version: 4.17.21
md5:
specifier: ^2.3.0
version: 2.3.0
morgan:
specifier: ^1.10.0
version: 1.10.0
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
react-dom: react-dom:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
tailchat-server-sdk:
specifier: workspace:^
version: link:../packages/sdk
tushan: tushan:
specifier: ^0.2.1 specifier: ^0.2.2
version: 0.2.1(react-is@18.2.0)(ts-node@10.9.1) version: 0.2.2(ts-node@10.9.1)
vite-express: vite-express:
specifier: ^0.5.4 specifier: ^0.5.4
version: 0.5.4(express@4.18.2)(vite@4.2.0) version: 0.5.4(express@4.18.2)(vite@4.2.0)
devDependencies: devDependencies:
'@types/body-parser':
specifier: ^1.19.2
version: 1.19.2
'@types/compression':
specifier: ^1.7.2
version: 1.7.2
'@types/express': '@types/express':
specifier: ^4.17.15 specifier: ^4.17.15
version: 4.17.15 version: 4.17.15
'@types/md5':
specifier: ^2.3.2
version: 2.3.2
'@types/morgan':
specifier: ^1.9.4
version: 1.9.4
'@types/react': '@types/react':
specifier: 18.0.20 specifier: 18.0.20
version: 18.0.20 version: 18.0.20
@ -7062,7 +7101,7 @@ packages:
'@jest/test-result': 27.5.1 '@jest/test-result': 27.5.1
'@jest/transform': 27.5.1 '@jest/transform': 27.5.1
'@jest/types': 27.5.1 '@jest/types': 27.5.1
'@types/node': 18.15.11 '@types/node': 18.15.12
ansi-escapes: 4.3.2 ansi-escapes: 4.3.2
chalk: 4.1.2 chalk: 4.1.2
emittery: 0.8.1 emittery: 0.8.1
@ -12108,7 +12147,7 @@ packages:
mongoose: 6.0.15 mongoose: 6.0.15
reflect-metadata: 0.1.13 reflect-metadata: 0.1.13
semver: 7.3.8 semver: 7.3.8
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/@typegoose/typegoose@9.3.1(mongoose@6.1.1): /@typegoose/typegoose@9.3.1(mongoose@6.1.1):
@ -12641,7 +12680,6 @@ packages:
/@types/md5@2.3.2: /@types/md5@2.3.2:
resolution: {integrity: sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==} resolution: {integrity: sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==}
dev: false
/@types/mdast@3.0.10: /@types/mdast@3.0.10:
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
@ -12812,7 +12850,7 @@ packages:
/@types/pino@6.3.12: /@types/pino@6.3.12:
resolution: {integrity: sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==} resolution: {integrity: sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==}
dependencies: dependencies:
'@types/node': 18.15.11 '@types/node': 18.15.12
'@types/pino-pretty': 4.7.5 '@types/pino-pretty': 4.7.5
'@types/pino-std-serializers': 2.4.1 '@types/pino-std-serializers': 2.4.1
sonic-boom: 2.8.0 sonic-boom: 2.8.0
@ -19286,6 +19324,17 @@ packages:
mongoose: 6.0.15 mongoose: 6.0.15
dev: false dev: false
/express-mongoose-ra-json-server@0.1.0(express@4.18.2)(mongoose@6.1.1):
resolution: {integrity: sha512-y/Z5zb3RWVsCLgof8MMWMmpyAe2ZUEy6sYqczELOqaCr0cChbI9Y1VNr5DcbwEjqWmEIoU9aqcsURoUse8r/gw==}
peerDependencies:
express: ^4.0.0 || ^3.0.0
mongoose: ^6.0.0 || ^5.0.0
dependencies:
escape-string-regexp: 4.0.0
express: 4.18.2
mongoose: 6.1.1
dev: false
/express@4.18.2: /express@4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
@ -23298,7 +23347,7 @@ packages:
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies: dependencies:
'@jest/types': 27.5.1 '@jest/types': 27.5.1
'@types/node': 18.15.11 '@types/node': 18.15.12
chalk: 4.1.2 chalk: 4.1.2
ci-info: 3.8.0 ci-info: 3.8.0
graceful-fs: 4.2.10 graceful-fs: 4.2.10
@ -34261,10 +34310,6 @@ packages:
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
dev: false dev: false
/tslib@2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
dev: false
/tslib@2.5.0: /tslib@2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
@ -34302,8 +34347,8 @@ packages:
domino: 2.1.6 domino: 2.1.6
dev: false dev: false
/tushan@0.2.1(react-is@18.2.0)(ts-node@10.9.1): /tushan@0.2.2(ts-node@10.9.1):
resolution: {integrity: sha512-ZLbG1yu6q/Dv7hXoSCjaK/SspsNxew2wx9Y9WUM5uPQ5odL+uVr9N7N4pZ536Sejat2dvm19/QPTSIXn8RHT0g==} resolution: {integrity: sha512-7Nxw5Ru9kQ8XyPZDgeS0CLRd0cbMs4INDgDt9kSUvqphnFdp7bMrG9FQGh+2RNYG65JIDZGhsR83Uv9wg2Esrg==}
dependencies: dependencies:
'@arco-design/web-react': 2.47.1(@types/react@18.0.20)(react-dom@18.2.0)(react@18.2.0) '@arco-design/web-react': 2.47.1(@types/react@18.0.20)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query': 4.29.3(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-query': 4.29.3(react-dom@18.2.0)(react@18.2.0)
@ -34320,7 +34365,7 @@ packages:
qs: 6.11.1 qs: 6.11.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
react-fastify-form: 1.0.12(react@18.2.0) react-is: 18.2.0
react-router: 6.8.1(react@18.2.0) react-router: 6.8.1(react@18.2.0)
react-router-dom: 6.8.1(react-dom@18.2.0)(react@18.2.0) react-router-dom: 6.8.1(react-dom@18.2.0)(react@18.2.0)
styled-components: 5.3.10(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) styled-components: 5.3.10(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
@ -34329,7 +34374,6 @@ packages:
zustand: 4.3.6(immer@9.0.21)(react@18.2.0) zustand: 4.3.6(immer@9.0.21)(react@18.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
- react-is
- react-native - react-native
- ts-node - ts-node
dev: false dev: false

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" /> <link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tushan</title> <title>Tailchat Admin</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

@ -9,14 +9,27 @@
"build": "vite build" "build": "vite build"
}, },
"dependencies": { "dependencies": {
"@fastify/busboy": "^1.1.0",
"body-parser": "^1.20.1",
"compression": "^1.7.4",
"express": "^4.18.2", "express": "^4.18.2",
"express-mongoose-ra-json-server": "^0.1.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"morgan": "^1.10.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tushan": "^0.2.1", "tailchat-server-sdk": "workspace:^",
"tushan": "^0.2.2",
"vite-express": "^0.5.4" "vite-express": "^0.5.4"
}, },
"devDependencies": { "devDependencies": {
"@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2",
"@types/express": "^4.17.15", "@types/express": "^4.17.15",
"@types/md5": "^2.3.2",
"@types/morgan": "^1.9.4",
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^3.1.0",

@ -1,33 +1,66 @@
import { AuthProvider } from 'tushan'; import type { AuthProvider } from 'tushan';
export const authStorageKey = 'tailchat:admin:auth';
export const authProvider: AuthProvider = { export const authProvider: AuthProvider = {
login: ({ username, password }) => { login: ({ username, password }) => {
if (username !== 'tushan' || password !== 'tushan') { const request = new Request('/admin/api/login', {
return Promise.reject(); method: 'POST',
} body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
localStorage.setItem('username', username); return fetch(request)
return Promise.resolve(); .then((response) => {
return response.json();
})
.then((auth) => {
console.log(auth);
localStorage.setItem(authStorageKey, JSON.stringify(auth));
})
.catch(() => {
throw new Error('Login Failed');
});
}, },
logout: () => { logout: () => {
localStorage.removeItem('username'); localStorage.removeItem(authStorageKey);
return Promise.resolve(); return Promise.resolve();
}, },
checkAuth: () => checkAuth: () => {
localStorage.getItem('username') ? Promise.resolve() : Promise.reject(), const auth = localStorage.getItem(authStorageKey);
if (auth) {
try {
const obj = JSON.parse(auth);
if (obj.expiredAt && Date.now() < obj.expiredAt) {
return Promise.resolve();
}
} catch (err) {}
}
return Promise.reject();
},
checkError: (error) => { checkError: (error) => {
const status = error.status; const status = error.status;
if (status === 401 || status === 403) { if (status === 401 || status === 403) {
localStorage.removeItem('username'); localStorage.removeItem(authStorageKey);
return Promise.reject(); return Promise.reject();
} }
// other error code (404, 500, etc): no need to log out
return Promise.resolve(); return Promise.resolve();
}, },
getIdentity: () => getIdentity: () => {
Promise.resolve({ const { username } = JSON.parse(
id: '0', localStorage.getItem(authStorageKey) ?? '{}'
fullName: 'Admin', );
}), if (!username) {
return Promise.reject();
}
return Promise.resolve({
id: username,
fullName: username,
});
},
getPermissions: () => Promise.resolve(''), getPermissions: () => Promise.resolve(''),
}; };

@ -0,0 +1,28 @@
import { TcBroker, SYSTEM_USERID } from 'tailchat-server-sdk';
import brokerConfig from '../../../moleculer.config';
const transporter = process.env.TRANSPORTER;
export const broker = new TcBroker({
...brokerConfig,
metrics: false,
logger: false,
transporter,
});
broker.start().then(() => {
console.log('Linked to Tailchat network, TRANSPORTER: ', transporter);
});
export function callBrokerAction<T>(
actionName: string,
params: any,
opts?: Record<string, any>
): Promise<T> {
return broker.call(actionName, params, {
...opts,
meta: {
...opts?.meta,
userId: SYSTEM_USERID,
},
});
}

@ -1,16 +1,58 @@
import express from 'express'; import express from 'express';
import ViteExpress from 'vite-express'; import ViteExpress from 'vite-express';
import mongoose from 'mongoose';
import compression from 'compression';
import morgan from 'morgan';
import bodyParser from 'body-parser';
import path from 'path';
import dotenv from 'dotenv';
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
import { apiRouter } from './router/api';
const app = express(); const app = express();
const port = Number(process.env.PORT || 13000); const port = Number(process.env.ADMIN_PORT || 13000);
app.get('/hello', (_, res) => { if (!process.env.MONGO_URL) {
res.send('Hello Vite + React + TypeScript!'); console.error('Require env: MONGO_URL');
process.exit(1);
}
// 链接数据库
mongoose.connect(process.env.MONGO_URL, (error: any) => {
if (!error) {
return console.info('Datebase connected');
}
console.error('Datebase connect error', error);
});
app.use(compression());
app.use(bodyParser());
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable('x-powered-by');
// Remix fingerprints its assets so we can cache forever.
app.use(
'/build',
express.static('public/build', { immutable: true, maxAge: '1y' })
);
// Everything else (like favicon.ico) is cached for an hour. You may want to be
// more aggressive with this caching.
app.use(express.static('public', { maxAge: '1h' }));
app.use(morgan('tiny'));
app.use('/admin/api', apiRouter);
app.use((err: any, req: any, res: any, next: any) => {
res.status(500);
res.json({ error: err.message });
}); });
ViteExpress.listen(app, port, () => ViteExpress.listen(app, port, () =>
console.log( console.log(
`Server is listening on port ${port}, visit with: http://localhost:${port}` `Server is listening on port ${port}, visit with: http://localhost:${port}/admin/`
) )
); );

@ -0,0 +1,39 @@
import type { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import md5 from 'md5';
export const adminAuth = {
username: process.env.ADMIN_USER,
password: process.env.ADMIN_PASS,
};
export const authSecret =
(process.env.SECRET || 'tailchat') + md5(JSON.stringify(adminAuth)); // 增加一个md5的盐值确保SECRET没有设置的情况下只修改了用户名密码也不会被人伪造token秘钥
export function auth() {
return (req: Request, res: Response, next: NextFunction) => {
try {
const authorization = req.headers.authorization;
if (!authorization) {
res.status(401).end('not found authorization in headers');
return;
}
const token = authorization.slice('Bearer '.length);
const payload = jwt.verify(token, authSecret);
if (typeof payload === 'string') {
res.status(401).end('payload type error');
return;
}
if (payload.platform !== 'admin') {
res.status(401).end('Payload invalid');
return;
}
next();
} catch (err) {
res.status(401).end(String(err));
}
};
}

@ -0,0 +1,90 @@
import { Router } from 'express';
import raExpressMongoose from 'express-mongoose-ra-json-server';
import jwt from 'jsonwebtoken';
import { callBrokerAction } from '../broker';
import { adminAuth, auth, authSecret } from '../middleware/auth';
import { configRouter } from './config';
import { networkRouter } from './network';
import { fileRouter } from './file';
const router = Router();
router.post('/login', (req, res) => {
if (!adminAuth.username || !adminAuth.password) {
res.status(401).end('Server not set env: ADMIN_USER, ADMIN_PASS');
return;
}
const { username, password } = req.body;
if (username === adminAuth.username && password === adminAuth.password) {
// 用户名和密码都正确返回token
const token = jwt.sign(
{
username,
platform: 'admin',
},
authSecret,
{
expiresIn: '2h',
}
);
res.json({
username,
token: token,
expiredAt: new Date().valueOf() + 2 * 60 * 60 * 1000,
});
} else {
res.status(401).end('username or password incorrect');
}
});
router.use('/network', networkRouter);
router.use('/config', configRouter);
router.use('/file', fileRouter);
router.use(
'/users',
auth(),
raExpressMongoose(require('../../../../models/user/user').default, {
q: ['nickname', 'email'],
})
);
router.delete('/messages/:id', auth(), async (req, res) => {
try {
const messageId = req.params.id;
await callBrokerAction('chat.message.deleteMessage', {
messageId,
});
res.json({ id: messageId });
} catch (err) {
console.error(err);
res.status(500).json({ message: (err as any).message });
}
});
router.use(
'/messages',
auth(),
raExpressMongoose(require('../../../../models/chat/message').default, {
q: ['content'],
allowedRegexFields: ['content'],
})
);
router.use(
'/groups',
auth(),
raExpressMongoose(require('../../../../models/group/group').default, {
q: ['name'],
})
);
router.use(
'/file',
auth(),
raExpressMongoose(require('../../../../models/file').default, {
q: ['objectName'],
})
);
export { router as apiRouter };

@ -0,0 +1,38 @@
/**
* Network
*/
import { Router } from 'express';
import { broker } from '../broker';
import { auth } from '../middleware/auth';
const router = Router();
router.get('/client', auth(), async (req, res, next) => {
try {
const config = await broker.call('config.client');
res.json({
config,
});
} catch (err) {
next(err);
}
});
router.patch('/client', auth(), async (req, res, next) => {
try {
await broker.call('config.setClientConfig', {
key: req.body.key,
value: req.body.value,
});
res.json({
success: true,
});
} catch (err) {
next(err);
}
});
export { router as configRouter };

@ -0,0 +1,60 @@
/**
* Network
*/
import { Router } from 'express';
import { callBrokerAction } from '../broker';
import { auth } from '../middleware/auth';
import Busboy from '@fastify/busboy';
const router = Router();
router.put('/upload', auth(), async (req, res) => {
const busboy = new Busboy({ headers: req.headers as any });
const promises: Promise<any>[] = [];
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
promises.push(
callBrokerAction('file.save', file, {
filename: filename,
})
.then((data) => {
console.log(data);
return data;
})
.catch((err) => {
file.resume(); // Drain file stream to continue processing form
busboy.emit('error', err);
return err;
})
);
});
busboy.on('finish', async () => {
/* istanbul ignore next */
if (promises.length == 0) {
res.status(500).json('File missing in the request');
return;
}
try {
const files = await Promise.all(promises);
res.json({ files });
} catch (err) {
console.error(err);
res.status(500).json(String(err));
}
});
busboy.on('error', (err) => {
console.error(err);
req.unpipe(busboy);
req.resume();
res.status(500).json({ err });
});
req.pipe(busboy);
});
export { router as fileRouter };

@ -0,0 +1,37 @@
/**
* Network
*/
import { Router } from 'express';
import { broker } from '../broker';
import { auth } from '../middleware/auth';
import _ from 'lodash';
const router = Router();
router.get('/all', auth(), async (req, res) => {
res.json({
nodes: Array.from(new Map(broker.registry.nodes.nodes).values()).map(
(item) =>
_.pick(item, [
'id',
'available',
'local',
'ipList',
'hostname',
'cpu',
'client',
])
),
events: broker.registry.events.events.map((item: any) => item.name),
services: broker.registry.services.services.map((item: any) => item.name),
actions: Array.from(new Map(broker.registry.actions.actions).keys()),
});
});
router.get('/ping', auth(), async (req, res) => {
const pong = await broker.ping();
res.json(pong);
});
export { router as networkRouter };

@ -7,7 +7,9 @@
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"module": "CommonJS", "module": "CommonJS",

Loading…
Cancel
Save