underpost 2.8.87 → 2.8.88

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.
Files changed (99) hide show
  1. package/.env.development +35 -3
  2. package/.env.production +39 -4
  3. package/.env.test +35 -3
  4. package/.github/workflows/ghpkg.ci.yml +1 -1
  5. package/.github/workflows/npmpkg.ci.yml +1 -1
  6. package/.github/workflows/pwa-microservices-template-page.cd.yml +6 -5
  7. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  8. package/.github/workflows/release.cd.yml +3 -3
  9. package/README.md +56 -2
  10. package/bin/build.js +4 -0
  11. package/bin/deploy.js +62 -8
  12. package/bin/file.js +3 -2
  13. package/cli.md +8 -2
  14. package/conf.js +30 -4
  15. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  16. package/manifests/deployment/dd-test-development/deployment.yaml +174 -0
  17. package/manifests/deployment/dd-test-development/proxy.yaml +51 -0
  18. package/package.json +6 -5
  19. package/src/api/core/core.router.js +2 -1
  20. package/src/api/default/default.controller.js +6 -1
  21. package/src/api/default/default.router.js +6 -2
  22. package/src/api/default/default.service.js +10 -1
  23. package/src/api/document/document.controller.js +66 -0
  24. package/src/api/document/document.model.js +51 -0
  25. package/src/api/document/document.router.js +24 -0
  26. package/src/api/document/document.service.js +125 -0
  27. package/src/api/file/file.controller.js +15 -1
  28. package/src/api/file/file.router.js +2 -1
  29. package/src/api/test/test.router.js +1 -1
  30. package/src/api/user/postman_collection.json +216 -0
  31. package/src/api/user/user.controller.js +25 -60
  32. package/src/api/user/user.model.js +29 -7
  33. package/src/api/user/user.router.js +9 -3
  34. package/src/api/user/user.service.js +84 -32
  35. package/src/cli/baremetal.js +33 -3
  36. package/src/cli/cloud-init.js +11 -0
  37. package/src/cli/cron.js +0 -1
  38. package/src/cli/deploy.js +46 -23
  39. package/src/cli/index.js +5 -0
  40. package/src/cli/lxd.js +7 -0
  41. package/src/cli/repository.js +42 -6
  42. package/src/cli/run.js +45 -13
  43. package/src/cli/ssh.js +20 -6
  44. package/src/client/Default.index.js +42 -1
  45. package/src/client/components/core/Account.js +10 -2
  46. package/src/client/components/core/AgGrid.js +30 -8
  47. package/src/client/components/core/Auth.js +99 -56
  48. package/src/client/components/core/BtnIcon.js +3 -2
  49. package/src/client/components/core/CalendarCore.js +2 -3
  50. package/src/client/components/core/CommonJs.js +1 -2
  51. package/src/client/components/core/Content.js +15 -12
  52. package/src/client/components/core/Css.js +2 -1
  53. package/src/client/components/core/CssCore.js +6 -1
  54. package/src/client/components/core/Docs.js +5 -5
  55. package/src/client/components/core/FileExplorer.js +3 -3
  56. package/src/client/components/core/Input.js +22 -17
  57. package/src/client/components/core/JoyStick.js +2 -2
  58. package/src/client/components/core/LoadingAnimation.js +2 -2
  59. package/src/client/components/core/LogIn.js +16 -23
  60. package/src/client/components/core/LogOut.js +5 -1
  61. package/src/client/components/core/Logger.js +4 -1
  62. package/src/client/components/core/Modal.js +82 -53
  63. package/src/client/components/core/ObjectLayerEngineModal.js +2 -1
  64. package/src/client/components/core/Pagination.js +207 -0
  65. package/src/client/components/core/Panel.js +10 -10
  66. package/src/client/components/core/PanelForm.js +130 -33
  67. package/src/client/components/core/Recover.js +2 -2
  68. package/src/client/components/core/Router.js +210 -34
  69. package/src/client/components/core/SignUp.js +1 -2
  70. package/src/client/components/core/Stream.js +1 -1
  71. package/src/client/components/core/VanillaJs.js +3 -84
  72. package/src/client/components/core/Worker.js +2 -2
  73. package/src/client/components/default/LogInDefault.js +0 -6
  74. package/src/client/components/default/LogOutDefault.js +0 -16
  75. package/src/client/components/default/MenuDefault.js +97 -44
  76. package/src/client/components/default/RoutesDefault.js +5 -2
  77. package/src/client/services/core/core.service.js +8 -20
  78. package/src/client/services/default/default.management.js +115 -18
  79. package/src/client/services/default/default.service.js +13 -4
  80. package/src/client/services/document/document.service.js +97 -0
  81. package/src/client/services/file/file.service.js +2 -0
  82. package/src/client/services/test/test.service.js +3 -0
  83. package/src/client/services/user/user.management.js +6 -0
  84. package/src/client/services/user/user.service.js +15 -4
  85. package/src/client/ssr/Render.js +1 -1
  86. package/src/client/ssr/head/DefaultScripts.js +3 -0
  87. package/src/client/ssr/head/Seo.js +1 -0
  88. package/src/index.js +24 -2
  89. package/src/runtime/lampp/Lampp.js +89 -2
  90. package/src/runtime/xampp/Xampp.js +48 -1
  91. package/src/server/auth.js +519 -155
  92. package/src/server/backup.js +2 -2
  93. package/src/server/conf.js +66 -8
  94. package/src/server/process.js +2 -1
  95. package/src/server/runtime.js +135 -286
  96. package/src/server/ssl.js +1 -2
  97. package/src/server/ssr.js +85 -0
  98. package/src/server/start.js +2 -2
  99. package/src/server/valkey.js +2 -1
@@ -8,228 +8,592 @@ import dotenv from 'dotenv';
8
8
  import jwt from 'jsonwebtoken';
9
9
  import { loggerFactory } from './logger.js';
10
10
  import crypto from 'crypto';
11
- import { userRoleEnum } from '../api/user/user.model.js';
11
+ import { promisify } from 'util';
12
+ import { UserDto } from '../api/user/user.model.js';
12
13
  import { commonAdminGuard, commonModeratorGuard, validatePassword } from '../client/components/core/CommonJs.js';
14
+ import helmet from 'helmet';
15
+ import rateLimit from 'express-rate-limit';
16
+ import slowDown from 'express-slow-down';
17
+ import cors from 'cors';
18
+ import cookieParser from 'cookie-parser';
19
+ import { DataBaseProvider } from '../db/DataBaseProvider.js';
13
20
 
14
21
  dotenv.config();
15
-
16
22
  const logger = loggerFactory(import.meta);
17
23
 
18
- /* The `const config` object is defining parameters related to the hashing process used for password
19
- security. Here's a breakdown of each property in the `config` object: */
24
+ // Promisified crypto functions
25
+ const pbkdf2 = promisify(crypto.pbkdf2);
26
+
27
+ // Config with sane defaults and parsing
20
28
  const config = {
21
- hashBytes: 32,
22
- saltBytes: 16,
23
- iterations: 872791,
24
- digest: 'sha512',
29
+ hashBytes: Number(process.env.PBKDF2_HASH_BYTES) || 32,
30
+ saltBytes: Number(process.env.PBKDF2_SALT_BYTES) || 16,
31
+ iterations: Number(process.env.PBKDF2_ITERATIONS) || 150_000,
32
+ digest: process.env.PBKDF2_DIGEST || 'sha512',
33
+ refreshTokenBytes: Number(process.env.REFRESH_TOKEN_BYTES) || 48,
34
+ jwtAlgorithm: process.env.JWT_ALGORITHM || 'HS512', // consider RS256 with keys
25
35
  };
26
36
 
37
+ // ---------- Password hashing (async) ----------
27
38
  /**
28
- * @param {String} password - given password to hash
29
- * @returns {String} the hash corresponding to the password
39
+ * Hash password asynchronously using PBKDF2.
40
+ * Stored format: iterations$salt$hash
41
+ * @param {string} password The password to hash.
42
+ * @returns {Promise<string>} The hashed password string.
30
43
  * @memberof Auth
31
44
  */
32
- function hashPassword(password) {
33
- const { iterations, hashBytes, digest, saltBytes } = config;
34
- const salt = crypto.randomBytes(saltBytes).toString('hex');
35
- const hash = crypto.pbkdf2Sync(password, salt, iterations, hashBytes, digest).toString('hex');
36
- return [salt, hash].join('$');
45
+ async function hashPassword(password) {
46
+ const salt = crypto.randomBytes(config.saltBytes).toString('hex');
47
+ const derived = await pbkdf2(password, salt, config.iterations, config.hashBytes, config.digest);
48
+ return `${config.iterations}$${salt}$${derived.toString('hex')}`;
37
49
  }
38
50
 
39
51
  /**
40
- * @param {String} password - password to verify
41
- * @param {String} combined - a combined salt + hash returned by hashPassword function
42
- * @returns {Boolean} true if password correspond to the hash. False otherwise
52
+ * Verify password using constant-time comparison
53
+ * @param {string} password The password to verify.
54
+ * @param {string} combined The stored hashed password string (iterations$salt$hash).
55
+ * @returns {Promise<boolean>} True if the password is valid, false otherwise.
43
56
  * @memberof Auth
44
57
  */
45
- function verifyPassword(password, combined) {
46
- const { iterations, hashBytes, digest } = config;
47
- const [salt, originalHash] = combined.split('$');
48
- const hash = crypto.pbkdf2Sync(password, salt, iterations, hashBytes, digest).toString('hex');
49
- return hash === originalHash;
58
+ async function verifyPassword(password, combined) {
59
+ if (!combined) return false;
60
+ const parts = combined.split('$');
61
+ if (parts.length !== 3) return false;
62
+ const [itersStr, salt, originalHex] = parts;
63
+ const iterations = parseInt(itersStr, 10);
64
+ const derived = await pbkdf2(password, salt, iterations, Buffer.from(originalHex, 'hex').length, config.digest);
65
+ const original = Buffer.from(originalHex, 'hex');
66
+ const ok = crypto.timingSafeEqual(derived, original);
67
+ return ok;
50
68
  }
51
69
 
52
- // jwt middleware
53
-
70
+ // ---------- Token hashing & utilities ----------
54
71
  /**
55
- * The hashJWT function generates a JSON Web Token (JWT) with a specified payload and expiration time.
56
- * @param payload - The `payload` parameter in the `hashJWT` function is the data that you want to
57
- * encode into the JSON Web Token (JWT). It typically contains information about the user or any other
58
- * relevant data that you want to securely transmit.
59
- * @param expire - The `expire` parameter in the `hashJWT` function is used to specify the expiration
60
- * time for the JSON Web Token (JWT) being generated. If a value is provided for `expire`, it will be
61
- * used as the expiration time. If `expire` is not provided (i.e., it
72
+ * Hashes a token using SHA256.
73
+ * @param {string} token The token to hash.
74
+ * @returns {string|null} The hashed token as a hex string, or null if token is falsy.
62
75
  * @memberof Auth
63
76
  */
64
- const hashJWT = (payload, expire) =>
65
- jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: expire !== undefined ? expire : `${process.env.EXPIRE}h` });
77
+ function hashToken(token) {
78
+ if (!token) return null;
79
+ return crypto.createHash('sha256').update(token).digest('hex');
80
+ }
66
81
 
67
82
  /**
68
- * The function `verifyJWT` is used to verify a JSON Web Token (JWT) using a secret key stored in the
69
- * environment variables.
70
- * @param token - The `token` parameter is a JSON Web Token (JWT) that is passed to the `verifyJWT`
71
- * function for verification.
83
+ * Generates a cryptographically secure random hex string.
84
+ * @param {number} [bytes=config.refreshTokenBytes] The number of bytes to generate.
85
+ * @returns {string} The random hex string.
72
86
  * @memberof Auth
73
87
  */
74
- const verifyJWT = (token = '') => jwt.verify(token, process.env.JWT_SECRET);
88
+ function generateRandomHex(bytes = config.refreshTokenBytes) {
89
+ return crypto.randomBytes(bytes).toString('hex');
90
+ }
75
91
 
76
92
  /**
77
- * The function `getBearerToken` extracts and returns the Bearer token from the Authorization header in
78
- * a request object.
79
- * @param req - The `req` parameter in the `getBearerToken` function is typically an object
80
- * representing the HTTP request. It is commonly used in Node.js applications with frameworks like
81
- * Express.js. The `req` object contains information about the incoming HTTP request, including
82
- * headers, body, parameters, and more. In
83
- * @returns {String} The function `getBearerToken` is returning the Bearer token extracted from the
84
- * Authorization header in the request object. If the Authorization header starts with 'Bearer ', it
85
- * will return the token portion of the header (excluding 'Bearer ').
93
+ * Generates a JWT issuer and audience based on the host and path.
94
+ * @param {object} options The options object.
95
+ * @param {string} options.host The host name.
96
+ * @param {string} options.path The path name.
97
+ * @returns {object} The issuer and audience.
86
98
  * @memberof Auth
87
99
  */
88
- const getBearerToken = (req) => {
89
- const authHeader = String(req.headers['authorization'] || req.headers['Authorization'] || '');
90
- if (authHeader.startsWith('Bearer ')) return authHeader.substring(7, authHeader.length);
91
- return '';
92
- };
100
+ function jwtIssuerAudienceFactory(options = { host: '', path: '' }) {
101
+ const audience = `${options.host}${options.path === '/' ? '' : options.path}`;
102
+ const issuer = `${audience}/api`;
103
+ return { issuer, audience };
104
+ }
93
105
 
106
+ // ---------- JWT helpers ----------
94
107
  /**
95
- * The function `getPayloadJWT` extracts and verifies a JWT payload from a request using a bearer
96
- * token.
97
- * @param req - The `req` parameter is typically used in web development to represent the HTTP request
98
- * object. It contains information about the incoming request, such as headers, parameters, and body
99
- * data. In this context, it seems like the `getPayloadJWT` function is designed to extract and verify
100
- * a JWT token from
101
- * @returns {Object} The JWT payload from a request using a bearer
108
+ * Signs a JWT payload.
109
+ * @param {object} payload The payload to sign.
110
+ * @param {object} [options={}] Additional JWT sign options.
111
+ * @param {string} options.host The host name.
112
+ * @param {string} options.path The path name.
113
+ * @param {number} expireMinutes The token expiration in minutes.
114
+ * @returns {string} The signed JWT.
115
+ * @throws {Error} If JWT key is not configured.
102
116
  * @memberof Auth
103
117
  */
104
- const getPayloadJWT = (req) => verifyJWT(getBearerToken(req));
118
+ function jwtSign(payload, options = { host: '', path: '' }, expireMinutes = process.env.ACCESS_EXPIRE_MINUTES) {
119
+ const { issuer, audience } = jwtIssuerAudienceFactory(options);
120
+ const signOptions = {
121
+ algorithm: config.jwtAlgorithm,
122
+ expiresIn: `${expireMinutes}m`,
123
+ issuer,
124
+ audience,
125
+ };
126
+
127
+ if (!payload.jwtid) signOptions.jwtid = generateRandomHex();
128
+
129
+ if (!process.env.JWT_SECRET) throw new Error('JWT key not configured');
130
+
131
+ logger.info('JWT signed', { payload, signOptions, expireMinutes });
132
+
133
+ return jwt.sign(payload, process.env.JWT_SECRET, signOptions);
134
+ }
105
135
 
106
136
  /**
107
- * The authMiddleware function checks and verifies the authorization token in the request headers
108
- * before allowing access to protected routes.
109
- * @param req - The `req` parameter in the `authMiddleware` function stands for the request object. It
110
- * contains information about the HTTP request made to the server, including headers, body, parameters,
111
- * and more. In this context, the function is extracting the authorization token from the request
112
- * headers to authenticate the user.
113
- * @param res - The `res` parameter in the `authMiddleware` function is the response object that
114
- * represents the HTTP response that an Express.js server sends when it receives an HTTP request. It is
115
- * used to send a response back to the client with status codes, headers, and data.
116
- * @param next - The `next` parameter in the `authMiddleware` function is a callback function that is
117
- * used to pass control to the next middleware function in the stack. When called, it invokes the next
118
- * middleware function in the chain. This is a common pattern in Express.js middleware functions to
119
- * move to the next middleware
120
- * @returns {Object} The `req.auth` included JWT payload in request authorization
137
+ * Verifies a JWT.
138
+ * @param {string} token The JWT to verify.
139
+ * @param {object} [options={}] Additional JWT verify options.
140
+ * @param {string} options.host The host name.
141
+ * @param {string} options.path The path name.
142
+ * @returns {object} The decoded payload.
143
+ * @throws {jwt.JsonWebTokenError} If the token is invalid or expired.
121
144
  * @memberof Auth
122
145
  */
123
- const authMiddleware = (req, res, next) => {
146
+ function jwtVerify(token, options = { host: '', path: '' }) {
124
147
  try {
125
- const token = getBearerToken(req);
126
- if (token) {
127
- const payload = verifyJWT(token);
128
- req.auth = payload;
129
- return next();
130
- } else
131
- return res.status(401).json({
132
- status: 'error',
133
- message: 'unauthorized: invalid token',
134
- });
135
- } catch (error) {
136
- logger.error(error, error.stack);
137
- return res.status(400).json({
138
- status: 'error',
139
- message: error.message,
140
- });
148
+ const { issuer, audience } = jwtIssuerAudienceFactory(options);
149
+ const verifyOptions = {
150
+ algorithms: [config.jwtAlgorithm],
151
+ issuer,
152
+ audience,
153
+ };
154
+ return jwt.verify(token, process.env.JWT_SECRET, verifyOptions);
155
+ } catch (err) {
156
+ throw err;
141
157
  }
158
+ }
159
+
160
+ // ---------- Request helpers ----------
161
+ /**
162
+ * Extracts the Bearer token from the request headers.
163
+ * @param {import('express').Request} req The Express request object.
164
+ * @returns {string} The token, or an empty string if not found.
165
+ * @memberof Auth
166
+ */
167
+ const getBearerToken = (req) => {
168
+ const header = String(req.headers['authorization'] || req.headers['Authorization'] || '');
169
+ if (header.startsWith('Bearer ')) return header.slice(7).trim();
170
+ return '';
142
171
  };
143
172
 
173
+ // ---------- Middleware ----------
144
174
  /**
145
- * The `adminGuard` function checks if the user has admin role permission and returns an error message
146
- * if not.
147
- * @param req - The `req` parameter typically represents the HTTP request object in Node.js. It
148
- * contains information about the incoming request such as the request headers, parameters, body, and
149
- * more. In the context of your `adminGuard` function, `req` is the request object that is being passed
150
- * to the middleware
151
- * @param res - The `res` parameter in the `adminGuard` function is the response object in Express.js.
152
- * It is used to send a response back to the client making the HTTP request.
153
- * @param next - The `next` parameter in the `adminGuard` function is a callback function that is used
154
- * to pass control to the next middleware function in the stack. When called, it executes the next
155
- * middleware function. If there are no more middleware functions in the stack, it will proceed to the
156
- * route handler.
157
- * @returns The `adminGuard` function is returning either a 403 status with an error message if the
158
- * user role is not 'admin', or it is calling the `next()` function to proceed to the next middleware
159
- * if the user role is 'admin'. If an error occurs during the process, it will log the error and return
160
- * a 400 status with the error message.
175
+ * Creates a middleware to authenticate requests using a JWT Bearer token.
176
+ * @param {object} options The options object.
177
+ * @param {string} options.host The host name.
178
+ * @param {string} options.path The path name.
179
+ * @returns {function} The middleware function.
180
+ * @memberof Auth
181
+ */
182
+ const authMiddlewareFactory = (options = { host: '', path: '' }) => {
183
+ /**
184
+ * Express middleware to authenticate requests using a JWT Bearer token.
185
+ * @param {import('express').Request} req The Express request object.
186
+ * @param {import('express').Response} res The Express response object.
187
+ * @param {import('express').NextFunction} next The next middleware function.
188
+ * @memberof Auth
189
+ */
190
+ const authMiddleware = async (req, res, next) => {
191
+ try {
192
+ const token = getBearerToken(req);
193
+ if (!token) return res.status(401).json({ status: 'error', message: 'unauthorized: token missing' });
194
+
195
+ const payload = jwtVerify(token, options);
196
+
197
+ // Validate IP and User-Agent to mitigate token theft
198
+ if (payload.ip && payload.ip !== req.ip) {
199
+ logger.warn(`IP mismatch for ${payload._id}: jwt(${payload.ip}) !== req(${req.ip})`);
200
+ return res.status(401).json({ status: 'error', message: 'unauthorized: ip mismatch' });
201
+ }
202
+
203
+ if (payload.userAgent && payload.userAgent !== req.headers['user-agent']) {
204
+ logger.warn(`UA mismatch for ${payload._id}`);
205
+ return res.status(401).json({ status: 'error', message: 'unauthorized: user-agent mismatch' });
206
+ }
207
+
208
+ // Non-guest verify session exists
209
+ if (payload.jwtid && payload.role !== 'guest') {
210
+ const User = DataBaseProvider.instance[`${payload.host}${payload.path}`].mongoose.models.User;
211
+ const user = await User.findOne({ _id: payload._id, 'activeSessions._id': payload.jwtid }).lean();
212
+
213
+ if (!user) {
214
+ return res.status(401).json({ status: 'error', message: 'unauthorized: invalid session' });
215
+ }
216
+ const session = user.activeSessions.find((s) => s._id.toString() === payload.jwtid);
217
+
218
+ if (!session) {
219
+ return res.status(401).json({ status: 'error', message: 'unauthorized: invalid session' });
220
+ }
221
+
222
+ // check session ip
223
+ if (session.ip !== req.ip) {
224
+ logger.warn(`IP mismatch for ${payload._id}: jwt(${session.ip}) !== req(${req.ip})`);
225
+ return res.status(401).json({ status: 'error', message: 'unauthorized: ip mismatch' });
226
+ }
227
+
228
+ // check session userAgent
229
+ if (session.userAgent !== req.headers['user-agent']) {
230
+ logger.warn(`UA mismatch for ${payload._id}`);
231
+ return res.status(401).json({ status: 'error', message: 'unauthorized: user-agent mismatch' });
232
+ }
233
+
234
+ // compare payload host and path with session host and path
235
+ if (payload.host !== session.host || payload.path !== session.path) {
236
+ logger.warn(`Host or path mismatch for ${payload._id}`);
237
+ return res.status(401).json({ status: 'error', message: 'unauthorized: host or path mismatch' });
238
+ }
239
+
240
+ // check session expiresAt
241
+ const isRefreshTokenReq = req.method === 'GET' && req.params.id === 'auth';
242
+
243
+ if (!isRefreshTokenReq && session.expiresAt < new Date()) {
244
+ return res.status(401).json({ status: 'error', message: 'unauthorized: session expired' });
245
+ }
246
+ }
247
+
248
+ req.auth = { user: payload };
249
+ return next();
250
+ } catch (err) {
251
+ logger.warn('authMiddleware error', err && err.message);
252
+ return res.status(401).json({ status: 'error', message: 'unauthorized' });
253
+ }
254
+ };
255
+ return authMiddleware;
256
+ };
257
+
258
+ /**
259
+ * Express middleware to guard routes for admin users.
260
+ * @param {import('express').Request} req The Express request object.
261
+ * @param {import('express').Response} res The Express response object.
262
+ * @param {import('express').NextFunction} next The next middleware function.
161
263
  * @memberof Auth
162
264
  */
163
265
  const adminGuard = (req, res, next) => {
164
266
  try {
165
- if (!commonAdminGuard(req.auth.user.role))
267
+ if (!req.auth || !commonAdminGuard(req.auth.user.role))
166
268
  return res.status(403).json({ status: 'error', message: 'Insufficient permission' });
167
269
  return next();
168
- } catch (error) {
169
- logger.error(error, error.stack);
170
- return res.status(400).json({
171
- status: 'error',
172
- message: error.message,
173
- });
270
+ } catch (err) {
271
+ logger.error(err);
272
+ return res.status(400).json({ status: 'error', message: 'bad request' });
174
273
  }
175
274
  };
176
-
177
275
  /**
178
- * The function `moderatorGuard` checks if the user's role is at least a moderator and handles errors
179
- * accordingly.
180
- * @param req - The `req` parameter in the `moderatorGuard` function typically represents the HTTP
181
- * request object, which contains information about the incoming request such as headers, parameters,
182
- * body, etc. It is commonly used to access data sent from the client to the server.
183
- * @param res - The `res` parameter in the `moderatorGuard` function is the response object in
184
- * Express.js. It is used to send a response back to the client making the HTTP request.
185
- * @param next - The `next` parameter in the `moderatorGuard` function is a callback function that is
186
- * used to pass control to the next middleware function in the stack. When called, it will execute the
187
- * next middleware function. In the context of Express.js middleware, `next` is typically called to
188
- * move to
189
- * @returns In the `moderatorGuard` function, if the user's role is not a moderator or higher, a 403
190
- * status with an error message "Insufficient permission" is returned. If there is an error during the
191
- * process, a 400 status with the error message is returned. If everything is successful, the `next()`
192
- * function is called to proceed to the next middleware in the chain.
276
+ * Express middleware to guard routes for moderator or admin users.
277
+ * @param {import('express').Request} req The Express request object.
278
+ * @param {import('express').Response} res The Express response object.
279
+ * @param {import('express').NextFunction} next The next middleware function.
193
280
  * @memberof Auth
194
281
  */
195
282
  const moderatorGuard = (req, res, next) => {
196
283
  try {
197
- if (!commonModeratorGuard(req.auth.user.role))
284
+ if (!req.auth || !commonModeratorGuard(req.auth.user.role))
198
285
  return res.status(403).json({ status: 'error', message: 'Insufficient permission' });
199
286
  return next();
200
- } catch (error) {
201
- logger.error(error, error.stack);
202
- return res.status(400).json({
203
- status: 'error',
204
- message: error.message,
205
- });
287
+ } catch (err) {
288
+ logger.error(err);
289
+ return res.status(400).json({ status: 'error', message: 'bad request' });
206
290
  }
207
291
  };
208
292
 
209
- const validatePasswordMiddleware = (req, password) => {
210
- let errors = [];
211
- if (req.body && 'password' in req.body) errors = validatePassword(req.body.password);
212
- if (errors.length > 0)
213
- return {
214
- status: 'error',
215
- message:
216
- 'Password, ' + errors.map((e, i) => (i > 0 ? ', ' : '') + (e[req.lang] ? e[req.lang] : e['en'])).join(''),
217
- };
218
- else
219
- return {
220
- status: 'success',
221
- };
293
+ // ---------- Password validation middleware (server-side) ----------
294
+ /**
295
+ * Validates the password from the request body.
296
+ * @param {import('express').Request} req The Express request object.
297
+ * @returns {{status: 'success'}|{status: 'error', message: string}} Validation result.
298
+ * @memberof Auth
299
+ */
300
+ const validatePasswordMiddleware = (req) => {
301
+ const errors = req.body && 'password' in req.body ? validatePassword(req.body.password) : [];
302
+ if (errors.length) {
303
+ return { status: 'error', message: 'Password: ' + errors.map((e) => e[req.lang] || e.en || e).join(', ') };
304
+ }
305
+ return { status: 'success' };
222
306
  };
223
307
 
308
+ // ---------- Session & Refresh token management ----------
309
+ /**
310
+ * Create session and set refresh cookie. Rotating and hashed stored token.
311
+ * @param {object} user The user object.
312
+ * @param {import('mongoose').Model} User The Mongoose User model.
313
+ * @param {import('express').Request} req The Express request object.
314
+ * @param {import('express').Response} res The Express response object.
315
+ * @param {object} options Additional options.
316
+ * @param {string} options.host The host name.
317
+ * @param {string} options.path The path name.
318
+ * @returns {Promise<{jwtid: string}>} The session ID.
319
+ * @memberof Auth
320
+ */
321
+ async function createSessionAndUserToken(user, User, req, res, options = { host: '', path: '' }) {
322
+ const refreshToken = hashToken(generateRandomHex());
323
+ const now = Date.now();
324
+ const expiresAt = new Date(now + parseInt(process.env.REFRESH_EXPIRE_MINUTES) * 60 * 1000);
325
+
326
+ const newSession = {
327
+ tokenHash: refreshToken,
328
+ ip: req.ip,
329
+ userAgent: req.headers['user-agent'],
330
+ host: options.host,
331
+ path: options.path,
332
+ createdAt: new Date(now),
333
+ expiresAt,
334
+ };
335
+
336
+ // push session
337
+ const updatedUser = await User.findByIdAndUpdate(user._id, { $push: { activeSessions: newSession } }, { new: true });
338
+ const session = updatedUser.activeSessions[updatedUser.activeSessions.length - 1];
339
+ const jwtid = session._id.toString();
340
+
341
+ // Secure cookie settings
342
+ res.cookie('refreshToken', refreshToken, {
343
+ httpOnly: true,
344
+ secure: process.env.NODE_ENV === 'production',
345
+ sameSite: 'Lax',
346
+ maxAge: parseInt(process.env.REFRESH_EXPIRE_MINUTES) * 60 * 1000,
347
+ path: '/',
348
+ });
349
+
350
+ return { jwtid };
351
+ }
352
+
353
+ /**
354
+ * Create user and immediate session + access token
355
+ * @param {import('express').Request} req The Express request object.
356
+ * @param {import('express').Response} res The Express response object.
357
+ * @param {import('mongoose').Model} User The Mongoose User model.
358
+ * @param {import('mongoose').Model} File The Mongoose File model.
359
+ * @param {object} [options={}] Additional options.
360
+ * @param {Function} options.getDefaultProfileImageId Function to get the default profile image ID.
361
+ * @param {string} options.host The host name.
362
+ * @param {string} options.path The path name.
363
+ * @returns {Promise<{token: string, user: object}>} The access token and user object.
364
+ * @throws {Error} If password validation fails.
365
+ * @memberof Auth
366
+ */
367
+ async function createUserAndSession(req, res, User, File, options = { host: '', path: '' }) {
368
+ const pwdCheck = validatePasswordMiddleware(req);
369
+ if (pwdCheck.status === 'error') throw new Error(pwdCheck.message);
370
+
371
+ req.body.password = await hashPassword(req.body.password);
372
+ req.body.role = req.body.role === 'guest' ? 'guest' : 'user';
373
+ req.body.profileImageId = await options.getDefaultProfileImageId(File);
374
+
375
+ const saved = await new User(req.body).save();
376
+ const user = await User.findOne({ _id: saved._id }).select(UserDto.select.get());
377
+
378
+ const { jwtid } = await createSessionAndUserToken(user, User, req, res, options);
379
+ const token = jwtSign(
380
+ UserDto.auth.payload(user, jwtid, req.ip, req.headers['user-agent'], options.host, options.path),
381
+ options,
382
+ );
383
+ return { token, user };
384
+ }
385
+
386
+ /**
387
+ * Refresh session and rotate refresh token.
388
+ * Detect token reuse: if a refresh token is presented but not found, consider
389
+ * it a possible theft and revoke all sessions for that user.
390
+ * @param {import('express').Request} req The Express request object.
391
+ * @param {import('express').Response} res The Express response object.
392
+ * @param {import('mongoose').Model} User The Mongoose User model.
393
+ * @param {object} options Additional options.
394
+ * @param {string} options.host The host name.
395
+ * @param {string} options.path The path name.
396
+ * @returns {Promise<{token: string}>} The new access token.
397
+ * @throws {Error} If the refresh token is missing, invalid, or expired.
398
+ * @memberof Auth
399
+ */
400
+ async function refreshSessionAndToken(req, res, User, options = { host: '', path: '' }) {
401
+ const currentRefreshToken = req.cookies.refreshToken;
402
+ if (!currentRefreshToken) throw new Error('Refresh token missing');
403
+
404
+ // Find user owning that token
405
+ const user = await User.findOne({ 'activeSessions.tokenHash': currentRefreshToken });
406
+
407
+ if (!user) {
408
+ // Possible token reuse: look up user by some other signals? If not possible, log and throw.
409
+ logger.warn('Refresh token reuse or invalid token detected');
410
+ // Optional: revoke by clearing cookie and returning unauthorized
411
+ res.clearCookie('refreshToken', { path: '/' });
412
+ throw new Error('Invalid refresh token');
413
+ }
414
+
415
+ // Locate session
416
+ const session = user.activeSessions.find((s) => s.tokenHash === currentRefreshToken);
417
+ if (!session) {
418
+ // Shouldn't happen, but safe-guard
419
+ res.clearCookie('refreshToken', { path: '/' });
420
+ throw new Error('Session not found');
421
+ }
422
+
423
+ // Check expiry
424
+ if (session.expiresAt && session.expiresAt < new Date()) {
425
+ // remove expired session
426
+ user.activeSessions.id(session._id).remove();
427
+ await user.save({ validateBeforeSave: false });
428
+ res.clearCookie('refreshToken', { path: '/' });
429
+ throw new Error('Refresh token expired');
430
+ }
431
+
432
+ // Rotate: generate new token, update stored hash and metadata
433
+ const refreshToken = hashToken(generateRandomHex());
434
+ session.tokenHash = refreshToken;
435
+ session.expiresAt = new Date(Date.now() + parseInt(process.env.REFRESH_EXPIRE_MINUTES) * 60 * 1000);
436
+ session.ip = req.ip;
437
+ session.userAgent = req.headers['user-agent'];
438
+ await user.save({ validateBeforeSave: false });
439
+
440
+ logger.warn('Refreshed session for user ' + user.email);
441
+
442
+ res.cookie('refreshToken', refreshToken, {
443
+ httpOnly: true,
444
+ secure: process.env.NODE_ENV === 'production',
445
+ sameSite: 'Lax',
446
+ maxAge: parseInt(process.env.REFRESH_EXPIRE_MINUTES) * 60 * 1000,
447
+ path: '/',
448
+ });
449
+
450
+ return jwtSign(
451
+ UserDto.auth.payload(user, session._id.toString(), req.ip, req.headers['user-agent'], options.host, options.path),
452
+ options,
453
+ );
454
+ }
455
+
456
+ // ---------- Security middleware composition ----------
457
+ /**
458
+ * Applies a set of security-related middleware to an Express app.
459
+ * @param {import('express').Application} app The Express application.
460
+ * @param {object} [opts={}] Options for security middleware.
461
+ * @param {string[]} opts.origin Allowed origins for CORS.
462
+ * @param {object} opts.rate Rate limiting options for `express-rate-limit`.
463
+ * @param {object} opts.slowdown Slow down options for `express-slow-down`.
464
+ * @param {object} opts.cookie Cookie options.
465
+ * @param {string[]} opts.frameAncestors Allowed frame ancestors for CSP.
466
+ * @memberof Auth
467
+ */
468
+ function applySecurity(app, opts = {}) {
469
+ const {
470
+ origin,
471
+ rate = { windowMs: 15 * 60 * 1000, max: 500 },
472
+ slowdown = { windowMs: 15 * 60 * 1000, delayAfter: 50, delayMs: () => 500 },
473
+ cookie = { secure: process.env.NODE_ENV === 'production', sameSite: 'Strict' },
474
+ frameAncestors = ["'self'"],
475
+ } = opts;
476
+
477
+ app.disable('x-powered-by');
478
+
479
+ // Generate nonce per request and attach to res.locals for templates
480
+ app.use((req, res, next) => {
481
+ res.locals.nonce = crypto.randomBytes(16).toString('base64');
482
+ next();
483
+ });
484
+
485
+ // Basic header hardening with Helmet
486
+ app.use(
487
+ helmet({
488
+ // We'll customize CSP below because many apps need tailored directives
489
+ crossOriginEmbedderPolicy: true,
490
+ crossOriginOpenerPolicy: { policy: 'same-origin' },
491
+ crossOriginResourcePolicy: { policy: 'same-origin' },
492
+ originAgentCluster: false,
493
+ // Permissions-Policy (formerly Feature-Policy) — limit powerful features
494
+ permissionsPolicy: {
495
+ // example: disable geolocation, camera, microphone, payment
496
+ features: {
497
+ fullscreen: ["'self'"],
498
+ geolocation: [],
499
+ camera: [],
500
+ microphone: [],
501
+ payment: [],
502
+ },
503
+ },
504
+ }),
505
+ );
506
+
507
+ // Strict HSTS (only enable in production over TLS)
508
+ // maxAge in seconds (e.g. 31536000 = 1 year). Use includeSubDomains and preload carefully.
509
+ if (process.env.NODE_ENV === 'production') {
510
+ app.use(
511
+ helmet.hsts({
512
+ maxAge: 31536000,
513
+ includeSubDomains: true,
514
+ preload: true,
515
+ }),
516
+ );
517
+ }
518
+
519
+ // Other helpful Helmet policies
520
+ app.use(helmet.noSniff()); // X-Content-Type-Options: nosniff
521
+ app.use(helmet.frameguard({ action: 'deny' })); // X-Frame-Options: DENY
522
+ app.use(helmet.referrerPolicy({ policy: 'no-referrer-when-downgrade' }));
523
+
524
+ // Content-Security-Policy: include nonce from res.locals
525
+ // Note: We avoid 'unsafe-inline' on script/style. Use nonces or hashes.
526
+ const httpDirective = process.env.NODE_ENV === 'production' ? 'https:' : 'http:';
527
+ app.use(
528
+ helmet.contentSecurityPolicy({
529
+ useDefaults: true,
530
+ directives: {
531
+ defaultSrc: ["'self'"],
532
+ baseUri: ["'self'"],
533
+ blockAllMixedContent: [],
534
+ fontSrc: ["'self'", httpDirective, 'data:'],
535
+ frameAncestors: frameAncestors,
536
+ imgSrc: ["'self'", 'data:', httpDirective],
537
+ objectSrc: ["'none'"],
538
+ // script-src and script-src-elem include dynamic nonce
539
+ scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
540
+ scriptSrcElem: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
541
+ // style-src: avoid 'unsafe-inline' when possible; if you must inline styles,
542
+ // use a nonce for them too (or hash).
543
+ styleSrc: ["'self'", httpDirective, (req, res) => `'nonce-${res.locals.nonce}'`],
544
+ // deny plugins
545
+ objectSrc: ["'none'"],
546
+ },
547
+ }),
548
+ );
549
+
550
+ // CORS - be explicit. In production, pass allowed origin(s) as opts.origin
551
+ app.use(
552
+ cors({
553
+ origin: origin || false,
554
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
555
+ credentials: true,
556
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'withcredentials', 'credentials'],
557
+ maxAge: 600,
558
+ }),
559
+ );
560
+
561
+ // Rate limiting + slow down
562
+ const limiter = rateLimit({
563
+ windowMs: rate.windowMs,
564
+ max: rate.max,
565
+ standardHeaders: true,
566
+ legacyHeaders: false,
567
+ message: { error: 'Too many requests, please try again later.' },
568
+ });
569
+ app.use(limiter);
570
+
571
+ const speedLimiter = slowDown({
572
+ windowMs: slowdown.windowMs,
573
+ delayAfter: slowdown.delayAfter,
574
+ delayMs: () => slowdown.delayMs,
575
+ });
576
+ app.use(speedLimiter);
577
+
578
+ // Cookie parsing
579
+ app.use(cookieParser(process.env.JWT_SECRET));
580
+ }
581
+
224
582
  export {
225
- authMiddleware,
583
+ authMiddlewareFactory,
226
584
  hashPassword,
227
585
  verifyPassword,
228
- hashJWT,
586
+ hashToken,
587
+ jwtSign,
588
+ jwtVerify,
589
+ jwtSign as hashJWT,
590
+ jwtVerify as verifyJWT,
229
591
  adminGuard,
230
592
  moderatorGuard,
231
- verifyJWT,
232
593
  validatePasswordMiddleware,
233
594
  getBearerToken,
234
- getPayloadJWT,
595
+ createSessionAndUserToken,
596
+ createUserAndSession,
597
+ refreshSessionAndToken,
598
+ applySecurity,
235
599
  };