binhend 2.2.12 → 2.3.1

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/index.js CHANGED
@@ -8,10 +8,13 @@
8
8
 
9
9
  const path = require('path');
10
10
 
11
+
11
12
  //___ Run scripts ___//
12
13
  const moduleAlias = require('./packages/module-alias'); // path alias '@' for requiring modules: require('@/any/path') - use jsconfig.json
13
14
  moduleAlias.path('@binhend/*', [ path.join(__dirname, './packages') ]); // set path alias '@binhend' for all packages to call each others when 'npm install' in another project
14
15
 
16
+
17
+ //___ Load packages ___//
15
18
  const server = require('./packages/core/src/server');
16
19
  const Router = require('./packages/core/src/Router');
17
20
  const { routes } = require('./packages/core/src/routes');
@@ -24,6 +27,7 @@ const { HTTPS } = require('./packages/https/src/https');
24
27
 
25
28
  const cors = require('./packages/middlewares/src/cors');
26
29
  const trycatch = require('./packages/middlewares/src/trycatch');
30
+ const middlewares = require('./packages/middlewares');
27
31
 
28
32
  const { config, ConfigLoader } = require('./packages/config/src/configuration');
29
33
 
@@ -39,12 +43,14 @@ const validation = require('./packages/validation/src/typeValidation');
39
43
  const { WebBuild } = require('./packages/web/src/WebBuild');
40
44
  const { binh } = require('./packages/web/src/component.method');
41
45
 
46
+
47
+ //___ Export packages ___//
42
48
  module.exports = {
43
49
  server, routes, Router,
44
50
 
45
51
  Bromise, HttpError, HttpCodes, HTTPS,
46
52
 
47
- cors, trycatch,
53
+ cors, trycatch, middlewares,
48
54
 
49
55
  config, ConfigLoader,
50
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "binhend",
3
- "version": "2.2.12",
3
+ "version": "2.3.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "author": "Nguyen Duc Binh",
@@ -15,10 +15,7 @@
15
15
  "scripts": {
16
16
  "test": "npm run test --workspace=test",
17
17
  "test:coverage": "npm run test:coverage --workspace=test",
18
- "test:jest": "jest --config=test/jest.config.js",
19
- "example-api": "node example_api PORT=5555",
20
- "example-webcomp": "node example_webcomp",
21
- "example-web2": "node example_web2"
18
+ "test:jest": "jest --config=test/jest.config.js"
22
19
  },
23
20
  "dependencies": {
24
21
  "express": "^4.17.1",
@@ -1,5 +1,7 @@
1
1
  module.exports = {
2
2
  cors: require('./src/cors'),
3
+ csrf: require('./src/csrf'),
4
+ authorize: require('./src/authorize'),
3
5
  parseCookies: require('./src/parseCookies'),
4
6
  parseBasicAuthToken: require('./src/parseBasicAuthToken'),
5
7
  trycatch: require('./src/trycatch'),
@@ -0,0 +1,157 @@
1
+
2
+ const util = require('util');
3
+ const must = require('@binhend/validation');
4
+ const { HttpError, HttpCodes } = require('@binhend/utils');
5
+ const { typeOf, isNumber, isFunction, isNullish } = require('@binhend/types');
6
+
7
+
8
+ const Rules = Object.freeze({
9
+ OPTIONAL: 0
10
+ });
11
+
12
+ /**
13
+ * Auth - Function constructor for creating auth middleware builder
14
+ *
15
+ * @constructor
16
+ * @param {Function} parseAuthPayload - Function to parse/verify auth payload from request
17
+ */
18
+ function Auth(parseAuthPayload) {
19
+ if (!isFunction(parseAuthPayload)) {
20
+ throw new Error('Auth middleware requires a function to parse/verify auth payload from request.');
21
+ }
22
+
23
+ this.Rules = Rules;
24
+
25
+ /**
26
+ * Create auth middleware with pre-defined rules
27
+ * @param {Object} rules - Auth rules
28
+ * @returns {Function} Auth middleware
29
+ */
30
+ this.auth = function (rules) {
31
+ return async function (request, response, next) {
32
+ var payload, errorMessage;
33
+
34
+ try {
35
+ payload = parseAuthPayload(request);
36
+ payload = payload instanceof Promise ? await payload : payload; // the input function works properly for both sync and async
37
+ errorMessage = checkRuleViolation(rules, payload, request); // validate payload with rules
38
+ }
39
+ catch (error) {
40
+ if (rules === Rules.OPTIONAL) { // Auth is not strictly required
41
+ next();
42
+ return;
43
+ }
44
+ if (error instanceof HttpError) throw error;
45
+ else errorMessage = 'Auth payload might be invalid, expired or not provided';
46
+ console.error(
47
+ `[BINHEND][AUTH] Failed parsing auth payload. Error:`,
48
+ util.inspect(error, { showHidden: true, depth: null, colors: true })
49
+ );
50
+ }
51
+
52
+ if (errorMessage) {
53
+ console.error(`[BINHEND][AUTH] Access denied. ${errorMessage}.`);
54
+ throw new HttpError(HttpCodes.UNAUTHORIZED, 'Access denied. ' + errorMessage);
55
+ }
56
+
57
+ next();
58
+ };
59
+ };
60
+
61
+ this.payload = function (request) {
62
+ // Implementation ensure throwing error natively in router middleware or API controller,
63
+ // even when this method (.payload()) or the parsing handler (.parseAuthPayload()) is called with sync/async approach.
64
+ try {
65
+ var payload = parseAuthPayload(request);
66
+ return payload instanceof Promise ? payload.catch(handleError) : payload;
67
+ }
68
+ catch (error) {
69
+ handleError(error);
70
+ }
71
+
72
+ function handleError(error) {
73
+ if (error instanceof HttpError) throw error;
74
+ throw new HttpError(HttpCodes.UNAUTHORIZED, 'Access denied.');
75
+ }
76
+
77
+ // NTOICE: why not handle cache (e.g. request.user) for payload()?
78
+ // => side-effect might happens, outside logic could replace reference of request.user
79
+ // => payload() must be a trust source of returning auth payload (parsing again required)
80
+ // => users must handle cache in parseAuthPayload() by their own, also helpful to unify where cache is. (e.g. request.user, request.credentials, request.data, etc.)
81
+ // Bonus: if handling cache in, e.g. request.user, then should not use request.user elsewhere, but always calling payload() to get auth payload (cache) in an unified manner.
82
+ };
83
+
84
+ this.match = {
85
+ query: function (key) {
86
+ return (value, request) => value === request?.query?.[key];
87
+ },
88
+ params: function (key) {
89
+ return (value, request) => value === request?.params?.[key];
90
+ },
91
+ body: function (key) {
92
+ return (value, request) => value === request?.body?.[key];
93
+ }
94
+ };
95
+ }
96
+
97
+ function checkRuleViolation(rules, payload, request) {
98
+ var errorMessage;
99
+
100
+ if (isNullish(payload)) {
101
+ errorMessage = `Auth payload is null or undefined`;
102
+ return errorMessage;
103
+ }
104
+
105
+ rules = must.Object(rules, { default: {} });
106
+
107
+ for (const [key, expectedValue] of Object.entries(rules)) {
108
+
109
+ if (!payload.hasOwnProperty(key)) {
110
+ errorMessage = `Auth payload has no assignment for property: ${key}`;
111
+ break;
112
+ }
113
+
114
+ const actualValue = payload[key];
115
+
116
+ switch (typeOf(expectedValue)) {
117
+
118
+ case 'Array': // Handle array values (membership check)
119
+ const matchOneOfAllowedValues = expectedValue.includes(actualValue);
120
+ if (!matchOneOfAllowedValues) {
121
+ errorMessage = `Expected ${key}: ${expectedValue.join('|')}. Got: ${actualValue}`;
122
+ }
123
+ break;
124
+
125
+ case 'Number': // Handle number values (minimum level check)
126
+ const reachMinNumber = isNumber(actualValue) && actualValue >= expectedValue;
127
+ if (!reachMinNumber) {
128
+ errorMessage = `Minimum ${key} required: ${expectedValue}. Got: ${actualValue}`;
129
+ }
130
+ break;
131
+
132
+ case 'Function': // Handle function values (custom validation)
133
+ try {
134
+ const isValidCheck = expectedValue(actualValue, request);
135
+ if (!isValidCheck) errorMessage = `Custom validation failed for field: ${key}`;
136
+ }
137
+ catch (error) {
138
+ errorMessage = `Custom validation error for field '${key}': ${error.message}`;
139
+ if (error instanceof HttpError) throw error;
140
+ }
141
+ break;
142
+
143
+ default: // Handle all other types (exact match)
144
+ const matchExactValue = actualValue !== expectedValue;
145
+ if (!matchExactValue) {
146
+ errorMessage = `Expected ${key}: ${expectedValue}. Got: ${actualValue}`;
147
+ }
148
+ break;
149
+ }
150
+
151
+ if (errorMessage) break;
152
+ }
153
+
154
+ return errorMessage;
155
+ }
156
+
157
+ module.exports = { Auth };
@@ -0,0 +1,89 @@
1
+ const crypto = require('crypto');
2
+ const types = require('@binhend/types');
3
+ const validation = require('@binhend/validation');
4
+ const { HttpCodes } = require('@binhend/utils');
5
+
6
+ /**
7
+ * Utility function to generate CSRF token with expiry.
8
+ *
9
+ * @returns {Object} { token, expiresAt }
10
+ */
11
+ function generateTokenWithExpiry() {
12
+ const token = crypto.randomBytes(32).toString('hex');
13
+ const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes expiry time
14
+ return JSON.stringify({ token, expiresAt });
15
+ }
16
+
17
+ /**
18
+ * Configurable middleware to verify and rotate CSRF tokens.
19
+ */
20
+ module.exports = function (options = {}) {
21
+ // Prepare settings from custom and default options
22
+ const SAFE_HTTP_METHODS = validation.Array(options.safeMethods, { default: ['GET', 'HEAD', 'OPTIONS'] });
23
+ const CSRF_COOKIE_NAME = validation.String(options.cookie, { default: 'csrf_token' });
24
+ const CSRF_HEADER_NAME = validation.String(options.header, { default: 'X-CSRF-Token' });
25
+ const cookieSecure = validation.Boolean(options.secure, { default: true });
26
+ const cookieSameSite = validation.String(options.sameSite, { default: 'Strict' });
27
+ const cookiePath = validation.String(options.path);
28
+ const response = validation.Object(options.response);
29
+
30
+ // Finalize cookie options
31
+ const cookieOptions = {
32
+ httpOnly: false, // Client needs to read this
33
+ secure: cookieSecure,
34
+ sameSite: cookieSameSite
35
+ };
36
+
37
+ if (!types.isEmptyString(cookiePath)) {
38
+ cookieOptions.path = cookiePath;
39
+ }
40
+
41
+ // No return, but apply CSRF token for a response to client.
42
+ if (response) {
43
+ response.cookie(CSRF_COOKIE_NAME, generateTokenWithExpiry(), cookieOptions);
44
+ return;
45
+ }
46
+
47
+ // Return a middleware to protect route from CSRF attacks
48
+ return function (request, response, next) {
49
+ // Skip CSRF verification for safe methods
50
+ if (SAFE_HTTP_METHODS.includes(request.method)) {
51
+ return next();
52
+ }
53
+
54
+ // Read and check existence of CSRF token in header and cookie
55
+ const csrfCookie = request.cookies[CSRF_COOKIE_NAME];
56
+ const csrfHeader = request.get(CSRF_HEADER_NAME);
57
+
58
+ if (!csrfCookie || !csrfHeader) {
59
+ return response.status(HttpCodes.FORBIDDEN).json({ error: 'Missing CSRF token' });
60
+ }
61
+
62
+ // Validate token format
63
+ var parsed;
64
+
65
+ try {
66
+ parsed = JSON.parse(csrfCookie);
67
+ }
68
+ catch {
69
+ return response.status(HttpCodes.FORBIDDEN).json({ error: 'Malformed CSRF token' });
70
+ }
71
+
72
+ const { token, expiresAt } = parsed;
73
+
74
+ // Verify if the token in the header matches the cookie => valid
75
+ if (csrfHeader !== token) {
76
+ return response.status(403).json({ error: 'Invalid CSRF token' });
77
+ }
78
+
79
+ const isExpired = Date.now() > expiresAt;
80
+
81
+ // If the current token is expired (but valid), generate a new token and set it in the cookie
82
+ if (isExpired) {
83
+ const newTokenCSRF = generateTokenWithExpiry();
84
+ response.cookie(CSRF_COOKIE_NAME, newTokenCSRF, cookieOptions);
85
+ }
86
+
87
+ next(); // Proceed with the request
88
+ }
89
+ };
package/index2.js DELETED
@@ -1,56 +0,0 @@
1
- /*
2
- * binhend
3
- * Copyright(c) 2023 Nguyen Duc Binh
4
- *
5
- */
6
-
7
- 'use strict';
8
-
9
- /** SERVER - Backend */
10
- const { server } = require('./src/server');
11
- const { ConfigLoader, config } = require('./src/configuration');
12
- const { HTTPS } = require('./src/https');
13
-
14
- const HttpError = require('./src/api/HttpError');
15
- const HttpCodes = require('./src/api/HttpCodes');
16
-
17
- const Router = require('./src/api/Router');
18
- const { routes, loadRoutes, buildRoutes, mapRoutes } = require('./src/api/routes');
19
- const trycatch = require('./src/api/trycatch');
20
-
21
- const cors = require('./src/middleware/cors');
22
-
23
- const createCSD = require('./src/csd');
24
- const crypto = require('./src/crypto');
25
- const types = require('./src/utils/types');
26
- const validation = require('./src/utils/validation');
27
- const typedefs = require('./src/utils/typedefs');
28
- const Bromise = require('./src/utils/Bromise.js');
29
-
30
- /** CLIENT - Frontend */
31
- const { WebBuild } = require('./src/web');
32
- const { binh } = require('./src/web/component.method');
33
-
34
- // Run scripts
35
- require('./src/alias-module');
36
-
37
- module.exports = {
38
- server,
39
- ConfigLoader, config,
40
- HTTPS,
41
- HttpError,
42
- HttpCodes,
43
- Router,
44
- routes, buildRoutes, loadRoutes, mapRoutes,
45
- trycatch,
46
- cors,
47
- createCSD,
48
- crypto,
49
- types,
50
- typedefs,
51
- validation,
52
- Bromise,
53
- WebBuild,
54
- WebBuilder: WebBuild,
55
- binh
56
- };
@@ -1,10 +0,0 @@
1
-
2
- /**
3
- * @typedef {Object<string, any>} ObjectType
4
- */
5
-
6
- /**
7
- * @typedef {(Date|string|number)} DateType
8
- */
9
-
10
- export {};
@@ -1,2 +0,0 @@
1
-
2
- export * from './common.d.ts';
@@ -1,31 +0,0 @@
1
-
2
- /**
3
- * @typedef {Object<string, any>} ObjectType
4
- */
5
-
6
- /**
7
- * Type definition for object accepting any extra properties
8
- *
9
- * @returns {ObjectType}
10
- */
11
- function ObjectType() { return this; }
12
-
13
-
14
-
15
- /**
16
- * @typedef {(Date|string|number)} DateType
17
- */
18
-
19
- /**
20
- * Type definition for date accepting any valid formats (string, number, date)
21
- *
22
- * @returns {DateType}
23
- */
24
- function DateType() { return this; }
25
-
26
-
27
-
28
- module.exports = {
29
- ObjectType,
30
- DateType
31
- };