beech-api 3.8.0 → 3.9.0-beta.9-rc

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.
@@ -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
- // Rate Request middleware
31
- const { Limiter, Duplicater } = require("./middleware/index");
32
- endpoint.use(Limiter);
33
- endpoint.use(Duplicater);
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({
@@ -164,9 +237,11 @@ init = async (jsfiles) => {
164
237
  Promise.all([testConnectToDB]).then(async (x) => {
165
238
  if (x[0]) {
166
239
  await (pool_base == "basic" ? new Promise((resolve) => resolve(mySqlDbConnect.connect())) : new Promise((resolve) => resolve(SequelizeDbConnect.connect())));
167
- await authPassport.init().then(async (x) => {
168
- if (x[0]) {
169
- throw x[0];
240
+ await authPassport.init().then(async (p) => {
241
+ if (p[0]) {
242
+ console.log("\n Init failed ", p[0]);
243
+ return;
244
+ //throw p[0];
170
245
  } else {
171
246
  await new Promise((resolve) => resolve(fileWalk.fileWalk(jsfiles)));
172
247
  await (pool_base == "basic" ? new Promise((resolve) => resolve()) : new Promise((resolve) => resolve(Base())));
@@ -176,6 +251,9 @@ init = async (jsfiles) => {
176
251
  });
177
252
  });
178
253
  }
254
+ }).catch((err) => {
255
+ console.log(" Catch init failed ", err);
256
+ throw err;
179
257
  });
180
258
  }
181
259
  });
@@ -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
- next();
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
- configure = { ...configure, ..._beech_.defineConfig.server.duplicateRequest };
10
- const Duplicater = configure.expiration ? duplicateRequest(configure) : nextDuplicater;
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 };
@@ -1,8 +1,9 @@
1
1
  const passport = require("passport");
2
2
 
3
3
  const checkRoleMiddleware = (options) => {
4
- return function (req, res, next) {
4
+ return (req, res, next) => {
5
5
  if(!Array.isArray(options)) {
6
+ // Perfectly with options is not type Array
6
7
  return next();
7
8
  } else {
8
9
  passport.authenticate("jwt", {
@@ -10,7 +11,7 @@ const checkRoleMiddleware = (options) => {
10
11
  }, (err, user, info) => {
11
12
  // error check
12
13
  if (err) {
13
- console.log(err, info);
14
+ //console.log(err, info);
14
15
  return res.status(403).json({
15
16
  code: 403,
16
17
  status: "FORBIDDEN",
@@ -20,49 +21,66 @@ const checkRoleMiddleware = (options) => {
20
21
  },
21
22
  });
22
23
  } else {
23
- let isPerfectly = true;
24
- options.forEach((element, key) => {
25
- let checkVal = Object.values(element).flat();
26
- let checkKey = Object.keys(element);
27
- if(user && user[checkKey]) {
28
- if(checkVal.includes(user[checkKey])) {
29
- if(options.length -1 === key) {
30
- if(isPerfectly) {
31
- // Perfectly
32
- return next();
33
- }
34
- }
35
- } else {
36
- if(isPerfectly) {
37
- res.status(403).json({
38
- code: 403,
39
- status: "FORBIDDEN",
40
- message: "Forbidden: Insufficient role",
41
- });
42
- }
43
- isPerfectly = false;
44
- }
24
+ if(!options.length) {
25
+ // Perfectly with no options
26
+ return next();
27
+ } else {
28
+ const allowed = options.some(rule => {
29
+ return Object.entries(rule).every(([key, condition]) => {
30
+ //console.log('----matchCondition(user?.[key], condition, user)-->>', matchCondition(user?.[key], condition, user));
31
+ return matchCondition(user?.[key], condition, user);
32
+ });
33
+ });
34
+ if (allowed) {
35
+ return next();
45
36
  } else {
46
- if(isPerfectly) {
47
- res.status(403).json({
48
- code: 403,
49
- status: "FORBIDDEN",
50
- message: `No ${checkKey} assigned to token.`,
51
- });
52
- }
53
- isPerfectly = false;
37
+ res.status(403).json({
38
+ code: 403,
39
+ status: "FORBIDDEN",
40
+ message: "Forbidden: Insufficient role",
41
+ info: {
42
+ status: "ROLE_NOT_ALLOWED",
43
+ error: "Insufficient role or User token does not have sufficient role.",
44
+ },
45
+ });
54
46
  }
55
- });
47
+ }
56
48
  }
57
49
  })(req, res, next);
58
50
  }
59
51
  }
60
52
  }
61
53
 
54
+ const operators = {
55
+ $eq: (userValue, expected) => userValue === expected,
56
+ $ne: (userValue, expected) => userValue !== expected,
57
+ $in: (userValue, expected) => Array.isArray(expected) && expected.includes(userValue),
58
+ $not: (userValue, expected) => Array.isArray(expected) && !expected.includes(userValue),
59
+ $regex: (userValue, expected) =>
60
+ typeof userValue === 'string' && expected instanceof RegExp
61
+ ? expected.test(userValue)
62
+ : false,
63
+ $fn: (userValue, fn, user) => typeof fn === 'function' && fn(userValue, user),
64
+ };
65
+
66
+ const matchCondition = (userValue, condition, user) => {
67
+ if (typeof condition !== 'object' || condition instanceof RegExp || Array.isArray(condition)) {
68
+ return Array.isArray(condition)
69
+ ? operators.$in(userValue, condition)
70
+ : operators.$eq(userValue, condition);
71
+ }
72
+ // operator object
73
+ return Object.entries(condition).every(([op, expected]) => {
74
+ const handler = operators[op];
75
+ if (!handler) return false;
76
+ return handler(userValue, expected, user);
77
+ });
78
+ };
79
+
62
80
  const checkRoleMiddlewareWithDefaultProject = (options) => {
63
81
  return function (req, res, next) {
64
82
  return checkRoleMiddleware(options)(req, res, next);
65
83
  }
66
84
  }
67
85
 
68
- module.exports = { checkRoleMiddleware, checkRoleMiddlewareWithDefaultProject }
86
+ module.exports = { checkRoleMiddleware, checkRoleMiddlewareWithDefaultProject };
@@ -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 = { ...configure, ..._beech_.defineConfig.server.rateLimit };
15
- const Limiter = rateLimit(configure);
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 };
@@ -14,12 +14,13 @@ function avg(req, res, next) {
14
14
  });
15
15
  // promise all
16
16
  Promise.all([checkPassport]).then((final) => {
17
- let item = final[0];
18
- let passport_config = item[1];
19
17
  /**
20
18
  * item[0] : Boolean = passport file found.
21
- * item[1] : Object = passport object.
19
+ * item[1] : Object = passport object. (for check on/off)
22
20
  */
21
+ let item = final[0];
22
+ let passport_config = item[1];
23
+ // check passport file exists ?, when not exists go to next
23
24
  if(item[0]) {
24
25
  if ((passport_config.model.guard.advanced_guard) ? passport_config.model.guard.advanced_guard.allow : false) {
25
26
  let advanced_guard_entity = req.headers[passport_config.model.guard.advanced_guard.entity || "timing"];
@@ -47,7 +48,7 @@ function avg(req, res, next) {
47
48
  message: "Bad request.",
48
49
  info: {
49
50
  status: "BAD_VALUE",
50
- message: "Bad with wrong Advance guard."
51
+ message: "Bad with wrong Advance guard key."
51
52
  },
52
53
  });
53
54
  }
@@ -3,8 +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 { avgDeHashIt } = require(__dirname + "/../helpers/math");
7
- const moment = require("moment");
6
+ const { Limiter, Duplicater } = require("../middleware");
8
7
 
9
8
  module.exports = {
10
9
  expressStart() {
@@ -144,7 +143,7 @@ module.exports = {
144
143
  // declare authentication endpoint name with publicPath
145
144
  let auth_endpoint = (passport_config.auth_endpoint) ? (passport_config.auth_endpoint[ 0 ] === "/" ? passport_config.auth_endpoint : "/" + passport_config.auth_endpoint) : "/authentication";
146
145
  // authentication endpoints
147
- endpoint.post(auth_endpoint, auth.credentialsGuard, (req, res, next) => {
146
+ endpoint.post(auth_endpoint, Duplicater(), Limiter(), auth.credentialsGuard, (req, res, next) => {
148
147
  passport.authenticate('local', { session: false }, (err, user, opt) => {
149
148
  if (err) {
150
149
  res.status(502).json({ code: 502, status: "BAD_GATEWAY", message: String(err) });
@@ -196,8 +195,49 @@ module.exports = {
196
195
  }
197
196
  })(req, res, next);
198
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
+ });
199
239
  // create auth data endpoints
200
- endpoint.post(auth_endpoint + '/create', auth.credentialsGuard, (req, res) => {
240
+ endpoint.post(auth_endpoint + '/create', Duplicater(), Limiter(), auth.credentialsGuard, (req, res) => {
201
241
  const promise = new Promise((resolve) => {
202
242
  /**
203
243
  *
@@ -251,7 +291,7 @@ module.exports = {
251
291
  });
252
292
  });
253
293
  // patch auth data endpoints
254
- endpoint.patch(auth_endpoint + '/update/:id', auth.credentials, (req, res) => {
294
+ endpoint.patch(auth_endpoint + '/update/:id', Duplicater(), Limiter(), auth.credentials, (req, res) => {
255
295
  const promise = new Promise((resolve) => {
256
296
  if (passport_config.app_key_allow) {
257
297
  if (req.headers.app_key) {
@@ -305,7 +345,7 @@ module.exports = {
305
345
  *
306
346
  */
307
347
  if (passport_config.strategy.google.allow) {
308
- endpoint.get(auth_endpoint + '/google', passport.authenticate('google', {
348
+ endpoint.get(auth_endpoint + '/google', Limiter(), passport.authenticate('google', {
309
349
  scope: [
310
350
  'https://www.googleapis.com/auth/userinfo.email',
311
351
  'https://www.googleapis.com/auth/plus.login'
@@ -313,7 +353,7 @@ module.exports = {
313
353
  }));
314
354
  // google auth callback
315
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";
316
- 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) => {
317
357
  if (typeof req.user.user !== 'undefined') {
318
358
  // declare user for sign JWT
319
359
  let user = JSON.parse(JSON.stringify(req.user.user));
@@ -362,10 +402,10 @@ module.exports = {
362
402
  *
363
403
  */
364
404
  if (passport_config.strategy.facebook.allow) {
365
- 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' ] }));
366
406
  // facebook callback
367
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";
368
- 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) => {
369
409
  if (typeof req.user.user !== 'undefined') {
370
410
  // declare user for sign JWT
371
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
+ }