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.
- package/README.md +10 -2
- package/bin/db.js +1 -4
- package/cli.md +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/maas/device-scan.sh +1 -1
- package/package.json +1 -1
- package/src/api/user/user.service.js +3 -10
- package/src/client/components/core/Docs.js +0 -1
- package/src/client/components/core/Modal.js +14 -8
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +262 -0
- package/src/runtime/lampp/Lampp.js +272 -128
- package/src/server/auth.js +74 -16
- package/src/server/runtime.js +54 -230
- package/src/runtime/nginx/Nginx.js +0 -3
- package/src/runtime/xampp/Xampp.js +0 -83
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
//
|
|
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
|
-
|
|
82
|
+
httpdConfPath,
|
|
16
83
|
fs
|
|
17
|
-
.readFileSync(
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
fs.readFileSync(
|
|
97
|
+
httpdConfPath,
|
|
98
|
+
fs.readFileSync(httpdConfPath, 'utf8').replace(`Listen 80`, `# Listen 80`),
|
|
27
99
|
'utf8',
|
|
28
100
|
);
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
fs.readFileSync(
|
|
106
|
+
httpdSslConfPath,
|
|
107
|
+
fs.readFileSync(httpdSslConfPath, 'utf8').replace(`Listen 443`, `# Listen 443`),
|
|
33
108
|
'utf8',
|
|
34
109
|
);
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
fs.readFileSync(
|
|
115
|
+
lamppScriptPath,
|
|
116
|
+
fs.readFileSync(lamppScriptPath, 'utf8').replace(`testport 443`, `testport 443 && false`),
|
|
39
117
|
'utf8',
|
|
40
118
|
);
|
|
41
|
-
if (!fs.readFileSync(
|
|
119
|
+
if (!fs.readFileSync(lamppScriptPath, 'utf8').match(/testport 80 && false/))
|
|
42
120
|
fs.writeFileSync(
|
|
43
|
-
|
|
44
|
-
fs.readFileSync(
|
|
121
|
+
lamppScriptPath,
|
|
122
|
+
fs.readFileSync(lamppScriptPath, 'utf8').replace(`testport 80`, `testport 80 && false`),
|
|
45
123
|
'utf8',
|
|
46
124
|
);
|
|
47
125
|
|
|
48
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 "${
|
|
246
|
+
DocumentRoot "${documentRoot}"
|
|
118
247
|
ServerName ${host}:${port}
|
|
119
248
|
|
|
120
|
-
<Directory "${
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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]
|
package/src/server/auth.js
CHANGED
|
@@ -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}
|
|
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(
|
|
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: `${
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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.
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
};
|