feat: 增加admin登录鉴权逻辑

pull/70/head
moonrailgun 2 years ago
parent 867fbd3223
commit 2c1aa02428

@ -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:

@ -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}
>
<Resource
icon={PersonIcon}

@ -1,33 +1,53 @@
import { AuthProvider } from 'react-admin';
export const authStorageKey = 'tailchat:admin:auth';
export const authProvider: AuthProvider = {
login: ({ username, password }) => {
// 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(''),
};

@ -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 };

@ -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];
}
}
}

@ -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));
}
};
}

@ -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",

@ -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');

@ -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;

Loading…
Cancel
Save