From 2c1aa02428b0ea04dde1ac0cf40511c690e2e635 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Fri, 13 Jan 2023 00:41:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0admin=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E9=89=B4=E6=9D=83=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 29 +++++++- server/admin/app/ra/App.tsx | 11 +-- server/admin/app/ra/authProvider.ts | 48 +++++++++---- server/admin/app/server/api.ts | 65 +++++++++++++++++ server/admin/app/server/index.ts | 100 +++++++++++++++++++------- server/admin/app/server/middleware.ts | 39 ++++++++++ server/admin/package.json | 6 +- server/admin/server.ts | 75 +------------------ server/models/group/group.ts | 5 ++ 9 files changed, 257 insertions(+), 121 deletions(-) create mode 100644 server/admin/app/server/api.ts create mode 100644 server/admin/app/server/middleware.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a7c17e1..c3ba659f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -903,9 +903,11 @@ importers: '@typegoose/typegoose': 9.3.1 '@types/compression': ^1.7.2 '@types/express': ^4.17.15 + '@types/md5': ^2.3.2 '@types/morgan': ^1.9.4 '@types/react': ^18.0.25 '@types/react-dom': ^18.0.8 + body-parser: ^1.20.1 compression: ^1.7.4 cross-env: ^7.0.3 dotenv: ^16.0.3 @@ -913,7 +915,9 @@ importers: express-mongoose-ra-json-server: ^0.1.0 filesize: ^8.0.7 isbot: ^3.6.5 + jsonwebtoken: ^8.5.1 lodash: ^4.17.21 + md5: ^2.3.0 morgan: ^1.10.0 nodemon: ^2.0.20 npm-run-all: ^4.1.5 @@ -931,12 +935,16 @@ importers: '@remix-run/node': 1.9.0_biqbaboplfbrettd7655fr4n2y '@remix-run/react': 1.9.0_biqbaboplfbrettd7655fr4n2y '@typegoose/typegoose': 9.3.1 + '@types/md5': 2.3.2 + body-parser: 1.20.1 compression: 1.7.4 express: 4.18.2 express-mongoose-ra-json-server: 0.1.0_express@4.18.2 filesize: 8.0.7 isbot: 3.6.5 + jsonwebtoken: 8.5.1 lodash: 4.17.21 + md5: 2.3.0 morgan: 1.10.0 ra-data-json-server: 4.6.3_7gebip3sgfc5uoqbhvi5xascza react: 18.2.0 @@ -13197,6 +13205,10 @@ packages: resolution: {integrity: sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==} dev: false + /@types/md5/2.3.2: + resolution: {integrity: sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==} + dev: false + /@types/mdast/3.0.10: resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} dependencies: @@ -16662,6 +16674,10 @@ packages: /chardet/0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + /charenc/0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + /cheerio-select/2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} dependencies: @@ -17988,6 +18004,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypt/0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + /crypto-browserify/3.12.0: resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} dependencies: @@ -23632,7 +23652,6 @@ packages: /is-buffer/1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - dev: true /is-buffer/2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} @@ -26097,6 +26116,14 @@ packages: inherits: 2.0.4 safe-buffer: 5.2.1 + /md5/2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + /mdast-squeeze-paragraphs/4.0.0: resolution: {integrity: sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==} dependencies: diff --git a/server/admin/app/ra/App.tsx b/server/admin/app/ra/App.tsx index 8b84c958..24192c66 100644 --- a/server/admin/app/ra/App.tsx +++ b/server/admin/app/ra/App.tsx @@ -7,7 +7,7 @@ import { ShowGuesser, } from 'react-admin'; import jsonServerProvider from 'ra-data-json-server'; -import { authProvider } from './authProvider'; +import { authProvider, authStorageKey } from './authProvider'; import { UserList } from './resources/user'; import React from 'react'; import { GroupList } from './resources/group'; @@ -25,7 +25,9 @@ const httpClient: typeof fetchUtils.fetchJson = (url, options = {}) => { if (!options.headers) { options.headers = new Headers({ Accept: 'application/json' }); } - const { token } = JSON.parse(window.localStorage.getItem('auth') ?? ''); + const { token } = JSON.parse( + window.localStorage.getItem(authStorageKey) ?? '{}' + ); (options.headers as Headers).set('Authorization', `Bearer ${token}`); return fetchUtils.fetchJson(url, options); @@ -36,8 +38,8 @@ const httpClient: typeof fetchUtils.fetchJson = (url, options = {}) => { const dataProvider = jsonServerProvider( // 'https://jsonplaceholder.typicode.com' - '/admin/api' - // httpClient + '/admin/api', + httpClient ); export const App = () => ( @@ -48,6 +50,7 @@ export const App = () => ( disableTelemetry={true} authProvider={authProvider} dataProvider={dataProvider} + requireAuth={true} > { - // TODO - if (username !== 'admin' || password !== 'admin') { - return Promise.reject(); - } - localStorage.setItem('username', username); - return Promise.resolve(); + const request = new Request('/admin/api/login', { + method: 'POST', + body: JSON.stringify({ username, password }), + headers: new Headers({ 'Content-Type': 'application/json' }), + }); + return fetch(request) + .then((response) => { + return response.json(); + }) + .then((auth) => { + localStorage.setItem(authStorageKey, JSON.stringify(auth)); + }) + .catch(() => { + throw new Error('登录失败'); + }); }, logout: () => { - localStorage.removeItem('username'); + localStorage.removeItem(authStorageKey); return Promise.resolve(); }, checkAuth: () => - localStorage.getItem('username') ? Promise.resolve() : Promise.reject(), + localStorage.getItem(authStorageKey) ? Promise.resolve() : Promise.reject(), checkError: (error) => { const status = error.status; if (status === 401 || status === 403) { - localStorage.removeItem('username'); + localStorage.removeItem(authStorageKey); return Promise.reject(); } + // other error code (404, 500, etc): no need to log out return Promise.resolve(); }, - getIdentity: () => - Promise.resolve({ - id: 'user', - fullName: 'Admin', - }), + getIdentity: () => { + const { username } = JSON.parse( + localStorage.getItem(authStorageKey) ?? '{}' + ); + if (!username) { + return Promise.reject(); + } + + return Promise.resolve({ + id: username, + fullName: username, + }); + }, getPermissions: () => Promise.resolve(''), }; diff --git a/server/admin/app/server/api.ts b/server/admin/app/server/api.ts new file mode 100644 index 00000000..7b3ed00a --- /dev/null +++ b/server/admin/app/server/api.ts @@ -0,0 +1,65 @@ +import { Router } from 'express'; +import raExpressMongoose from 'express-mongoose-ra-json-server'; +import jwt from 'jsonwebtoken'; +import { adminAuth, auth, authSecret } from './middleware'; + +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 + ); + + res.json({ + username, + token: token, + }); + } else { + res.status(401).end('username or password incorrect'); + } +}); + +router.use( + '/users', + auth(), + raExpressMongoose(require('../../../models/user/user').default, { + q: ['nickname', 'email'], + }) +); +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 }; diff --git a/server/admin/app/server/index.ts b/server/admin/app/server/index.ts index b4010c0e..631ff7a7 100644 --- a/server/admin/app/server/index.ts +++ b/server/admin/app/server/index.ts @@ -1,32 +1,78 @@ -import { Router } from 'express'; -import raExpressMongoose from 'express-mongoose-ra-json-server'; +import path from 'path'; +import express from 'express'; +import compression from 'compression'; +import morgan from 'morgan'; +import { createRequestHandler } from '@remix-run/express'; +import mongoose from 'mongoose'; +import bodyParser from 'body-parser'; +import { router } from './api'; -const router = Router(); +// 链接数据库 +mongoose.connect(process.env.MONGO_URL!, (error: any) => { + if (!error) { + return console.info('Mongo connected'); + } + console.error(error); +}); -router.use( - '/users', - raExpressMongoose(require('../../../models/user/user').default, { - q: ['nickname', 'email'], - }) -); -router.use( - '/messages', - raExpressMongoose(require('../../../models/chat/message').default, { - q: ['content'], - allowedRegexFields: ['content'], - }) -); -router.use( - '/groups', - raExpressMongoose(require('../../../models/group/group').default, { - q: ['name'], - }) +const BUILD_DIR = path.join(process.cwd(), 'build'); + +const app = express(); + +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' }) ); -router.use( - '/file', - raExpressMongoose(require('../../../models/file').default, { - q: ['objectName'], - }) + +// 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', router); + +app.all( + '/admin/*', + process.env.NODE_ENV === 'development' + ? (req, res, next) => { + purgeRequireCache(); + + return createRequestHandler({ + build: require(BUILD_DIR), + mode: process.env.NODE_ENV, + })(req, res, next); + } + : createRequestHandler({ + build: require(BUILD_DIR), + mode: process.env.NODE_ENV, + }) ); -export { router }; +const port = process.env.PORT || 3000; + +app.listen(port, () => { + console.log( + `Express server listening on port ${port}, visit with: http://localhost:${port}/admin/` + ); +}); + +function purgeRequireCache() { + // purge require cache on requests for "server side HMR" this won't let + // you have in-memory objects between requests in development, + // alternatively you can set up nodemon/pm2-dev to restart the server on + // file changes, but then you'll have to reconnect to databases/etc on each + // change. We prefer the DX of this, so we've included it for you by default + for (const key in require.cache) { + if (key.startsWith(BUILD_DIR)) { + delete require.cache[key]; + } + } +} diff --git a/server/admin/app/server/middleware.ts b/server/admin/app/server/middleware.ts new file mode 100644 index 00000000..1abd6fe8 --- /dev/null +++ b/server/admin/app/server/middleware.ts @@ -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(500).end(String(err)); + } + }; +} diff --git a/server/admin/package.json b/server/admin/package.json index 613e0e8f..1830196d 100644 --- a/server/admin/package.json +++ b/server/admin/package.json @@ -7,7 +7,7 @@ "dev": "remix build && run-p \"dev:*\"", "dev:node": "cross-env NODE_ENV=development nodemon", "dev:remix": "remix watch", - "start": "cross-env NODE_ENV=production node ./server.js", + "start": "cross-env NODE_ENV=production ts-node ./server.ts", "typecheck": "tsc -b" }, "dependencies": { @@ -17,12 +17,16 @@ "@remix-run/node": "^1.9.0", "@remix-run/react": "^1.9.0", "@typegoose/typegoose": "9.3.1", + "@types/md5": "^2.3.2", + "body-parser": "^1.20.1", "compression": "^1.7.4", "express": "^4.18.2", "express-mongoose-ra-json-server": "^0.1.0", "filesize": "^8.0.7", "isbot": "^3.6.5", + "jsonwebtoken": "^8.5.1", "lodash": "^4.17.21", + "md5": "^2.3.0", "morgan": "^1.10.0", "ra-data-json-server": "^4.6.3", "react": "^18.2.0", diff --git a/server/admin/server.ts b/server/admin/server.ts index c485e713..65642526 100644 --- a/server/admin/server.ts +++ b/server/admin/server.ts @@ -1,78 +1,5 @@ import path from 'path'; -import express from 'express'; -import compression from 'compression'; -import morgan from 'morgan'; -import { createRequestHandler } from '@remix-run/express'; -import mongoose from 'mongoose'; import dotenv from 'dotenv'; -import { router } from './app/server/index'; dotenv.config({ path: path.resolve(__dirname, '../.env') }); -// 链接数据库 -mongoose.connect(process.env.MONGO_URL!, (error: any) => { - if (!error) { - return console.info('Mongo connected'); - } - console.error(error); -}); - -const BUILD_DIR = path.join(process.cwd(), 'build'); - -const app = express(); - -app.use(compression()); - -// 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', router); - -app.all( - '/admin/*', - process.env.NODE_ENV === 'development' - ? (req, res, next) => { - purgeRequireCache(); - - return createRequestHandler({ - build: require(BUILD_DIR), - mode: process.env.NODE_ENV, - })(req, res, next); - } - : createRequestHandler({ - build: require(BUILD_DIR), - mode: process.env.NODE_ENV, - }) -); - -const port = process.env.PORT || 3000; - -app.listen(port, () => { - console.log( - `Express server listening on port ${port}, visit with: http://localhost:${port}/admin/` - ); -}); - -function purgeRequireCache() { - // purge require cache on requests for "server side HMR" this won't let - // you have in-memory objects between requests in development, - // alternatively you can set up nodemon/pm2-dev to restart the server on - // file changes, but then you'll have to reconnect to databases/etc on each - // change. We prefer the DX of this, so we've included it for you by default - for (const key in require.cache) { - if (key.startsWith(BUILD_DIR)) { - delete require.cache[key]; - } - } -} +import('./app/server'); diff --git a/server/models/group/group.ts b/server/models/group/group.ts index 84353476..29fa749c 100644 --- a/server/models/group/group.ts +++ b/server/models/group/group.ts @@ -98,6 +98,11 @@ export class GroupRole implements Base { /** * 群组 */ +@modelOptions({ + options: { + allowMixed: Severity.ALLOW, + }, +}) export class Group extends TimeStamps implements Base { _id: Types.ObjectId; id: string;