binhend 2.2.11 → 2.3.0
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 +5 -0
- package/package.json +2 -5
- package/packages/middlewares/index.js +2 -0
- package/packages/middlewares/src/authorize.js +157 -0
- package/packages/middlewares/src/csrf.js +89 -0
- package/packages/module-alias/src/alias-cjs-loader.js +3 -5
- package/index2.js +0 -56
- package/src/types/common.d.ts +0 -10
- package/src/types/index.d.ts +0 -2
- package/src/utils/typedefs.js +0 -31
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');
|
|
@@ -39,6 +42,8 @@ const validation = require('./packages/validation/src/typeValidation');
|
|
|
39
42
|
const { WebBuild } = require('./packages/web/src/WebBuild');
|
|
40
43
|
const { binh } = require('./packages/web/src/component.method');
|
|
41
44
|
|
|
45
|
+
|
|
46
|
+
//___ Export packages ___//
|
|
42
47
|
module.exports = {
|
|
43
48
|
server, routes, Router,
|
|
44
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "binhend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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
|
+
};
|
|
@@ -53,17 +53,15 @@ Module._resolveFilename = function (request, parent, isMain, options) {
|
|
|
53
53
|
if (!request.startsWith(alias)) continue;
|
|
54
54
|
// Loop through the array of resolved paths for this alias
|
|
55
55
|
for (const resolvedPath of aliasMap[alias]) {
|
|
56
|
-
// Construct
|
|
57
|
-
var newRequest;
|
|
56
|
+
// Construct a new request path (convert @ path to resolved path)
|
|
57
|
+
var newRequest = request.replace(alias, resolvedPath);
|
|
58
58
|
|
|
59
|
+
// Check if the above resolved path matching web component path, then change from source directory to module directory
|
|
59
60
|
if (parent.filename.startsWith(settings.webModulePath)) {
|
|
60
61
|
// BINHEND web components (formatted code) load each others in build-time still requiring src paths (src file paths), not module paths (formatted file paths)
|
|
61
62
|
// parent.filename is the file location of module which makes this request by calling require()
|
|
62
63
|
newRequest = newRequest.replace(settings.webSourcePath, settings.webModulePath);
|
|
63
64
|
}
|
|
64
|
-
else {
|
|
65
|
-
newRequest = request.replace(alias, resolvedPath);
|
|
66
|
-
}
|
|
67
65
|
|
|
68
66
|
// Attempt to resolve the new request
|
|
69
67
|
try {
|
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
|
-
};
|
package/src/types/common.d.ts
DELETED
package/src/types/index.d.ts
DELETED
package/src/utils/typedefs.js
DELETED
|
@@ -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
|
-
};
|