@azteam/express 1.2.282 → 1.2.283

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Server.js ADDED
@@ -0,0 +1,409 @@
1
+ import _ from 'lodash';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import http from 'http';
5
+ import express from 'express';
6
+ import helmet from 'helmet';
7
+ import methodOverride from 'method-override';
8
+ import bodyParser from 'body-parser';
9
+ import cookieParser from 'cookie-parser';
10
+ import morgan from 'morgan';
11
+ import cors from 'cors';
12
+ import psl from 'psl';
13
+ import 'express-async-errors';
14
+ import {CORS, errorCatch, ErrorException, NOT_FOUND, UNKNOWN} from '@azteam/error';
15
+
16
+ const RES_TYPE = {
17
+ ARRAY: 'ARRAY',
18
+ OBJECT: 'OBJECT',
19
+ DOCS: 'DOCS',
20
+ };
21
+
22
+ function omitItem(item, guard, allow) {
23
+ let guardFields = guard;
24
+ let itemFields = item;
25
+
26
+ if (_.isArray(guardFields)) {
27
+ guardFields = _.difference(guardFields, allow);
28
+ }
29
+
30
+ if (itemFields.toJSON) {
31
+ itemFields = item.toJSON();
32
+ }
33
+ if (_.isObject(itemFields)) {
34
+ if (guardFields === '*') {
35
+ return _.pick(itemFields, allow);
36
+ }
37
+ return _.omit(itemFields, guardFields);
38
+ }
39
+ return itemFields;
40
+ }
41
+
42
+ class Server {
43
+ constructor(currentDir = '', options = {}) {
44
+ this.redis = null;
45
+ this.options = {
46
+ isAllowEmptyOrigin: true,
47
+ ...options,
48
+ };
49
+
50
+ this.cookieOption = {
51
+ domain: null,
52
+ path: '/',
53
+ secure: process.env.NODE_ENV !== 'development',
54
+ httpOnly: true,
55
+ signed: true,
56
+ sameSite: 'Lax',
57
+ };
58
+
59
+ this.middlewares = [];
60
+ this.controllers = [];
61
+ this.whiteList = [];
62
+ this.debug = process.env.NODE_ENV === 'development';
63
+
64
+ this.initController(currentDir);
65
+ }
66
+
67
+ setRedis(redis) {
68
+ this.redis = redis;
69
+ return this;
70
+ }
71
+
72
+ setCookieOption(cookieOption) {
73
+ this.cookieOption = {
74
+ ...this.cookieOption,
75
+ ...cookieOption,
76
+ };
77
+ return this;
78
+ }
79
+
80
+ setCallbackError(callback = null) {
81
+ this.callbackError = callback;
82
+ return this;
83
+ }
84
+
85
+ setWhiteList(whiteList) {
86
+ this.whiteList = whiteList;
87
+ return this;
88
+ }
89
+
90
+ setDebug(debug) {
91
+ this.debug = debug;
92
+ return this;
93
+ }
94
+
95
+ addController(name, version, controller) {
96
+ this.controllers.push({
97
+ name,
98
+ version,
99
+ controller,
100
+ });
101
+ return this;
102
+ }
103
+
104
+ initController(apiDir) {
105
+ if (apiDir) {
106
+ const controllerDirs = fs.readdirSync(apiDir);
107
+
108
+ for (let i = 0; i < controllerDirs.length; i += 1) {
109
+ const dirName = controllerDirs[i];
110
+ if (fs.statSync(`${apiDir}/${dirName}`).isDirectory()) {
111
+ const versionDirs = fs.readdirSync(`${apiDir}/${dirName}`);
112
+
113
+ for (let j = 0; j < versionDirs.length; j += 1) {
114
+ const versionName = versionDirs[j];
115
+
116
+ // eslint-disable-next-line global-require,import/no-dynamic-require
117
+ const controller = require(`${apiDir}/${dirName}/${versionName}/controller`).default;
118
+ this.addController(dirName, versionName, controller);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return this;
124
+ }
125
+
126
+ addGlobalMiddleware(middleware) {
127
+ this.middlewares.push(middleware);
128
+ return this;
129
+ }
130
+
131
+ startAPI(port) {
132
+ if (!_.isEmpty(this.controllers)) {
133
+ const WHITE_LIST = this.whiteList;
134
+ const COOKIE_OPTION = this.cookieOption;
135
+ const {isAllowEmptyOrigin} = this.options;
136
+
137
+ const app = express();
138
+ app.use(
139
+ helmet({
140
+ frameguard: false,
141
+ })
142
+ );
143
+
144
+ app.use(methodOverride());
145
+ app.use(bodyParser.urlencoded({limit: '5mb', extended: true}));
146
+ app.use(bodyParser.json({limit: '5mb'}));
147
+
148
+ app.set('trust proxy', 1);
149
+
150
+ app.use(cookieParser(process.env.SECRET_KEY));
151
+
152
+ app.use(
153
+ cors(function (req, callback) {
154
+ const origin = req.header('Origin');
155
+ const authorization = req.header('Authorization');
156
+ const appSecret = req.header('x-app-secret');
157
+ const agent = req.header('User-Agent');
158
+
159
+ let error = null;
160
+ if (!authorization && !appSecret && !agent.startsWith('toda')) {
161
+ if (!origin) {
162
+ if (!isAllowEmptyOrigin) {
163
+ error = new ErrorException(CORS, `Not allowed by CORS`);
164
+ }
165
+ } else if (!WHITE_LIST.some((re) => origin.endsWith(re))) {
166
+ error = new ErrorException(CORS, `${origin} Not allowed by CORS`);
167
+ }
168
+ }
169
+ callback(error, {
170
+ credentials: true,
171
+ origin: true,
172
+ });
173
+ })
174
+ );
175
+
176
+ if (this.debug) {
177
+ app.use(morgan('dev'));
178
+ }
179
+
180
+ app.get('/robots.txt', function (req, res) {
181
+ res.type('text/plain');
182
+ res.send('User-agent: *\nDisallow: /');
183
+ });
184
+ app.get('/favicon.ico', (req, res) => res.status(204).json({}));
185
+
186
+ const {redis} = this;
187
+
188
+ if (redis) {
189
+ app.request.redis = redis;
190
+ app.response.redis = redis;
191
+ }
192
+
193
+ app.response.error = function (code, errors = []) {
194
+ throw new ErrorException(code, errors);
195
+ };
196
+
197
+ app.response.success = function (data = {}, guard = [], allow = []) {
198
+ let guardData = data;
199
+ if (data) {
200
+ let resType = null;
201
+ if (_.isArray(data)) {
202
+ resType = RES_TYPE.ARRAY;
203
+ } else if (_.isObject(data)) {
204
+ resType = RES_TYPE.OBJECT;
205
+ if (data.docs) {
206
+ resType = RES_TYPE.DOCS;
207
+ }
208
+ }
209
+
210
+ let responseGuard = guard;
211
+ const responseAllows = allow;
212
+ if (_.isArray(guard)) {
213
+ responseGuard = [
214
+ ...guard,
215
+ '__v',
216
+ '_id',
217
+ 'deleted_at',
218
+ 'updated_at',
219
+ 'created_id',
220
+ 'modified_id',
221
+ 'resource',
222
+ 'is_processing',
223
+ 'priority',
224
+ ];
225
+ }
226
+ if (resType === RES_TYPE.DOCS) {
227
+ guardData.docs = _.map(data.docs, (item) => {
228
+ return omitItem(item, responseGuard, responseAllows);
229
+ });
230
+ } else if (resType === RES_TYPE.ARRAY) {
231
+ guardData = _.map(data, (item) => {
232
+ return omitItem(item, responseGuard, responseAllows);
233
+ });
234
+ } else if (resType === RES_TYPE.OBJECT) {
235
+ guardData = omitItem(data, responseGuard, responseAllows);
236
+ }
237
+ }
238
+
239
+ const resData = {
240
+ success: true,
241
+ data: guardData,
242
+ options: this.resOptions,
243
+ };
244
+
245
+ if (this.redis && this.cache) {
246
+ this.redis.set(this.cache.key, resData, this.cache.ttl);
247
+ }
248
+
249
+ return this.json(resData);
250
+ };
251
+
252
+ app.response.cleanCookie = function (data) {
253
+ _.map(data, (name) => {
254
+ this.clearCookie(name, {
255
+ domain: COOKIE_OPTION.domain,
256
+ });
257
+ });
258
+ };
259
+
260
+ app.response.addCookie = function (data) {
261
+ _.map(data, (value, key) => {
262
+ const maxAge = 86400000 * 365; // 1 year
263
+ this.cookie(key, value, {
264
+ ...COOKIE_OPTION,
265
+ maxAge,
266
+ expires: new Date(Date.now() + maxAge),
267
+ });
268
+ });
269
+ };
270
+
271
+ app.use(function (req, res, next) {
272
+ delete res.cache;
273
+
274
+ const origin = req.get('Origin');
275
+ req.rootOrigin = origin ? psl.parse(origin).domain : null;
276
+
277
+ req.trackDevice = {
278
+ ip: req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.ip,
279
+ device: req.get('X-DEVICE') || req.get('User-Agent'),
280
+ device_id: req.get('X-DEVICE-ID') || 'web',
281
+ os: req.get('X-OS') || 'web',
282
+ };
283
+
284
+ next();
285
+ });
286
+
287
+ _.map(this.middlewares, function (middleware) {
288
+ app.use(middleware);
289
+ });
290
+
291
+ const msg = [];
292
+ _.map(this.controllers, (data) => {
293
+ const {controller} = data;
294
+ const controllerName = data.name;
295
+ const controllerVersion = data.version;
296
+
297
+ const listPublicRouter = controller.publicRouter();
298
+
299
+ _.map(listPublicRouter, (method) => {
300
+ const {name, type} = method;
301
+
302
+ const router = controller[name]();
303
+ if (!router.disabled) {
304
+ router.path = `/${method.path}/${router.path}`;
305
+ router.path = controller.pathName ? `/${controller.pathName}${router.path}` : router.path;
306
+ router.path = controllerVersion.startsWith('v') ? `/${controllerVersion}${router.path}` : router.path;
307
+ router.path = path.normalize(router.path);
308
+
309
+ msg.push({
310
+ controller: controllerName,
311
+ version: controllerVersion,
312
+ type,
313
+ method: name,
314
+ path: router.path,
315
+ });
316
+ app[type](router.path, ...router.method);
317
+ }
318
+ });
319
+ });
320
+
321
+ console.table(msg);
322
+
323
+ app.all('/', async function (req, res) {
324
+ return res.success('welcome');
325
+ });
326
+ app.get('/track', async function (req, res) {
327
+ return res.success({
328
+ ...req.trackDevice,
329
+ host: req.hostname,
330
+ origin: req.get('origin'),
331
+ });
332
+ });
333
+
334
+ app.use(function (req, res) {
335
+ throw new ErrorException(NOT_FOUND);
336
+ });
337
+
338
+ app.use((err, req, res, next) => {
339
+ const error = errorCatch(err);
340
+
341
+ if (process.env.NODE_ENV === 'development') {
342
+ console.log(error.errors);
343
+ } else if (error.errors[0].code === UNKNOWN) {
344
+ console.error(req.originalUrl, err);
345
+ }
346
+
347
+ if (this.callbackError) {
348
+ this.callbackError(error, req.originalUrl);
349
+ }
350
+
351
+ return res.status(error.status).json({success: false, errors: error.errors});
352
+ });
353
+
354
+ const server = http.Server(app);
355
+
356
+ server.on('listening', () => {
357
+ this._alert('listening', `Server start at http://localhost:${server.address().port}`);
358
+ });
359
+
360
+ server.on('error', (error) => {
361
+ if (error.syscall !== 'listen') {
362
+ throw error;
363
+ }
364
+
365
+ const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
366
+
367
+ switch (error.code) {
368
+ case 'EACCES':
369
+ this._alert('EACCES', `${bind} requires elevated privileges`);
370
+ process.exit(1);
371
+ break;
372
+ case 'EADDRINUSE':
373
+ this._alert('EACCES', `${bind} is already in use`);
374
+ process.exit(1);
375
+ break;
376
+ default:
377
+ throw error;
378
+ }
379
+ });
380
+
381
+ server.listen(port);
382
+
383
+ return server;
384
+ }
385
+ throw new Error('No controllers in use API');
386
+ }
387
+
388
+ startSocket(port) {
389
+ if (!_.isEmpty(this.controllers)) {
390
+ return this;
391
+ }
392
+ throw new Error('No controllers in use SOCKET');
393
+ }
394
+
395
+ setAlertCallback(callback) {
396
+ this.alertCallback = callback;
397
+ return this;
398
+ }
399
+
400
+ _alert(status, msg) {
401
+ if (typeof this.alertCallback === 'function') {
402
+ this.alertCallback(status, msg);
403
+ } else {
404
+ console.log(status, msg);
405
+ }
406
+ }
407
+ }
408
+
409
+ export default Server;
@@ -0,0 +1,186 @@
1
+ import fs from 'fs';
2
+ import http from 'http';
3
+ import express from 'express';
4
+ import socketIO from 'socket.io';
5
+ import bodyParser from 'body-parser';
6
+ import cookieParser from 'cookie-parser';
7
+ import redisAdapter from 'socket.io-redis';
8
+ import _ from 'lodash';
9
+
10
+ const wrap = (middleware) => (socket, next) => middleware(socket.request, {}, next);
11
+
12
+ class SocketServer {
13
+ constructor(currentDir = '', options = {}) {
14
+ this.options = {
15
+ redisConfig: null,
16
+ ...options,
17
+ };
18
+
19
+ this.middlewares = [];
20
+ this.controllers = [];
21
+ this.whiteList = [];
22
+ this.debug = process.env.NODE_ENV === 'development';
23
+ this.initController(currentDir);
24
+ }
25
+
26
+ setCallbackError(callback = null) {
27
+ this.callbackError = callback;
28
+ return this;
29
+ }
30
+
31
+ setWhiteList(whiteList) {
32
+ this.whiteList = whiteList;
33
+ return this;
34
+ }
35
+
36
+ setDebug(debug) {
37
+ this.debug = debug;
38
+ return this;
39
+ }
40
+
41
+ addController(name, version, controller) {
42
+ this.controllers.push({
43
+ name,
44
+ version,
45
+ controller,
46
+ });
47
+ return this;
48
+ }
49
+
50
+ initController(apiDir) {
51
+ if (apiDir) {
52
+ const controllerDirs = fs.readdirSync(apiDir);
53
+
54
+ for (const dirName of controllerDirs) {
55
+ if (fs.statSync(`${apiDir}/${dirName}`).isDirectory()) {
56
+ const versionDirs = fs.readdirSync(`${apiDir}/${dirName}`);
57
+
58
+ for (const versionName of versionDirs) {
59
+ // eslint-disable-next-line import/no-dynamic-require,global-require
60
+ const controller = require(`${apiDir}/${dirName}/${versionName}/controller`).default;
61
+ this.addController(dirName, versionName, controller);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ return this;
67
+ }
68
+
69
+ addGlobalMiddleware(middleware) {
70
+ this.middlewares.push(middleware);
71
+ return this;
72
+ }
73
+
74
+ startPort(port) {
75
+ if (!_.isEmpty(this.controllers)) {
76
+ const WHITE_LIST = this.whiteList;
77
+
78
+ const server = http.Server(express());
79
+
80
+ const io = socketIO(server, {
81
+ wsEngine: 'eiows',
82
+ perMessageDeflate: {
83
+ threshold: 32768,
84
+ },
85
+ cors: {
86
+ credentials: true,
87
+ origin(origin, callback) {
88
+ if (!origin || !WHITE_LIST.length || WHITE_LIST.some((re) => origin.endsWith(re))) {
89
+ callback(null, true);
90
+ } else {
91
+ callback(new Error(`${origin} Not allowed by CORS`));
92
+ }
93
+ },
94
+ },
95
+ });
96
+
97
+ if (this.options.redisConfig) {
98
+ io.adapter(redisAdapter(this.options.redisConfig));
99
+ }
100
+
101
+ const msg = [];
102
+ _.map(this.controllers, (obj) => {
103
+ const {controller} = obj;
104
+
105
+ _.map(controller, (item, key) => {
106
+ item.path = obj.version.startsWith('v') ? `/${obj.version}${item.path}` : item.path;
107
+
108
+ const nsp = io.of(item.path);
109
+
110
+ const middlewares = [...this.middlewares, ...(item.middlewares || [])];
111
+
112
+ nsp.use(wrap(bodyParser.urlencoded({limit: '5mb', extended: true})));
113
+ nsp.use(wrap(bodyParser.json({limit: '5mb'})));
114
+ nsp.use(wrap(cookieParser(process.env.SECRET_KEY)));
115
+
116
+ _.map(middlewares, (middleware) => {
117
+ nsp.use(wrap(middleware));
118
+ });
119
+
120
+ if (item.schedule) {
121
+ item.schedule(io);
122
+ }
123
+
124
+ if (item.connection) {
125
+ nsp.on('connection', (socket) => {
126
+ item.connection(io, socket);
127
+ });
128
+ }
129
+
130
+ msg.push({
131
+ controller: obj.name,
132
+ version: obj.version,
133
+ path: item.path,
134
+ });
135
+ });
136
+ });
137
+
138
+ console.table(msg);
139
+
140
+ server.on('listening', () => {
141
+ this._alert('listening', `Server start at http://localhost:${server.address().port}`);
142
+ });
143
+
144
+ server.on('error', (error) => {
145
+ if (error.syscall !== 'listen') {
146
+ throw error;
147
+ }
148
+
149
+ let bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
150
+
151
+ switch (error.code) {
152
+ case 'EACCES':
153
+ this._alert('EACCES', `${bind} requires elevated privileges`);
154
+ process.exit(1);
155
+ break;
156
+ case 'EADDRINUSE':
157
+ this._alert('EACCES', `${bind} is already in use`);
158
+ process.exit(1);
159
+ break;
160
+ default:
161
+ throw error;
162
+ }
163
+ });
164
+
165
+ server.listen(port);
166
+
167
+ return server;
168
+ }
169
+ throw new Error('No controllers in use');
170
+ }
171
+
172
+ setAlertCallback(callback) {
173
+ this.alertCallback = callback;
174
+ return this;
175
+ }
176
+
177
+ _alert(status, msg) {
178
+ if (typeof this.alertCallback === 'function') {
179
+ this.alertCallback(status, msg);
180
+ } else {
181
+ console.log(status, msg);
182
+ }
183
+ }
184
+ }
185
+
186
+ export default SocketServer;
@@ -0,0 +1,12 @@
1
+ export const REQUEST_TYPE = {
2
+ PARAMS: 'params',
3
+ BODY: 'body',
4
+ QUERY: 'query',
5
+ };
6
+
7
+ export const USER_LEVEL = {
8
+ GUESS: 0,
9
+ USER: 1,
10
+ ADMIN: 50,
11
+ SYSTEM: 100,
12
+ };
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import Server from './Server';
2
+
3
+ import Controller from './Controller';
4
+ import AdminController from './AdminController';
5
+
6
+ export * from './middleware';
7
+ export * from './constant';
8
+ export * from './validate';
9
+
10
+ export {Controller, AdminController};
11
+
12
+ export default Server;
@@ -0,0 +1,7 @@
1
+ import {USER_LEVEL} from '../constant';
2
+
3
+ import roleMiddleware from './roleMiddleware';
4
+
5
+ export default function (roles = null) {
6
+ return roleMiddleware(roles, USER_LEVEL.ADMIN);
7
+ }
@@ -0,0 +1,53 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import {ErrorException, TOKEN_EXPIRED, TOKEN_FAILED} from '@azteam/error';
3
+
4
+ function systemLogin(userData = null) {
5
+ let user = {};
6
+ if (userData) {
7
+ try {
8
+ user = JSON.parse(userData);
9
+ } catch (err) {}
10
+ }
11
+ return user;
12
+ }
13
+
14
+ export default function (cbLoginAPI) {
15
+ return async function (req, res, next) {
16
+ const {headers} = req;
17
+
18
+ if (headers['x-app-secret'] === process.env.SECRET_KEY) {
19
+ req.user = systemLogin(headers['x-app-user']);
20
+ } else {
21
+ let token = null;
22
+
23
+ if (headers.authorization) {
24
+ token = headers.authorization;
25
+ }
26
+
27
+ if (token) {
28
+ if (token.startsWith('Bearer ')) {
29
+ token = token.replace('Bearer ', '');
30
+
31
+ try {
32
+ req.user = jwt.verify(token, process.env.SECRET_KEY);
33
+ return next();
34
+ } catch (err) {
35
+ if (err.name === 'TokenExpiredError') {
36
+ throw new ErrorException(TOKEN_EXPIRED, err);
37
+ }
38
+ throw new ErrorException(TOKEN_FAILED, err);
39
+ }
40
+ } else {
41
+ const data = await cbLoginAPI(token);
42
+ if (data) {
43
+ req.user = data;
44
+ } else {
45
+ return next(ErrorException(TOKEN_FAILED));
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ return next();
52
+ };
53
+ }