beech-api 3.9.0-beta.8-rc → 3.9.57
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.
Potentially problematic release.
This version of beech-api might be problematic. Click here for more details.
- package/README.md +78 -33
- package/package.json +6 -2
- package/packages/cli/bin/beech-app.js +1 -1
- package/packages/cli/bin/beech-service.js +3 -2
- package/packages/cli/core/auth/Credentials.js +80 -58
- package/packages/cli/core/databases/test.js +1 -1
- package/packages/cli/core/generator/_help +9 -1
- package/packages/cli/core/generator/_package +5 -6
- package/packages/cli/core/generator/_scheduler +16 -6
- package/packages/cli/core/generator/index.js +170 -8
- package/packages/cli/core/index.js +80 -7
- package/packages/cli/core/middleware/express/duplicateRequest.js +10 -6
- package/packages/cli/core/middleware/express/jwtCheckAllow.js +3 -3
- package/packages/cli/core/middleware/express/rateLimit.js +14 -2
- package/packages/cli/core/services/http.express.js +49 -7
- package/packages/cli/core/test/check-node.js +21 -0
- package/packages/lib/src/endpoint.js +344 -194
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
const logUpdate = require("log-update");
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const walk = require("walk");
|
|
5
|
+
const recast = require("recast");
|
|
6
|
+
const { builders } = require("ast-types");
|
|
5
7
|
const { connectForGenerateModel } = require("../databases/test");
|
|
6
8
|
|
|
7
9
|
class Generator {
|
|
@@ -179,6 +181,24 @@ class Generator {
|
|
|
179
181
|
resolve("\n[101m Faltal [0m commnad it's not available.");
|
|
180
182
|
}
|
|
181
183
|
}
|
|
184
|
+
} else if (this.option == 'update') {
|
|
185
|
+
if(this.argument == 'model') {
|
|
186
|
+
if(this.special) {
|
|
187
|
+
if (!this.special) {
|
|
188
|
+
resolve("\n[103m[90m Warning [0m[0m Please specify model name to update.");
|
|
189
|
+
} else {
|
|
190
|
+
this.updateModel()
|
|
191
|
+
.then(res => resolve(res))
|
|
192
|
+
.catch(err => reject(err));
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
resolve("\n[103m[90m Warning [0m[0m Please specify model name to update.");
|
|
196
|
+
}
|
|
197
|
+
} else if(this.argument != 'model') {
|
|
198
|
+
resolve("\n[101m Faltal [0m commnad it's not available.");
|
|
199
|
+
} else {
|
|
200
|
+
resolve("\n[103m[90m Warning [0m[0m Please specify what you want to update.");
|
|
201
|
+
}
|
|
182
202
|
} else if (this.option == 'passport') {
|
|
183
203
|
if (this.argument == "init") {
|
|
184
204
|
this.makePassportInit()
|
|
@@ -451,6 +471,150 @@ class Generator {
|
|
|
451
471
|
});
|
|
452
472
|
}
|
|
453
473
|
|
|
474
|
+
updateModel() {
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
try {
|
|
477
|
+
this.fs.readFile("./global.config.js", 'utf8', (err, globalData) => {
|
|
478
|
+
if (err) return resolve("\n[101m Faltal [0m Can't read `global.config.js` file.");
|
|
479
|
+
this.fs.readFile("./app.config.js", 'utf8', (appErr, appData) => {
|
|
480
|
+
if (appErr) return resolve("\n[101m Faltal [0m Can't read `app.config.js` file.");
|
|
481
|
+
const appConfig = eval(appData);
|
|
482
|
+
const modelName = this.special; // <table_name>
|
|
483
|
+
const modelFileName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
484
|
+
const modelPath = `./src/models/${modelFileName}.js`;
|
|
485
|
+
if (!this.fs.existsSync(modelPath)) {
|
|
486
|
+
return resolve(`\n[103m[90m Warning [0m[0m Model file \`${modelFileName}\` not found.`);
|
|
487
|
+
}
|
|
488
|
+
// Choose connection for new schema
|
|
489
|
+
inquirer.prompt([{
|
|
490
|
+
type: "list",
|
|
491
|
+
name: "selectDbConnect",
|
|
492
|
+
message: " [93mSelect database connection to sync schema: [0m",
|
|
493
|
+
choices: appConfig.database_config.map(e => e.name),
|
|
494
|
+
}]).then(dbSelected => {
|
|
495
|
+
const { connectForGenerateModel } = require("../databases/test");
|
|
496
|
+
// pull new schema from database
|
|
497
|
+
connectForGenerateModel(dbSelected.selectDbConnect, modelName, appConfig.database_config, (dbErr, tableSchema, tableName) => {
|
|
498
|
+
if (dbErr) return reject(dbErr);
|
|
499
|
+
// read current model file for AST
|
|
500
|
+
this.fs.readFile(modelPath, 'utf8', (readErr, code) => {
|
|
501
|
+
if (readErr) return reject(readErr);
|
|
502
|
+
const ast = recast.parse(code);
|
|
503
|
+
let isUpdated = false;
|
|
504
|
+
// Function for map raw database type to Sequelize DataTypes AST Node
|
|
505
|
+
const getDataTypeNode = (rawType) => {
|
|
506
|
+
const type = rawType.toUpperCase();
|
|
507
|
+
if (type.includes('INT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('INTEGER'));
|
|
508
|
+
if (type.includes('BIGINT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BIGINT'));
|
|
509
|
+
if (type.includes('FLOAT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('FLOAT'));
|
|
510
|
+
if (type.includes('DOUBLE')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DOUBLE'));
|
|
511
|
+
if (type.includes('DECIMAL')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DECIMAL'));
|
|
512
|
+
if (type.includes('BOOLEAN') || type === 'TINYINT(1)') return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BOOLEAN'));
|
|
513
|
+
if (type.includes('CHAR') || type.includes('VARCHAR')) {
|
|
514
|
+
const match = type.match(/\((\d+)\)/);
|
|
515
|
+
const length = match ? match[1] : '255';
|
|
516
|
+
return builders.callExpression(
|
|
517
|
+
builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('STRING')),
|
|
518
|
+
[builders.literal(parseInt(length))]
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (type.includes('TEXT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('TEXT'));
|
|
522
|
+
if (type.includes('DATE')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DATE'));
|
|
523
|
+
if (type.includes('TIME')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('TIME'));
|
|
524
|
+
if (type.includes('JSON')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('JSON'));
|
|
525
|
+
if (type.includes('UUID')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('UUID'));
|
|
526
|
+
if (type.includes('BLOB')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BLOB'));
|
|
527
|
+
if (type.includes('ENUM')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('ENUM'));
|
|
528
|
+
if (type.includes('GEOMETRY')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('GEOMETRY'));
|
|
529
|
+
return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('STRING'));
|
|
530
|
+
};
|
|
531
|
+
// recast.visit for find position of Schema(...).define() and update fields
|
|
532
|
+
recast.visit(ast, {
|
|
533
|
+
visitCallExpression(path) {
|
|
534
|
+
const node = path.node;
|
|
535
|
+
if (node.callee.property && node.callee.property.name === 'define' && node.arguments[1] && node.arguments[1].type === 'ObjectExpression') {
|
|
536
|
+
const fieldsObject = node.arguments[1];
|
|
537
|
+
const newFieldNames = Object.keys(tableSchema);
|
|
538
|
+
const updatedProperties = [];
|
|
539
|
+
newFieldNames.forEach(dbFieldName => {
|
|
540
|
+
const dbFieldData = tableSchema[dbFieldName];
|
|
541
|
+
let existingProperty = fieldsObject.properties.find(p => {
|
|
542
|
+
const propName = p.key.name || p.key.value;
|
|
543
|
+
if (propName === dbFieldName) return true;
|
|
544
|
+
if (p.value && p.value.properties) {
|
|
545
|
+
const hasFieldMapping = p.value.properties.find(
|
|
546
|
+
innerP => (innerP.key.name === 'field' || innerP.key.value === 'field') &&
|
|
547
|
+
(innerP.value.value === dbFieldName)
|
|
548
|
+
);
|
|
549
|
+
if (hasFieldMapping) return true;
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
});
|
|
553
|
+
const latestFieldProps = [];
|
|
554
|
+
// Prepare properties from Table DB (Type, PK, AI, Default, etc.)
|
|
555
|
+
latestFieldProps.push(builders.property('init', builders.identifier('type'), getDataTypeNode(dbFieldData.type)));
|
|
556
|
+
latestFieldProps.push(builders.property('init', builders.identifier('allowNull'), builders.literal(dbFieldData.allowNull === 'YES' || dbFieldData.allowNull === true)));
|
|
557
|
+
if (dbFieldData.primaryKey) latestFieldProps.push(builders.property('init', builders.identifier('primaryKey'), builders.literal(true)));
|
|
558
|
+
if (dbFieldData.autoIncrement) latestFieldProps.push(builders.property('init', builders.identifier('autoIncrement'), builders.literal(true)));
|
|
559
|
+
if (dbFieldData.unique) latestFieldProps.push(builders.property('init', builders.identifier('unique'), builders.literal(true)));
|
|
560
|
+
if(this.extra != '--no-comment') {
|
|
561
|
+
if (dbFieldData.comment) latestFieldProps.push(builders.property('init', builders.identifier('comment'), builders.literal(dbFieldData.comment)));
|
|
562
|
+
}
|
|
563
|
+
if (dbFieldData.defaultValue !== null && dbFieldData.defaultValue !== undefined) {
|
|
564
|
+
const dVal = String(dbFieldData.defaultValue).toUpperCase();
|
|
565
|
+
const defaultNode = (dVal === 'CURRENT_TIMESTAMP' || dVal === 'NOW()')
|
|
566
|
+
? builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('NOW'))
|
|
567
|
+
: builders.literal(dbFieldData.defaultValue);
|
|
568
|
+
latestFieldProps.push(builders.property('init', builders.identifier('defaultValue'), defaultNode));
|
|
569
|
+
}
|
|
570
|
+
if (!existingProperty) {
|
|
571
|
+
existingProperty = builders.property('init', builders.identifier(dbFieldName), builders.objectExpression(latestFieldProps));
|
|
572
|
+
isUpdated = true;
|
|
573
|
+
} else {
|
|
574
|
+
const currentProps = existingProperty.value.properties;
|
|
575
|
+
latestFieldProps.forEach(newProp => {
|
|
576
|
+
const oldProp = currentProps.find(p => p.key.name === newProp.key.name);
|
|
577
|
+
if (!oldProp || recast.print(oldProp.value).code !== recast.print(newProp.value).code) {
|
|
578
|
+
isUpdated = true;
|
|
579
|
+
if (!oldProp) currentProps.push(newProp); else oldProp.value = newProp.value;
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
const latestPropNames = latestFieldProps.map(p => p.key.name);
|
|
583
|
+
latestPropNames.push('field');
|
|
584
|
+
existingProperty.value.properties = currentProps.filter(p => latestPropNames.includes(p.key.name));
|
|
585
|
+
}
|
|
586
|
+
updatedProperties.push(existingProperty);
|
|
587
|
+
});
|
|
588
|
+
const currentOrder = fieldsObject.properties.map(p => p.key.name || p.key.value).join(',');
|
|
589
|
+
const newOrder = updatedProperties.map(p => p.key.name || p.key.value).join(',');
|
|
590
|
+
if (currentOrder !== newOrder || fieldsObject.properties.length !== updatedProperties.length) isUpdated = true;
|
|
591
|
+
fieldsObject.properties = updatedProperties;
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
// Write back updated AST, Shout updated
|
|
597
|
+
if (isUpdated) {
|
|
598
|
+
// Write updated AST back to file
|
|
599
|
+
const output = recast.print(ast).code;
|
|
600
|
+
this.fs.writeFile(modelPath, output, 'utf8', (writeErr) => {
|
|
601
|
+
if (writeErr) return reject(writeErr);
|
|
602
|
+
resolve(`\n[102m[90m Updated [0m[0m The model \`${modelFileName}\` has been updated via AST.`);
|
|
603
|
+
});
|
|
604
|
+
} else {
|
|
605
|
+
resolve(`\n[103m[90m Warning [0m[0m No new fields found to update for \`${modelFileName}\`.`);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
} catch (error) {
|
|
613
|
+
reject(error);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
454
618
|
generateModel(tmpModelsPath, dbSelected, appBuf2eval, pool_base) {
|
|
455
619
|
return new Promise((resolve, reject) => {
|
|
456
620
|
try {
|
|
@@ -611,21 +775,19 @@ class Generator {
|
|
|
611
775
|
// Check is primary key
|
|
612
776
|
if (props.primaryKey) lines.push(` primaryKey: true,`);
|
|
613
777
|
if (props.autoIncrement) lines.push(` autoIncrement: true,`);
|
|
614
|
-
//
|
|
615
|
-
|
|
778
|
+
// Check <extra> for comment, because some database have extra comment in field but it's not comment for field, so we need to check it first before assign to comment
|
|
779
|
+
if(this.extra != '--no-comment') {
|
|
780
|
+
if (props.comment) lines.push(` comment: "${props.comment}",`);
|
|
781
|
+
}
|
|
616
782
|
// Handle defaultValue
|
|
617
783
|
if (props.defaultValue !== null && props.defaultValue !== undefined) {
|
|
618
784
|
const defaultVal = String(props.defaultValue).toUpperCase();
|
|
619
|
-
if (
|
|
620
|
-
defaultVal === 'CURRENT_TIMESTAMP' ||
|
|
621
|
-
defaultVal === 'NOW()' ||
|
|
622
|
-
defaultVal.includes('CURRENT_TIMESTAMP')
|
|
623
|
-
) {
|
|
785
|
+
if (defaultVal === 'CURRENT_TIMESTAMP' || defaultVal === 'NOW()' || defaultVal.includes('CURRENT_TIMESTAMP')) {
|
|
624
786
|
lines.push(` defaultValue: DataTypes.NOW,`);
|
|
625
787
|
} else if (defaultVal === 'UUID()' || defaultVal === 'uuid()' || defaultVal === 'UUID') {
|
|
626
788
|
lines.push(` defaultValue: DataTypes.UUIDV4,`);
|
|
627
789
|
} else if (typeof props.defaultValue === 'string') {
|
|
628
|
-
lines.push(` defaultValue:
|
|
790
|
+
lines.push(` defaultValue: "${props.defaultValue}",`);
|
|
629
791
|
} else {
|
|
630
792
|
lines.push(` defaultValue: ${props.defaultValue},`);
|
|
631
793
|
}
|
|
@@ -15,7 +15,6 @@ _app_.use(helmet());
|
|
|
15
15
|
const cors = require("cors");
|
|
16
16
|
global.endpoint = _express_.Router();
|
|
17
17
|
const cookieParser = require("cookie-parser");
|
|
18
|
-
const bodyParser = require("body-parser");
|
|
19
18
|
const methodOverride = require("method-override");
|
|
20
19
|
const expressSession = require("express-session");
|
|
21
20
|
const expressValidator = require("express-validator");
|
|
@@ -27,10 +26,86 @@ const _beech_ = require(appRoot + "/beech.config.js").defineConfig;
|
|
|
27
26
|
global._publicPath_ = _beech_.base;
|
|
28
27
|
const mySqlDbConnect = require("./databases/mysql");
|
|
29
28
|
const SequelizeDbConnect = require("./databases/sequelize");
|
|
30
|
-
//
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// Set limit payload for request body & multipart/form-data (multer)
|
|
30
|
+
const multer = require("multer");
|
|
31
|
+
const uploadAllowMethod = _beech_?.payload?.file?.uploadAllowMethod || ["POST", "PATCH", "PUT"];
|
|
32
|
+
const allowedTypes = _beech_?.payload?.file?.allowedTypes || []; // default: no allowed type
|
|
33
|
+
const fileLimitSize = _beech_?.payload?.file?.limit || 5 * 1024 * 1024; // 5MB
|
|
34
|
+
const uploadStrategy = multer({
|
|
35
|
+
limits: { fileSize: fileLimitSize },
|
|
36
|
+
fileFilter: (req, file, cb) => {
|
|
37
|
+
if (allowedTypes.length === 0) {
|
|
38
|
+
cb(new Error("INVALID_FILE_TYPE_ALLOW"), false); // Allow all file types if no allowed types specified
|
|
39
|
+
} else if (allowedTypes.includes(file.mimetype)) {
|
|
40
|
+
cb(null, true);
|
|
41
|
+
} else {
|
|
42
|
+
cb(new Error("INVALID_FILE_TYPE"), false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}).any();
|
|
46
|
+
const jsonLimitSize = _beech_?.payload?.json?.limit || "100KB"; // json payload
|
|
47
|
+
const urlencodedLimitSize = _beech_?.payload?.urlencoded?.limit || "100KB"; // urlencoded payload (multipart/form-data)
|
|
48
|
+
const urlencodedExtended = !!(_beech_?.payload?.urlencoded?.extended ?? true);
|
|
49
|
+
_app_.use(_express_.urlencoded({ limit: urlencodedLimitSize, extended: urlencodedExtended })); // application/x-www-form-urlencoded payload
|
|
50
|
+
_app_.use(_express_.json({ limit: jsonLimitSize }));
|
|
51
|
+
_app_.use((req, res, next) => {
|
|
52
|
+
const isUploadMethod = uploadAllowMethod.includes(req.method);
|
|
53
|
+
const isMultipart = req.headers["content-type"]?.includes("multipart/form-data");
|
|
54
|
+
// Handle file upload for allowed methods
|
|
55
|
+
if (isUploadMethod) {
|
|
56
|
+
return uploadStrategy(req, res, (err) => {
|
|
57
|
+
if (err) return next(err);
|
|
58
|
+
next();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Handle method not allowed for file upload
|
|
62
|
+
if (!isUploadMethod && isMultipart) {
|
|
63
|
+
return res.status(405).json({
|
|
64
|
+
code: 405,
|
|
65
|
+
status: "METHOD_NOT_ALLOWED_FOR_UPLOAD",
|
|
66
|
+
message: _config_.main_config?.dev ? `File upload is not allowed for ${req.method} method.` : "Method Not Allowed for file upload.",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// next to next middleware
|
|
70
|
+
next();
|
|
71
|
+
});
|
|
72
|
+
_app_.use((err, req, res, next) => {
|
|
73
|
+
// Handle payload too large error for JSON and URL-encoded
|
|
74
|
+
if (err.type === "entity.too.large") {
|
|
75
|
+
const isJson = req.headers["content-type"]?.includes("application/json");
|
|
76
|
+
const limitUsed = isJson ? jsonLimitSize : urlencodedLimitSize;
|
|
77
|
+
return res.status(413).json({
|
|
78
|
+
code: 413,
|
|
79
|
+
status: "PAYLOAD_TOO_LARGE",
|
|
80
|
+
message:_config_.main_config?.dev ? `${isJson ? 'JSON' : 'Form data'} too large, Max limit is ${limitUsed}` : "Payload Too Large.",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (err.message === "INVALID_FILE_TYPE_ALLOW") {
|
|
84
|
+
return res.status(400).json({
|
|
85
|
+
code: 400,
|
|
86
|
+
status: "INVALID_FILE_TYPE_ALLOW",
|
|
87
|
+
message: "Invalid file type, No file types allowed.",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Handle invalid file type error from multer
|
|
91
|
+
if (err.message === "INVALID_FILE_TYPE") {
|
|
92
|
+
return res.status(400).json({
|
|
93
|
+
code: 400,
|
|
94
|
+
status: "INVALID_FILE_TYPE",
|
|
95
|
+
message: _config_.main_config?.dev ? `Invalid file type, Allowed: ${allowedTypes.join(', ')}` : "Invalid file type.",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Handle multer file size limit error
|
|
99
|
+
if (err.code === "LIMIT_FILE_SIZE") {
|
|
100
|
+
return res.status(413).json({
|
|
101
|
+
code: 413,
|
|
102
|
+
status: "PAYLOAD_TOO_LARGE",
|
|
103
|
+
message: _config_.main_config?.dev ? `File size too large, Max limit is ${fileLimitSize / 1024 / 1024}MB` : "Payload Too Large.",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// next to general error handler
|
|
107
|
+
next(err);
|
|
108
|
+
});
|
|
34
109
|
// Database test
|
|
35
110
|
const {
|
|
36
111
|
testConnectInProcess,
|
|
@@ -57,8 +132,6 @@ _app_.use((req, res, next) => {
|
|
|
57
132
|
});
|
|
58
133
|
});
|
|
59
134
|
// View engine
|
|
60
|
-
_app_.use(bodyParser.json());
|
|
61
|
-
_app_.use(bodyParser.urlencoded({ extended: true }));
|
|
62
135
|
_app_.use(methodOverride());
|
|
63
136
|
_app_.use(cookieParser());
|
|
64
137
|
_app_.use(expressSession({
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
const _beech_ = require(appRoot + "/beech.config.js");
|
|
2
2
|
const { duplicateRequest } = require("express-duplicate-request");
|
|
3
|
-
const nextDuplicater = (req, res, next) =>
|
|
4
|
-
|
|
5
|
-
};
|
|
6
|
-
let configure = {
|
|
3
|
+
const nextDuplicater = (req, res, next) => next();
|
|
4
|
+
const defaultConfigure = {
|
|
7
5
|
expiration: _beech_.defineConfig.server.duplicateRequest ? _beech_.defineConfig.server.duplicateRequest.expiration : 0,
|
|
8
6
|
};
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const baseConfigure = {
|
|
8
|
+
..._beech_.defineConfig.server.duplicateRequest, // Override default configure with user configure.
|
|
9
|
+
...defaultConfigure,
|
|
10
|
+
};
|
|
11
|
+
const Duplicater = (more_configure = {}) => {
|
|
12
|
+
const config = { ...baseConfigure, ...more_configure };
|
|
13
|
+
return config.expiration ? duplicateRequest(config) : nextDuplicater;
|
|
14
|
+
};
|
|
11
15
|
|
|
12
16
|
module.exports = { Duplicater, duplicateRequest };
|
|
@@ -26,9 +26,10 @@ const checkRoleMiddleware = (options) => {
|
|
|
26
26
|
return next();
|
|
27
27
|
} else {
|
|
28
28
|
const allowed = options.some(rule => {
|
|
29
|
-
Object.entries(rule).every(([key, condition]) => {
|
|
29
|
+
return Object.entries(rule).every(([key, condition]) => {
|
|
30
|
+
//console.log('----matchCondition(user?.[key], condition, user)-->>', matchCondition(user?.[key], condition, user));
|
|
30
31
|
return matchCondition(user?.[key], condition, user);
|
|
31
|
-
})
|
|
32
|
+
});
|
|
32
33
|
});
|
|
33
34
|
if (allowed) {
|
|
34
35
|
return next();
|
|
@@ -63,7 +64,6 @@ const operators = {
|
|
|
63
64
|
};
|
|
64
65
|
|
|
65
66
|
const matchCondition = (userValue, condition, user) => {
|
|
66
|
-
// primitive -> eq
|
|
67
67
|
if (typeof condition !== 'object' || condition instanceof RegExp || Array.isArray(condition)) {
|
|
68
68
|
return Array.isArray(condition)
|
|
69
69
|
? operators.$in(userValue, condition)
|
|
@@ -11,7 +11,19 @@ let configure = {
|
|
|
11
11
|
legacyHeaders: (_beech_.defineConfig.server.rateLimit) ? (_beech_.defineConfig.server.rateLimit.legacyHeaders || false) : false,
|
|
12
12
|
message: (_beech_.defineConfig.server.rateLimit) ? (_beech_.defineConfig.server.rateLimit.message || tooManyMsg) : tooManyMsg,
|
|
13
13
|
};
|
|
14
|
-
configure = {
|
|
15
|
-
|
|
14
|
+
configure = {
|
|
15
|
+
..._beech_.defineConfig.server.rateLimit, // Override default configure with user configure.
|
|
16
|
+
...configure,
|
|
17
|
+
keyGenerator: (req) => `${req.ip}:${req.params.hash}${req.params['0']}`,
|
|
18
|
+
};
|
|
19
|
+
const Limiter = (more_configure = {}) => {
|
|
20
|
+
const middleware = rateLimit({
|
|
21
|
+
...configure,
|
|
22
|
+
...more_configure,
|
|
23
|
+
});
|
|
24
|
+
return (req, res, next) => {
|
|
25
|
+
middleware(req, res, next);
|
|
26
|
+
};
|
|
27
|
+
};
|
|
16
28
|
|
|
17
29
|
module.exports = { Limiter, rateLimit };
|
|
@@ -3,6 +3,7 @@ const fs = require("fs");
|
|
|
3
3
|
const passport_config_file = "/passport.config.js";
|
|
4
4
|
const auth = require("../auth/Credentials");
|
|
5
5
|
const { TwoFactor } = require("../helpers/2fa");
|
|
6
|
+
const { Limiter, Duplicater } = require("../middleware");
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
expressStart() {
|
|
@@ -142,7 +143,7 @@ module.exports = {
|
|
|
142
143
|
// declare authentication endpoint name with publicPath
|
|
143
144
|
let auth_endpoint = (passport_config.auth_endpoint) ? (passport_config.auth_endpoint[ 0 ] === "/" ? passport_config.auth_endpoint : "/" + passport_config.auth_endpoint) : "/authentication";
|
|
144
145
|
// authentication endpoints
|
|
145
|
-
endpoint.post(auth_endpoint, auth.credentialsGuard, (req, res, next) => {
|
|
146
|
+
endpoint.post(auth_endpoint, Duplicater(), Limiter(), auth.credentialsGuard, (req, res, next) => {
|
|
146
147
|
passport.authenticate('local', { session: false }, (err, user, opt) => {
|
|
147
148
|
if (err) {
|
|
148
149
|
res.status(502).json({ code: 502, status: "BAD_GATEWAY", message: String(err) });
|
|
@@ -194,8 +195,49 @@ module.exports = {
|
|
|
194
195
|
}
|
|
195
196
|
})(req, res, next);
|
|
196
197
|
});
|
|
198
|
+
// refresh token endpoints
|
|
199
|
+
endpoint.post(auth_endpoint + '/refresh', Duplicater(), Limiter(), auth.credentials, (req, res) => {
|
|
200
|
+
const refreshToken = req.headers.authorization.split("Bearer ")[1] || null;
|
|
201
|
+
if (!refreshToken) {
|
|
202
|
+
return res.status(400).json({
|
|
203
|
+
code: 400,
|
|
204
|
+
status: "BAD_REQUEST",
|
|
205
|
+
message: "Bad request.",
|
|
206
|
+
info: {
|
|
207
|
+
status: "BAD_ENTITY",
|
|
208
|
+
message: "Refresh token is required.",
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
jwt.verify(refreshToken, passport_config.secret, (err, user) => {
|
|
213
|
+
if (err) {
|
|
214
|
+
return res.status(401).json({
|
|
215
|
+
code: 401,
|
|
216
|
+
status: "UNAUTHORIZED",
|
|
217
|
+
message: "Unauthorized user.",
|
|
218
|
+
info: {
|
|
219
|
+
status: "TOKEN_INVALID_ERR",
|
|
220
|
+
message: "Invalid refresh token.",
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
delete user.iat;
|
|
225
|
+
delete user.exp;
|
|
226
|
+
const newAccessToken = jwt.sign(user, passport_config.secret, {
|
|
227
|
+
expiresIn: passport_config.token_expired
|
|
228
|
+
});
|
|
229
|
+
res.status(200).json({
|
|
230
|
+
code: 200,
|
|
231
|
+
status: "TOKEN_REFRESHED",
|
|
232
|
+
user: user,
|
|
233
|
+
accessToken: newAccessToken,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
197
239
|
// create auth data endpoints
|
|
198
|
-
endpoint.post(auth_endpoint + '/create', auth.credentialsGuard, (req, res) => {
|
|
240
|
+
endpoint.post(auth_endpoint + '/create', Duplicater(), Limiter(), auth.credentialsGuard, (req, res) => {
|
|
199
241
|
const promise = new Promise((resolve) => {
|
|
200
242
|
/**
|
|
201
243
|
*
|
|
@@ -249,7 +291,7 @@ module.exports = {
|
|
|
249
291
|
});
|
|
250
292
|
});
|
|
251
293
|
// patch auth data endpoints
|
|
252
|
-
endpoint.patch(auth_endpoint + '/update/:id', auth.credentials, (req, res) => {
|
|
294
|
+
endpoint.patch(auth_endpoint + '/update/:id', Duplicater(), Limiter(), auth.credentials, (req, res) => {
|
|
253
295
|
const promise = new Promise((resolve) => {
|
|
254
296
|
if (passport_config.app_key_allow) {
|
|
255
297
|
if (req.headers.app_key) {
|
|
@@ -303,7 +345,7 @@ module.exports = {
|
|
|
303
345
|
*
|
|
304
346
|
*/
|
|
305
347
|
if (passport_config.strategy.google.allow) {
|
|
306
|
-
endpoint.get(auth_endpoint + '/google', passport.authenticate('google', {
|
|
348
|
+
endpoint.get(auth_endpoint + '/google', Limiter(), passport.authenticate('google', {
|
|
307
349
|
scope: [
|
|
308
350
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
309
351
|
'https://www.googleapis.com/auth/plus.login'
|
|
@@ -311,7 +353,7 @@ module.exports = {
|
|
|
311
353
|
}));
|
|
312
354
|
// google auth callback
|
|
313
355
|
const googleCallback = (passport_config.strategy.google.callbackURL) ? (passport_config.strategy.google.callbackURL[ 0 ] === "/" ? passport_config.strategy.google.callbackURL : "/" + passport_config.strategy.google.callbackURL) : "/google/callback";
|
|
314
|
-
endpoint.get(auth_endpoint + googleCallback, passport.authenticate('google', { failureRedirect: passport_config.strategy.google.failureRedirect, failureMessage: true }), (req, res) => {
|
|
356
|
+
endpoint.get(auth_endpoint + googleCallback, Limiter(), passport.authenticate('google', { failureRedirect: passport_config.strategy.google.failureRedirect, failureMessage: true }), (req, res) => {
|
|
315
357
|
if (typeof req.user.user !== 'undefined') {
|
|
316
358
|
// declare user for sign JWT
|
|
317
359
|
let user = JSON.parse(JSON.stringify(req.user.user));
|
|
@@ -360,10 +402,10 @@ module.exports = {
|
|
|
360
402
|
*
|
|
361
403
|
*/
|
|
362
404
|
if (passport_config.strategy.facebook.allow) {
|
|
363
|
-
endpoint.get(auth_endpoint + '/facebook', passport.authenticate('facebook', { scope: [ 'email', 'public_profile' ] }));
|
|
405
|
+
endpoint.get(auth_endpoint + '/facebook', Limiter(), passport.authenticate('facebook', { scope: [ 'email', 'public_profile' ] }));
|
|
364
406
|
// facebook callback
|
|
365
407
|
const facebookCallback = (passport_config.strategy.facebook.callbackURL) ? (passport_config.strategy.facebook.callbackURL[ 0 ] === "/" ? passport_config.strategy.facebook.callbackURL : "/" + passport_config.strategy.facebook.callbackURL) : "/facebook/callback";
|
|
366
|
-
endpoint.get(auth_endpoint + facebookCallback, passport.authenticate('facebook', { failureRedirect: passport_config.strategy.facebook.failureRedirect, failureMessage: true }), (req, res) => {
|
|
408
|
+
endpoint.get(auth_endpoint + facebookCallback, Limiter(), passport.authenticate('facebook', { failureRedirect: passport_config.strategy.facebook.failureRedirect, failureMessage: true }), (req, res) => {
|
|
367
409
|
if (typeof req.user.user !== 'undefined') {
|
|
368
410
|
// declare user for sign JWT
|
|
369
411
|
let user = JSON.parse(JSON.stringify(req.user.user));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const current = process.versions.node;
|
|
2
|
+
|
|
3
|
+
const required = {
|
|
4
|
+
min16: "16.20.0",
|
|
5
|
+
min22: "22.18.0",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const semver = (a, b) => {
|
|
9
|
+
const pa = a.split(".").map(Number);
|
|
10
|
+
const pb = b.split(".").map(Number);
|
|
11
|
+
for (let i = 0; i < 3; i++) {
|
|
12
|
+
if (pa[i] > pb[i]) return 1;
|
|
13
|
+
if (pa[i] < pb[i]) return -1;
|
|
14
|
+
}
|
|
15
|
+
return 0;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (semver(current, required.min16) < 0 && semver(current, required.min22) < 0) {
|
|
19
|
+
console.error(`\n❌ You are using Node.js ${current}.\n` + `Beech requires Node.js version ${required.min16}+ or ${required.min22}+.\n` + `Please upgrade your Node.js version.\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|