underpost 2.8.882 → 2.8.884

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.
@@ -1,123 +1,252 @@
1
+ /**
2
+ * Exported singleton instance of the LamppService class.
3
+ * This object is used to interact with the Lampp configuration and service.
4
+ * @module src/runtime/lampp/Lampp.js
5
+ * @namespace LamppService
6
+ */
7
+
1
8
  import fs from 'fs-extra';
2
9
  import { getRootDirectory, shellCd, shellExec } from '../../server/process.js';
3
10
  import { loggerFactory } from '../../server/logger.js';
4
11
 
5
12
  const logger = loggerFactory(import.meta);
6
13
 
7
- const Lampp = {
8
- ports: [],
9
- initService: async function (options = { daemon: false }) {
14
+ /**
15
+ * @class LamppService
16
+ * @description Provides utilities for managing the XAMPP (Lampp) service on Linux,
17
+ * including initialization, router configuration, and virtual host creation.
18
+ * It manages the server's configuration files and controls the service process.
19
+ * @memberof LamppService
20
+ */
21
+ class LamppService {
22
+ /**
23
+ * @private
24
+ * @type {string | undefined}
25
+ * @description Stores the accumulated Apache virtual host configuration (router definition).
26
+ * @memberof LamppService
27
+ */
28
+ router;
29
+
30
+ /**
31
+ * @public
32
+ * @type {number[]}
33
+ * @description A list of ports currently configured and listened to by the Lampp service.
34
+ * @memberof LamppService
35
+ */
36
+ ports;
37
+
38
+ /**
39
+ * Creates an instance of LamppService.
40
+ * Initializes the router configuration and ports list.
41
+ * @method constructor
42
+ * @memberof LamppService
43
+ */
44
+ constructor() {
45
+ this.router = undefined;
46
+ this.ports = [];
47
+ }
48
+
49
+ /**
50
+ * Checks if the XAMPP (Lampp) service appears to be installed based on the presence of its main configuration file.
51
+ *
52
+ * @method enabled
53
+ * @memberof LamppService
54
+ * @returns {boolean} True if the configuration file exists, indicating Lampp is likely installed.
55
+ * @throws {Error} If the configuration file does not exist.
56
+ * @memberof LamppService
57
+ */
58
+ enabled() {
59
+ return fs.existsSync('/opt/lampp/etc/httpd.conf');
60
+ }
61
+
62
+ /**
63
+ * Initializes or restarts the Lampp Apache service.
64
+ * This method configures virtual hosts, disables default ports (80/443) in the main config
65
+ * to avoid conflicts, and starts or stops the service using shell commands.
66
+ *
67
+ * @method initService
68
+ * @param {object} [options={daemon: false}] - Options for service initialization.
69
+ * @param {boolean} [options.daemon=false] - Flag to indicate if the service should be run as a daemon (currently unused in logic).
70
+ * @returns {Promise<void>}
71
+ * @memberof LamppService
72
+ */
73
+ async initService(options = { daemon: false }) {
10
74
  let cmd;
11
- // linux
12
- // /opt/lampp/apache2/conf/httpd.conf
75
+
76
+ // 1. Write the current virtual host router configuration
13
77
  fs.writeFileSync(`/opt/lampp/etc/extra/httpd-vhosts.conf`, this.router || '', 'utf8');
78
+
79
+ // 2. Ensure the vhosts file is included in the main httpd.conf
80
+ const httpdConfPath = `/opt/lampp/etc/httpd.conf`;
14
81
  fs.writeFileSync(
15
- `/opt/lampp/etc/httpd.conf`,
82
+ httpdConfPath,
16
83
  fs
17
- .readFileSync(`/opt/lampp/etc/httpd.conf`, 'utf8')
84
+ .readFileSync(httpdConfPath, 'utf8')
18
85
  .replace(`#Include etc/extra/httpd-vhosts.conf`, `Include etc/extra/httpd-vhosts.conf`),
19
86
  'utf8',
20
87
  );
21
88
 
89
+ // 3. Stop the service before making port changes
22
90
  cmd = `sudo /opt/lampp/lampp stop`;
23
- if (!fs.readFileSync(`/opt/lampp/etc/httpd.conf`, 'utf8').match(`# Listen 80`))
91
+ shellExec(cmd);
92
+
93
+ // 4. Comment out default port Listen directives (80 and 443) to prevent conflicts
94
+ // Modify httpd.conf (port 80)
95
+ if (!fs.readFileSync(httpdConfPath, 'utf8').match(/# Listen 80/))
24
96
  fs.writeFileSync(
25
- `/opt/lampp/etc/httpd.conf`,
26
- fs.readFileSync(`/opt/lampp/etc/httpd.conf`, 'utf8').replace(`Listen 80`, `# Listen 80`),
97
+ httpdConfPath,
98
+ fs.readFileSync(httpdConfPath, 'utf8').replace(`Listen 80`, `# Listen 80`),
27
99
  'utf8',
28
100
  );
29
- if (!fs.readFileSync(`/opt/lampp/etc/extra/httpd-ssl.conf`, 'utf8').match(`# Listen 443`))
101
+
102
+ // Modify httpd-ssl.conf (port 443)
103
+ const httpdSslConfPath = `/opt/lampp/etc/extra/httpd-ssl.conf`;
104
+ if (fs.existsSync(httpdSslConfPath) && !fs.readFileSync(httpdSslConfPath, 'utf8').match(/# Listen 443/))
30
105
  fs.writeFileSync(
31
- `/opt/lampp/etc/extra/httpd-ssl.conf`,
32
- fs.readFileSync(`/opt/lampp/etc/extra/httpd-ssl.conf`, 'utf8').replace(`Listen 443`, `# Listen 443`),
106
+ httpdSslConfPath,
107
+ fs.readFileSync(httpdSslConfPath, 'utf8').replace(`Listen 443`, `# Listen 443`),
33
108
  'utf8',
34
109
  );
35
- if (!fs.readFileSync(`/opt/lampp/lampp`, 'utf8').match(`testport 443 && false`))
110
+
111
+ // 5. Modify the lampp startup script to bypass port checking for 80 and 443
112
+ const lamppScriptPath = `/opt/lampp/lampp`;
113
+ if (!fs.readFileSync(lamppScriptPath, 'utf8').match(/testport 443 && false/))
36
114
  fs.writeFileSync(
37
- `/opt/lampp/lampp`,
38
- fs.readFileSync(`/opt/lampp/lampp`, 'utf8').replace(`testport 443`, `testport 443 && false`),
115
+ lamppScriptPath,
116
+ fs.readFileSync(lamppScriptPath, 'utf8').replace(`testport 443`, `testport 443 && false`),
39
117
  'utf8',
40
118
  );
41
- if (!fs.readFileSync(`/opt/lampp/lampp`, 'utf8').match(`testport 80 && false`))
119
+ if (!fs.readFileSync(lamppScriptPath, 'utf8').match(/testport 80 && false/))
42
120
  fs.writeFileSync(
43
- `/opt/lampp/lampp`,
44
- fs.readFileSync(`/opt/lampp/lampp`, 'utf8').replace(`testport 80`, `testport 80 && false`),
121
+ lamppScriptPath,
122
+ fs.readFileSync(lamppScriptPath, 'utf8').replace(`testport 80`, `testport 80 && false`),
45
123
  'utf8',
46
124
  );
47
125
 
48
- shellExec(cmd);
126
+ // 6. Start the service
49
127
  cmd = `sudo /opt/lampp/lampp start`;
50
128
  if (this.router) fs.writeFileSync(`./tmp/lampp-router.conf`, this.router, 'utf-8');
51
129
  shellExec(cmd);
52
- },
53
- enabled: () => fs.existsSync(`/opt/lampp/etc/httpd.conf`),
54
- appendRouter: function (render) {
130
+ }
131
+
132
+ /**
133
+ * Appends new Apache VirtualHost configuration content to the internal router string.
134
+ * If a router config file exists from a previous run, it loads it first.
135
+ *
136
+ * @method appendRouter
137
+ * @param {string} render - The new VirtualHost configuration string to append.
138
+ * @returns {string} The complete, updated router configuration string.
139
+ * @memberof LamppService
140
+ */
141
+ appendRouter(render) {
55
142
  if (!this.router) {
56
- if (fs.existsSync(`./tmp/lampp-router.conf`))
57
- return (this.router = fs.readFileSync(`./tmp/lampp-router.conf`, 'utf-8')) + render;
143
+ if (fs.existsSync(`./tmp/lampp-router.conf`)) {
144
+ this.router = fs.readFileSync(`./tmp/lampp-router.conf`, 'utf-8');
145
+ return this.router + render;
146
+ }
58
147
  return (this.router = render);
59
148
  }
60
149
  return (this.router += render);
61
- },
62
- removeRouter: function () {
150
+ }
151
+
152
+ /**
153
+ * Resets the internal router configuration and removes the temporary configuration file.
154
+ *
155
+ * @memberof LamppService
156
+ * @returns {void}
157
+ */
158
+ removeRouter() {
63
159
  this.router = undefined;
64
160
  if (fs.existsSync(`./tmp/lampp-router.conf`)) fs.rmSync(`./tmp/lampp-router.conf`);
65
- },
66
- install: async function () {
67
- switch (process.platform) {
68
- case 'linux':
69
- {
70
- if (!fs.existsSync(`./engine-private/setup`)) fs.mkdirSync(`./engine-private/setup`, { recursive: true });
71
-
72
- shellCd(`./engine-private/setup`);
73
-
74
- if (!process.argv.includes(`server`)) {
75
- shellExec(
76
- `curl -Lo xampp-linux-installer.run https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/7.4.30/xampp-linux-x64-7.4.30-1-installer.run?from_af=true`,
77
- );
78
- shellExec(`sudo chmod +x xampp-linux-installer.run`);
79
- shellExec(
80
- `sudo ./xampp-linux-installer.run --mode unattended && \\` +
81
- `ln -sf /opt/lampp/lampp /usr/bin/lampp && \\` +
82
- `sed -i.bak s'/Require local/Require all granted/g' /opt/lampp/etc/extra/httpd-xampp.conf && \\` +
83
- `sed -i.bak s'/display_errors=Off/display_errors=On/g' /opt/lampp/etc/php.ini && \\` +
84
- `mkdir /opt/lampp/apache2/conf.d && \\` +
85
- `echo "IncludeOptional /opt/lampp/apache2/conf.d/*.conf" >> /opt/lampp/etc/httpd.conf && \\` +
86
- `mkdir /www && \\` +
87
- `ln -s /www /opt/lampp/htdocs`,
88
- );
89
- }
90
-
91
- if (fs.existsSync(`/opt/lampp/logs/access_log`))
92
- fs.copySync(`/opt/lampp/logs/access_log`, `/opt/lampp/logs/access.log`);
93
- if (fs.existsSync(`/opt/lampp/logs/error_log`))
94
- fs.copySync(`/opt/lampp/logs/error_log`, `/opt/lampp/logs/error.log`);
95
- if (fs.existsSync(`/opt/lampp/logs/php_error_log`))
96
- fs.copySync(`/opt/lampp/logs/php_error_log`, `/opt/lampp/logs/php_error.log`);
97
- if (fs.existsSync(`/opt/lampp/logs/ssl_request_log`))
98
- fs.copySync(`/opt/lampp/logs/ssl_request_log`, `/opt/lampp/logs/ssl_request.log`);
99
-
100
- await Lampp.initService({ daemon: true });
101
- }
102
-
103
- break;
104
-
105
- default:
106
- break;
161
+ }
162
+
163
+ /**
164
+ * Installs and configures the Lampp service on Linux.
165
+ * This includes downloading the installer, running it, and setting up initial configurations.
166
+ * Only runs on the 'linux' platform.
167
+ *
168
+ * @method install
169
+ * @returns {Promise<void>}
170
+ * @memberof LamppService
171
+ */
172
+ async install() {
173
+ if (process.platform === 'linux') {
174
+ if (!fs.existsSync(`./engine-private/setup`)) fs.mkdirSync(`./engine-private/setup`, { recursive: true });
175
+
176
+ shellCd(`./engine-private/setup`);
177
+
178
+ if (!process.argv.includes(`server`)) {
179
+ // Download and run the XAMPP installer
180
+ shellExec(
181
+ `curl -Lo xampp-linux-installer.run https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/7.4.30/xampp-linux-x64-7.4.30-1-installer.run?from_af=true`,
182
+ );
183
+ shellExec(`sudo chmod +x xampp-linux-installer.run`);
184
+ shellExec(
185
+ `sudo ./xampp-linux-installer.run --mode unattended && \\` +
186
+ // Create symlink for easier access
187
+ `ln -sf /opt/lampp/lampp /usr/bin/lampp && \\` +
188
+ // Allow all access to xampp config (security measure override)
189
+ `sed -i.bak s'/Require local/Require all granted/g' /opt/lampp/etc/extra/httpd-xampp.conf && \\` +
190
+ // Enable display errors in PHP
191
+ `sed -i.bak s'/display_errors=Off/display_errors=On/g' /opt/lampp/etc/php.ini && \\` +
192
+ // Allow including custom Apache configuration files
193
+ `mkdir /opt/lampp/apache2/conf.d && \\` +
194
+ `echo "IncludeOptional /opt/lampp/apache2/conf.d/*.conf" >> /opt/lampp/etc/httpd.conf && \\` +
195
+ // Create /www directory and symlink it to htdocs
196
+ `mkdir /www && \\` +
197
+ `ln -s /www /opt/lampp/htdocs`,
198
+ );
199
+ }
200
+
201
+ // Copy log files to standard names for easier consumption
202
+ if (fs.existsSync(`/opt/lampp/logs/access_log`))
203
+ fs.copySync(`/opt/lampp/logs/access_log`, `/opt/lampp/logs/access.log`);
204
+ if (fs.existsSync(`/opt/lampp/logs/error_log`))
205
+ fs.copySync(`/opt/lampp/logs/error_log`, `/opt/lampp/logs/error.log`);
206
+ if (fs.existsSync(`/opt/lampp/logs/php_error_log`))
207
+ fs.copySync(`/opt/lampp/logs/php_error_log`, `/opt/lampp/logs/php_error.log`);
208
+ if (fs.existsSync(`/opt/lampp/logs/ssl_request_log`))
209
+ fs.copySync(`/opt/lampp/logs/ssl_request_log`, `/opt/lampp/logs/ssl_request.log`);
210
+
211
+ // Initialize the service after installation
212
+ await this.initService({ daemon: true });
107
213
  }
108
- },
109
- createApp: async ({ port, host, path, directory, rootHostPath, redirect, redirectTarget, resetRouter }) => {
110
- if (!Lampp.enabled()) return { disabled: true };
111
- if (!Lampp.ports.includes(port)) Lampp.ports.push(port);
112
- if (resetRouter) Lampp.removeRouter();
113
- Lampp.appendRouter(`
214
+ }
215
+
216
+ /**
217
+ * Creates and appends a new Apache VirtualHost entry to the router configuration for a web application.
218
+ * The router is then applied by calling {@link LamppService#initService}.
219
+ *
220
+ * @method createApp
221
+ * @param {object} options - Configuration options for the new web application.
222
+ * @param {number} options.port - The port the VirtualHost should listen on.
223
+ * @param {string} options.host - The ServerName/host for the VirtualHost.
224
+ * @param {string} options.path - The base path for error documents (e.g., '/app').
225
+ * @param {string} [options.directory] - Optional absolute path to the document root.
226
+ * @param {string} [options.rootHostPath] - Relative path from the root directory to the document root, used if `directory` is not provided.
227
+ * @param {boolean} [options.redirect] - If true, enables RewriteEngine for redirection.
228
+ * @param {string} [options.redirectTarget] - The target URL for redirection.
229
+ * @param {boolean} [options.resetRouter] - If true, clears the existing router configuration before appending the new one.
230
+ * @returns {{disabled: boolean}} An object indicating if the service is disabled.
231
+ * @memberof LamppService
232
+ */
233
+ createApp({ port, host, path, directory, rootHostPath, redirect, redirectTarget, resetRouter }) {
234
+ if (!this.enabled()) return { disabled: true };
235
+
236
+ if (!this.ports.includes(port)) this.ports.push(port);
237
+ if (resetRouter) this.removeRouter();
238
+
239
+ const documentRoot = directory ? directory : `${getRootDirectory()}${rootHostPath}`;
240
+
241
+ // Append the new VirtualHost configuration
242
+ this.appendRouter(`
114
243
  Listen ${port}
115
244
 
116
245
  <VirtualHost *:${port}>
117
- DocumentRoot "${directory ? directory : `${getRootDirectory()}${rootHostPath}`}"
246
+ DocumentRoot "${documentRoot}"
118
247
  ServerName ${host}:${port}
119
248
 
120
- <Directory "${directory ? directory : `${getRootDirectory()}${rootHostPath}`}">
249
+ <Directory "${documentRoot}">
121
250
  Options Indexes FollowSymLinks MultiViews
122
251
  AllowOverride All
123
252
  Require all granted
@@ -128,12 +257,14 @@ Listen ${port}
128
257
  ? `
129
258
  RewriteEngine on
130
259
 
260
+ # Exclude the ACME challenge path for certificate renewals
131
261
  RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge
132
262
  RewriteRule ^(.*)$ ${redirectTarget}%{REQUEST_URI} [R=302,L]
133
263
  `
134
264
  : ''
135
265
  }
136
266
 
267
+ # Custom Error Documents
137
268
  ErrorDocument 400 ${path === '/' ? '' : path}/400.html
138
269
  ErrorDocument 404 ${path === '/' ? '' : path}/400.html
139
270
  ErrorDocument 500 ${path === '/' ? '' : path}/500.html
@@ -144,56 +275,69 @@ Listen ${port}
144
275
  </VirtualHost>
145
276
 
146
277
  `);
147
- // ERR too many redirects:
148
- // Check: SELECT * FROM database.wp_options where option_name = 'siteurl' or option_name = 'home';
149
- // Check: wp-config.php
150
- // if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
151
- // $_SERVER['HTTPS'] = 'on';
152
- // }
153
- // For plugins:
154
- // define( 'FS_METHOD', 'direct' );
155
-
156
- // ErrorDocument 404 /custom_404.html
157
- // ErrorDocument 500 /custom_50x.html
158
- // ErrorDocument 502 /custom_50x.html
159
- // ErrorDocument 503 /custom_50x.html
160
- // ErrorDocument 504 /custom_50x.html
161
-
162
- // Respond When Error Pages are Directly Requested
163
-
164
- // <Files "custom_404.html">
165
- // <If "-z %{ENV:REDIRECT_STATUS}">
166
- // RedirectMatch 404 ^/custom_404.html$
167
- // </If>
168
- // </Files>
169
-
170
- // <Files "custom_50x.html">
171
- // <If "-z %{ENV:REDIRECT_STATUS}">
172
- // RedirectMatch 404 ^/custom_50x.html$
173
- // </If>
174
- // </Files>
175
-
176
- // Add www or https with htaccess rewrite
177
-
178
- // Options +FollowSymLinks
179
- // RewriteEngine On
180
- // RewriteCond %{HTTP_HOST} ^ejemplo.com [NC]
181
- // RewriteRule ^(.*)$ http://ejemplo.com/$1 [R=301,L]
182
-
183
- // Redirect http to https with htaccess rewrite
184
-
185
- // RewriteEngine On
186
- // RewriteCond %{SERVER_PORT} 80
187
- // RewriteRule ^(.*)$ https://www.ejemplo.com/$1 [R,L]
188
-
189
- // Redirect to HTTPS with www subdomain
190
-
191
- // RewriteEngine On
192
- // RewriteCond %{HTTPS} off [OR]
193
- // RewriteCond %{HTTP_HOST} ^www\. [NC]
194
- // RewriteCond %{HTTP_HOST} ^(?:www\.)?(.+)$ [NC]
195
- // RewriteRule ^ https://%1%{REQUEST_URI} [L,NE,R=301]
196
- },
197
- };
278
+
279
+ return { disabled: false };
280
+ }
281
+ }
282
+
283
+ /**
284
+ * @description Exported singleton instance of the LamppService class.
285
+ * This object is used to interact with the Lampp configuration and service.
286
+ * @memberof LamppService
287
+ * @type {LamppService}
288
+ */
289
+ const Lampp = new LamppService();
198
290
 
199
291
  export { Lampp };
292
+
293
+ // -- helper info --
294
+
295
+ // ERR too many redirects:
296
+ // Check: SELECT * FROM database.wp_options where option_name = 'siteurl' or option_name = 'home';
297
+ // Check: wp-config.php
298
+ // if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
299
+ // $_SERVER['HTTPS'] = 'on';
300
+ // }
301
+ // For plugins:
302
+ // define( 'FS_METHOD', 'direct' );
303
+
304
+ // ErrorDocument 404 /custom_404.html
305
+ // ErrorDocument 500 /custom_50x.html
306
+ // ErrorDocument 502 /custom_50x.html
307
+ // ErrorDocument 503 /custom_50x.html
308
+ // ErrorDocument 504 /custom_50x.html
309
+
310
+ // Respond When Error Pages are Directly Requested
311
+
312
+ // <Files "custom_404.html">
313
+ // <If "-z %{ENV:REDIRECT_STATUS}">
314
+ // RedirectMatch 404 ^/custom_404.html$
315
+ // </If>
316
+ // </Files>
317
+
318
+ // <Files "custom_50x.html">
319
+ // <If "-z %{ENV:REDIRECT_STATUS}">
320
+ // RedirectMatch 404 ^/custom_50x.html$
321
+ // </If>
322
+ // </Files>
323
+
324
+ // Add www or https with htaccess rewrite
325
+
326
+ // Options +FollowSymLinks
327
+ // RewriteEngine On
328
+ // RewriteCond %{HTTP_HOST} ^example.com [NC]
329
+ // RewriteRule ^(.*)$ http://example.com/$1 [R=301,L]
330
+
331
+ // Redirect http to https with htaccess rewrite
332
+
333
+ // RewriteEngine On
334
+ // RewriteCond %{SERVER_PORT} 80
335
+ // RewriteRule ^(.*)$ https://www.example.com/$1 [R,L]
336
+
337
+ // Redirect to HTTPS with www subdomain
338
+
339
+ // RewriteEngine On
340
+ // RewriteCond %{HTTPS} off [OR]
341
+ // RewriteCond %{HTTP_HOST} ^www\. [NC]
342
+ // RewriteCond %{HTTP_HOST} ^(?:www\.)?(.+)$ [NC]
343
+ // RewriteRule ^ https://%1%{REQUEST_URI} [L,NE,R=301]
@@ -110,16 +110,22 @@ function jwtIssuerAudienceFactory(options = { host: '', path: '' }) {
110
110
  * @param {object} [options={}] Additional JWT sign options.
111
111
  * @param {string} options.host The host name.
112
112
  * @param {string} options.path The path name.
113
- * @param {number} expireMinutes The token expiration in minutes.
113
+ * @param {number} accessExpireMinutes The access token expiration in minutes.
114
+ * @param {number} refreshExpireMinutes The refresh token expiration in minutes.
114
115
  * @returns {string} The signed JWT.
115
116
  * @throws {Error} If JWT key is not configured.
116
117
  * @memberof Auth
117
118
  */
118
- function jwtSign(payload, options = { host: '', path: '' }, expireMinutes = process.env.ACCESS_EXPIRE_MINUTES) {
119
+ function jwtSign(
120
+ payload,
121
+ options = { host: '', path: '' },
122
+ accessExpireMinutes = process.env.ACCESS_EXPIRE_MINUTES,
123
+ refreshExpireMinutes = process.env.REFRESH_EXPIRE_MINUTES,
124
+ ) {
119
125
  const { issuer, audience } = jwtIssuerAudienceFactory(options);
120
126
  const signOptions = {
121
127
  algorithm: config.jwtAlgorithm,
122
- expiresIn: `${expireMinutes}m`,
128
+ expiresIn: `${accessExpireMinutes}m`,
123
129
  issuer,
124
130
  audience,
125
131
  };
@@ -128,7 +134,11 @@ function jwtSign(payload, options = { host: '', path: '' }, expireMinutes = proc
128
134
 
129
135
  if (!process.env.JWT_SECRET) throw new Error('JWT key not configured');
130
136
 
131
- logger.info('JWT signed', { payload, signOptions, expireMinutes });
137
+ // Add refreshExpiresAt to the payload, which is the same as the token's own expiry.
138
+ const now = Date.now();
139
+ payload.refreshExpiresAt = now + refreshExpireMinutes * 60 * 1000;
140
+
141
+ logger.info('JWT signed', { payload, signOptions, accessExpireMinutes, refreshExpireMinutes });
132
142
 
133
143
  return jwt.sign(payload, process.env.JWT_SECRET, signOptions);
134
144
  }
@@ -170,6 +180,14 @@ const getBearerToken = (req) => {
170
180
  return '';
171
181
  };
172
182
 
183
+ /**
184
+ * Checks if the request is a refresh token request.
185
+ * @param {import('express').Request} req The Express request object.
186
+ * @returns {boolean} True if the request is a refresh token request, false otherwise.
187
+ * @memberof Auth
188
+ */
189
+ const isRefreshTokenReq = (req) => req.method === 'GET' && req.params.id === 'auth';
190
+
173
191
  // ---------- Middleware ----------
174
192
  /**
175
193
  * Creates a middleware to authenticate requests using a JWT Bearer token.
@@ -237,10 +255,7 @@ const authMiddlewareFactory = (options = { host: '', path: '' }) => {
237
255
  return res.status(401).json({ status: 'error', message: 'unauthorized: host or path mismatch' });
238
256
  }
239
257
 
240
- // check session expiresAt
241
- const isRefreshTokenReq = req.method === 'GET' && req.params.id === 'auth';
242
-
243
- if (!isRefreshTokenReq && session.expiresAt < new Date()) {
258
+ if (!isRefreshTokenReq(req) && session.expiresAt < new Date()) {
244
259
  return res.status(401).json({ status: 'error', message: 'unauthorized: session expired' });
245
260
  }
246
261
  }
@@ -342,11 +357,11 @@ const cookieOptionsFactory = (req) => {
342
357
  const reqIsSecure = Boolean(req.secure || forwardedProto.split(',')[0] === 'https');
343
358
 
344
359
  // secure must be true for SameSite=None to work across sites
345
- const secure = isProduction ? reqIsSecure : false;
360
+ const secure = isProduction ? reqIsSecure : req.protocol === 'https';
346
361
  const sameSite = secure ? 'None' : 'Lax';
347
362
 
348
363
  // Safe parse of maxAge minutes
349
- const minutes = Number.parseInt(process.env.REFRESH_EXPIRE_MINUTES, 10);
364
+ const minutes = Number.parseInt(process.env.ACCESS_EXPIRE_MINUTES, 10);
350
365
  const maxAge = Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : undefined;
351
366
 
352
367
  const opts = {
@@ -399,6 +414,48 @@ async function createSessionAndUserToken(user, User, req, res, options = { host:
399
414
  return { jwtid };
400
415
  }
401
416
 
417
+ /**
418
+ * Removes a session by its ID for a given user.
419
+ * @param {import('mongoose').Model} User The Mongoose User model.
420
+ * @param {string} userId The ID of the user.
421
+ * @param {string} sessionId The ID of the session to remove.
422
+ * @returns {Promise<void>}
423
+ * @memberof Auth
424
+ */
425
+ async function removeSession(User, userId, sessionId) {
426
+ return await User.updateOne({ _id: userId }, { $pull: { activeSessions: { _id: sessionId } } });
427
+ }
428
+
429
+ /**
430
+ * Logs out a user session by removing it from the database and clearing the refresh token cookie.
431
+ * @param {import('mongoose').Model} User The Mongoose User model.
432
+ * @param {import('express').Request} req The Express request object.
433
+ * @param {import('express').Response} res The Express response object.
434
+ * @returns {Promise<boolean>} True if a session was found and removed, false otherwise.
435
+ * @memberof Auth
436
+ */
437
+ async function logoutSession(User, req, res) {
438
+ const refreshToken = req.cookies?.refreshToken;
439
+
440
+ if (!refreshToken) {
441
+ return false;
442
+ }
443
+
444
+ const user = await User.findOne({ 'activeSessions.tokenHash': refreshToken });
445
+
446
+ if (!user) {
447
+ return false;
448
+ }
449
+
450
+ const session = user.activeSessions.find((s) => s.tokenHash === refreshToken);
451
+
452
+ const result = await removeSession(User, user._id, session._id);
453
+
454
+ res.clearCookie('refreshToken', { path: '/' });
455
+
456
+ return result.modifiedCount > 0;
457
+ }
458
+
402
459
  /**
403
460
  * Create user and immediate session + access token
404
461
  * @param {import('express').Request} req The Express request object.
@@ -470,12 +527,10 @@ async function refreshSessionAndToken(req, res, User, options = { host: '', path
470
527
  }
471
528
 
472
529
  // Check expiry
473
- if (session.expiresAt && session.expiresAt < new Date()) {
474
- // remove expired session
475
- user.activeSessions.id(session._id).remove();
476
- await user.save({ validateBeforeSave: false });
477
- res.clearCookie('refreshToken', { path: '/' });
478
- throw new Error('Refresh token expired');
530
+ if (!isRefreshTokenReq(req) && session.expiresAt && session.expiresAt < new Date()) {
531
+ const result = await removeSession(User, user._id, session._id);
532
+ if (result) throw new Error('Refresh token expired');
533
+ else throw new Error('Session not found');
479
534
  }
480
535
 
481
536
  // Rotate: generate new token, update stored hash and metadata
@@ -646,5 +701,8 @@ export {
646
701
  createSessionAndUserToken,
647
702
  createUserAndSession,
648
703
  refreshSessionAndToken,
704
+ logoutSession,
705
+ removeSession,
649
706
  applySecurity,
707
+ isRefreshTokenReq,
650
708
  };