@ti-engine/web-framework 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +4 -0
- package/CHANGELOG.md +274 -0
- package/README.md +26 -0
- package/bin/build/post-install.js +18 -0
- package/bin/localization/web-server-labels.json +28 -0
- package/bin/static/.well-known/appspecific/com.chrome.devtools.json +6 -0
- package/bin/static/favicon.ico +0 -0
- package/bin/static/fragments/components/component-notification-bar.html +21 -0
- package/bin/static/fragments/components/component-sidebar-flyout.html +37 -0
- package/bin/static/fragments/components/component-sidebar.html +33 -0
- package/bin/static/fragments/components/component-tooltip.html +11 -0
- package/bin/static/fragments/components/component-topbar.html +6 -0
- package/bin/static/fragments/frame-administration.html +3 -0
- package/bin/static/fragments/frame-application.html +19 -0
- package/bin/static/fragments/frame-dashboard.html +3 -0
- package/bin/static/fragments/frame-login.html +105 -0
- package/bin/static/fragments/frame-not-found.html +3 -0
- package/bin/static/fragments/frame-profile.html +3 -0
- package/bin/static/index.html +23 -0
- package/bin/static/scripts/lib/alpinejs-csp.min.js +7 -0
- package/bin/static/scripts/lib/htmx.min.js +1 -0
- package/bin/static/scripts/lib/safe-nonce.min.js +1 -0
- package/bin/static/scripts/ti-charts.js +1591 -0
- package/bin/static/scripts/ti-framework.css +3195 -0
- package/bin/static/scripts/ti-framework.js +1427 -0
- package/bin/static/scripts/ti-theme-black-glass.css +216 -0
- package/bin/static/scripts/ti-theme-daylight.css +87 -0
- package/bin/web-app-manager.js +564 -0
- package/bin/web-server.js +604 -0
- package/bin/web-server.json +49 -0
- package/components/admin-config-handlers.js +92 -0
- package/components/auth-manager.js +344 -0
- package/components/authorization.js +135 -0
- package/components/config-change-notifier.js +98 -0
- package/components/config-registry.js +246 -0
- package/components/config-service.js +349 -0
- package/components/config-store.js +246 -0
- package/components/definitions.types.js +26 -0
- package/components/session-store.js +111 -0
- package/components/user.js +133 -0
- package/components/web-handlers.js +765 -0
- package/package.json +66 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
|
|
3
|
+
* Copyright © 2021-2025 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
|
+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
|
+
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
|
|
10
|
+
const exceptions = require( "@ti-engine/core/exceptions" );
|
|
11
|
+
const logger = require( "@ti-engine/core/logger" );
|
|
12
|
+
const { randomBytes } = require( "node:crypto" );
|
|
13
|
+
const path = require( "node:path" );
|
|
14
|
+
const fs = require( "node:fs" );
|
|
15
|
+
const _ = require( "lodash" );
|
|
16
|
+
const express = require( "express" );
|
|
17
|
+
const helmet = require( "helmet" );
|
|
18
|
+
const session = require( "express-session" );
|
|
19
|
+
const cookieParser = require( "cookie-parser" );
|
|
20
|
+
const webHandlers = require( "#web-handlers" );
|
|
21
|
+
const SessionStore = require( "#session-store" );
|
|
22
|
+
const AuthManager = require( "#auth-manager" );
|
|
23
|
+
const authMethod = require( "#auth-manager" ).authMethod;
|
|
24
|
+
const authorization = require( "#authorization" );
|
|
25
|
+
const adminConfigHandlers = require( "#admin-config-handlers" );
|
|
26
|
+
const configService = require( "#config-service" );
|
|
27
|
+
|
|
28
|
+
/** @typedef {import("node:http").Server} NodeServer */
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {ServiceConfiguration} TiWebServiceConfiguration
|
|
32
|
+
* @property {ApiConfig} api
|
|
33
|
+
* @property {TiWebApplicationConfig} application
|
|
34
|
+
* @property {SettingsAuth} auth
|
|
35
|
+
* @property {SettingsCookies} cookies
|
|
36
|
+
* @property {string} host
|
|
37
|
+
* @property {TiLocalizationLanguage} language
|
|
38
|
+
* @property {number} port
|
|
39
|
+
* @property {string} publicPath
|
|
40
|
+
* @property {number} requestTimeout
|
|
41
|
+
* @property {string} tlsCertPath
|
|
42
|
+
* @property {string} tlsKeyPath
|
|
43
|
+
* @property {boolean} useTLS
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} TiWebApplicationConfig
|
|
48
|
+
* @property {string} classPath
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} ApiConfig
|
|
53
|
+
* @property {boolean} endpointEnabled
|
|
54
|
+
* @property {ApiInventory} inventory
|
|
55
|
+
* @property {number} requestTimeout
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} SettingsAuth
|
|
60
|
+
* @property {string[]} enabledMethods
|
|
61
|
+
* @property {Object} local
|
|
62
|
+
* @property {Object} oauth2
|
|
63
|
+
* @property {SettingsOAuth2Client} [oauth2.azure]
|
|
64
|
+
* @property {SettingsOAuth2Client} [oauth2.google]
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @typedef {Object} SettingsOAuth2Client
|
|
69
|
+
* @property {string} [clientID]
|
|
70
|
+
* @property {string} [clientSecret]
|
|
71
|
+
* @property {string} [callbackUrl]
|
|
72
|
+
* @property {string} [discoveryUrl]
|
|
73
|
+
* @property {boolean} [isPublic]
|
|
74
|
+
* @property {TiTokenEndpointAuthMethod} [tokenEndpointAuthMethod]
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {Object} SettingsCookies
|
|
79
|
+
* @property {string} secret
|
|
80
|
+
* @property {string} path
|
|
81
|
+
* @property {boolean} httpOnly
|
|
82
|
+
* @property {"lax"|"strict"|"none"} sameSite
|
|
83
|
+
* @property {number} maxAge
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @typedef {Record<string, Record<string, ServiceAddress>>} ApiInventory
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
const webServerConfig = require( "#web-server-config" );
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A web server microservice based on the ti-engine.
|
|
94
|
+
* <br/>
|
|
95
|
+
* Note: The web server is fully functional and already comes with all the necessary fundamentals and security features. However, it is designed to be extended
|
|
96
|
+
* with custom logic and functionality to fit your specific needs. Here is a list of methods that you can override to customize the web server behavior:
|
|
97
|
+
* - {@link TiWebServer#defineWebApplicationRoutes} Override this to define custom web application routes. Remember to call the base method if you want to preserve the default behavior as well (recommended).
|
|
98
|
+
* - {@link TiWebServer#defineUnprotectedRoutes} Override this to define unprotected routes. Remember to call the base method if you want to preserve the default behavior as well (recommended).
|
|
99
|
+
* - {@link TiWebServer#verifySession} Override this to implement custom session verification logic.
|
|
100
|
+
*
|
|
101
|
+
* @class TiWebServer
|
|
102
|
+
* @extends ServiceConsumer
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
class TiWebServer extends ServiceConsumer {
|
|
106
|
+
|
|
107
|
+
#webServer;
|
|
108
|
+
#netServer;
|
|
109
|
+
#serverUrl = "";
|
|
110
|
+
#isShuttingDown = false;
|
|
111
|
+
#staticContentPaths = [];
|
|
112
|
+
#allowedHosts = [];
|
|
113
|
+
#unprotectedRoutes = [];
|
|
114
|
+
#webAppManager;
|
|
115
|
+
#authManager;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @constructor
|
|
119
|
+
* @param {string} serviceDomainName The service domain name for this service instance.
|
|
120
|
+
* @param {TiWebServiceConfiguration} serviceConfig The JSON configuration for this service. Note that the configuration provided will be merged with the default web server configuration, and it will override any conflicting properties.
|
|
121
|
+
* @throws {TiException.E_GEN_JS_INTERNAL_ERROR} If the web application manager cannot be loaded.
|
|
122
|
+
*/
|
|
123
|
+
constructor( serviceDomainName, serviceConfig ) {
|
|
124
|
+
super( serviceDomainName, _.merge( {}, webServerConfig, ( _.isObjectLike( serviceConfig ) ) ? serviceConfig : {} ) );
|
|
125
|
+
|
|
126
|
+
// Include the current host in the list of allowed hosts:
|
|
127
|
+
this.#allowedHosts.push( this.serviceConfig.host );
|
|
128
|
+
|
|
129
|
+
// Add the default and custom public paths to the list of static content:
|
|
130
|
+
this.#staticContentPaths.push( path.join( __dirname, "static" ) );
|
|
131
|
+
let customStaticContentPath = path.normalize( path.isAbsolute( this.serviceConfig.publicPath ) ? this.serviceConfig.publicPath : path.join( process.cwd(), this.serviceConfig.publicPath ) );
|
|
132
|
+
if ( fs.existsSync( customStaticContentPath ) === false ) {
|
|
133
|
+
logger.log( `Public path '${ customStaticContentPath }' does not exist. Static routes will resolve with 404 until path is created.`, logger.logSeverity.WARNING );
|
|
134
|
+
} else {
|
|
135
|
+
this.#staticContentPaths.push( customStaticContentPath );
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.#authManager = new AuthManager( this.serviceConfig.auth );
|
|
139
|
+
|
|
140
|
+
// If there is a web application configuration, create the web application manager:
|
|
141
|
+
if ( this.serviceConfig.application ) {
|
|
142
|
+
try {
|
|
143
|
+
const webApplicationConstructor = require( path.join( process.cwd(), this.serviceConfig.application.classPath ) );
|
|
144
|
+
this.#webAppManager = new webApplicationConstructor();
|
|
145
|
+
} catch ( error ) {
|
|
146
|
+
logger.log( `Failed to load web application manager from '${ this.serviceConfig.application.classPath }'`, logger.logSeverity.ERROR, error );
|
|
147
|
+
throw exceptions.raise( error );
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Public interface */
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Property returning the service configuration JSON.
|
|
156
|
+
*
|
|
157
|
+
* @property
|
|
158
|
+
* @returns {TiWebServiceConfiguration}
|
|
159
|
+
* @override
|
|
160
|
+
* @public
|
|
161
|
+
*/
|
|
162
|
+
get serviceConfig() {
|
|
163
|
+
return super.serviceConfig;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Property returning if the web server is currently shutting down.
|
|
168
|
+
*
|
|
169
|
+
* @property
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
173
|
+
get isShuttingDown() {
|
|
174
|
+
return this.#isShuttingDown;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Property returning the list of static content directories.
|
|
179
|
+
*
|
|
180
|
+
* @property
|
|
181
|
+
* @returns {string[]}
|
|
182
|
+
* @public
|
|
183
|
+
*/
|
|
184
|
+
get staticContentPaths() {
|
|
185
|
+
return this.#staticContentPaths;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Property returning the server URL.
|
|
190
|
+
*
|
|
191
|
+
* @property
|
|
192
|
+
* @returns {string}
|
|
193
|
+
* @public
|
|
194
|
+
*/
|
|
195
|
+
get serverUrl() {
|
|
196
|
+
return this.#serverUrl;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Property returning the {@link TiWebAppManager} instance.
|
|
201
|
+
*
|
|
202
|
+
* @property
|
|
203
|
+
* @returns {TiWebAppManager}
|
|
204
|
+
* @public
|
|
205
|
+
*/
|
|
206
|
+
get webAppManager() {
|
|
207
|
+
return this.#webAppManager;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Starts the web server.
|
|
212
|
+
*
|
|
213
|
+
* @method
|
|
214
|
+
* @returns {Promise}
|
|
215
|
+
* @override
|
|
216
|
+
* @public
|
|
217
|
+
*/
|
|
218
|
+
onStart() {
|
|
219
|
+
return new Promise( ( resolve, reject ) => {
|
|
220
|
+
super.onStart().then( () => {
|
|
221
|
+
// Create and configure the web server:
|
|
222
|
+
this.#webServer = express();
|
|
223
|
+
this.#webServer.set( "trust proxy", true );
|
|
224
|
+
|
|
225
|
+
// Create and configure the net server for HTTPS if enabled in the service config:
|
|
226
|
+
let netServerOptions = {};
|
|
227
|
+
const timeoutCandidates = [
|
|
228
|
+
this.serviceConfig.api.requestTimeout,
|
|
229
|
+
this.serviceConfig.requestTimeout
|
|
230
|
+
].filter( ( value ) => Number.isFinite( value ) );
|
|
231
|
+
const resolvedRequestTimeout = timeoutCandidates.length ? Math.max( ...timeoutCandidates ) : undefined;
|
|
232
|
+
if ( this.serviceConfig.useTLS === true ) {
|
|
233
|
+
if ( !this.serviceConfig.tlsKeyPath || !this.serviceConfig.tlsCertPath ) {
|
|
234
|
+
// Abort initialization if there is something wrong with the TLS key or cert paths:
|
|
235
|
+
return reject( exceptions.raise( exceptions.exceptionCode.E_GEN_INVALID_ARGUMENT_TYPE, {
|
|
236
|
+
tlsKeyPath: this.serviceConfig.tlsKeyPath,
|
|
237
|
+
tlsCertPath: this.serviceConfig.tlsCertPath
|
|
238
|
+
}, exceptions.httpCode.C_500 ) );
|
|
239
|
+
}
|
|
240
|
+
netServerOptions.key = fs.readFileSync( path.join( process.cwd(), this.serviceConfig.tlsKeyPath ) );
|
|
241
|
+
netServerOptions.cert = fs.readFileSync( path.join( process.cwd(), this.serviceConfig.tlsCertPath ) );
|
|
242
|
+
|
|
243
|
+
this.#webServer.use( webHandlers.httpRedirectHandler( this ) );
|
|
244
|
+
this.#netServer = require( "node:https" ).createServer( netServerOptions, this.#webServer );
|
|
245
|
+
} else {
|
|
246
|
+
this.#netServer = require( "node:http" ).createServer( netServerOptions, this.#webServer );
|
|
247
|
+
}
|
|
248
|
+
if ( Number.isFinite( resolvedRequestTimeout ) ) {
|
|
249
|
+
this.#netServer.requestTimeout = resolvedRequestTimeout;
|
|
250
|
+
this.#netServer.headersTimeout = resolvedRequestTimeout + 100;
|
|
251
|
+
if ( typeof this.#netServer.keepAliveTimeout === "number" ) {
|
|
252
|
+
this.#netServer.keepAliveTimeout = resolvedRequestTimeout + 1000;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Set up security and session middlewares first:
|
|
257
|
+
this.#webServer.use( webHandlers.nonceGenerationHandler() );
|
|
258
|
+
this.#webServer.use( helmet( { contentSecurityPolicy: false } ) );
|
|
259
|
+
this.#webServer.use( webHandlers.cspHeaderHandler() );
|
|
260
|
+
this.#webServer.use( express.json( { limit: "1mb" } ) );
|
|
261
|
+
this.#webServer.use( express.urlencoded( { extended: false, limit: "100kb" } ) );
|
|
262
|
+
this.#webServer.use( cookieParser() );
|
|
263
|
+
this.#webServer.use( session( {
|
|
264
|
+
secret: this.serviceConfig.cookies.secret || randomBytes( 32 ).toString( "base64" ),
|
|
265
|
+
resave: false,
|
|
266
|
+
saveUninitialized: false,
|
|
267
|
+
cookie: {
|
|
268
|
+
path: this.serviceConfig.cookies.path,
|
|
269
|
+
httpOnly: this.serviceConfig.cookies.httpOnly,
|
|
270
|
+
secure: "auto",
|
|
271
|
+
sameSite: this.serviceConfig.cookies.sameSite,
|
|
272
|
+
maxAge: this.serviceConfig.cookies.maxAge
|
|
273
|
+
},
|
|
274
|
+
unset: "destroy",
|
|
275
|
+
store: new SessionStore()
|
|
276
|
+
} ) );
|
|
277
|
+
this.#webServer.use( webHandlers.csrfInitHandler( this ) );
|
|
278
|
+
this.#webServer.use( webHandlers.originRefererValidationHandler() );
|
|
279
|
+
this.#webServer.use( webHandlers.csrfProtectionHandler() );
|
|
280
|
+
|
|
281
|
+
// Set up the web server routes:
|
|
282
|
+
this.#webServer.use( webHandlers.onShutDownHandler( this ) );
|
|
283
|
+
this.#webServer.use( webHandlers.resourceProtectionHandler( this ) );
|
|
284
|
+
this.#webServer.use( "/.well-known", express.static( path.join( this.#staticContentPaths[ 0 ], ".well-known" ), { dotfiles: "allow" } ) );
|
|
285
|
+
|
|
286
|
+
// Static content routes are registered in reverse order to ensure that custom assets can override the default ones and be served first:
|
|
287
|
+
_.forEachRight( this.#staticContentPaths, ( staticContentPath ) => {
|
|
288
|
+
this.#webServer.use( "/static", express.static( staticContentPath, { maxAge: "1y", immutable: true } ) );
|
|
289
|
+
} );
|
|
290
|
+
|
|
291
|
+
// Set up the web application routes:
|
|
292
|
+
this.defineWebApplicationRoutes();
|
|
293
|
+
|
|
294
|
+
// API service proxy route (protected by auth middleware):
|
|
295
|
+
if ( this.serviceConfig.api.endpointEnabled === true ) {
|
|
296
|
+
this.#webServer.post( "/service/:version/:name", webHandlers.serviceCallHandler( this ) );
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Set up error handling middleware:
|
|
300
|
+
this.#webServer.all( "*splat", webHandlers.invalidRouteHandler() );
|
|
301
|
+
this.#webServer.use( webHandlers.defaultErrorHandler() );
|
|
302
|
+
|
|
303
|
+
// Set up the unprotected routes:
|
|
304
|
+
this.defineUnprotectedRoutes();
|
|
305
|
+
|
|
306
|
+
return this.#authManager.initialize();
|
|
307
|
+
} ).then( () => {
|
|
308
|
+
return this.#beginListening( this.#netServer, this.serviceConfig.port, this.serviceConfig.host );
|
|
309
|
+
} ).then( ( server ) => {
|
|
310
|
+
if ( server.listening === true ) {
|
|
311
|
+
this.#serverUrl = `http${ this.serviceConfig.useTLS === true ? "s" : "" }://${ server.address().address }:${ server.address().port }`;
|
|
312
|
+
logger.log( `Web server started at address '${ this.#serverUrl }' within instance '${ ServiceConsumer.instanceID }'.`, logger.logSeverity.NOTICE );
|
|
313
|
+
} else {
|
|
314
|
+
logger.log( `Web server is not listening for requests after startup within instance '${ ServiceConsumer.instanceID }'.`, logger.logSeverity.WARNING );
|
|
315
|
+
}
|
|
316
|
+
resolve();
|
|
317
|
+
} ).catch( ( error ) => {
|
|
318
|
+
logger.log( `Error while trying to start web server within instance '${ ServiceConsumer.instanceID }'!`, logger.logSeverity.ERROR, error );
|
|
319
|
+
reject( exceptions.raise( error ) );
|
|
320
|
+
} );
|
|
321
|
+
} );
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Shuts down the web server.
|
|
326
|
+
*
|
|
327
|
+
* @method
|
|
328
|
+
* @returns {Promise}
|
|
329
|
+
* @override
|
|
330
|
+
* @public
|
|
331
|
+
*/
|
|
332
|
+
onStop() {
|
|
333
|
+
return new Promise( ( resolve, reject ) => {
|
|
334
|
+
this.#isShuttingDown = true;
|
|
335
|
+
|
|
336
|
+
super.onStop().then( () => {
|
|
337
|
+
return this.#endListening( this.#netServer );
|
|
338
|
+
} ).then( () => {
|
|
339
|
+
logger.log( `Web server stopped successfully.`, logger.logSeverity.NOTICE );
|
|
340
|
+
resolve();
|
|
341
|
+
} ).catch( ( error ) => {
|
|
342
|
+
reject( exceptions.raise( error ) );
|
|
343
|
+
} );
|
|
344
|
+
} );
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Used to report health status of the service instance for external monitoring.
|
|
349
|
+
* This is a scheduled job that will be executed at SERVICE_HEALTH_CHECK_INTERVAL time.
|
|
350
|
+
*
|
|
351
|
+
* @method
|
|
352
|
+
* @override
|
|
353
|
+
* @public
|
|
354
|
+
*/
|
|
355
|
+
reportHealthy() {
|
|
356
|
+
super.reportHealthy();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Used to verify the session of a request.
|
|
361
|
+
*
|
|
362
|
+
* @method
|
|
363
|
+
* @param {TiSession} session
|
|
364
|
+
* @returns {boolean}
|
|
365
|
+
* @public
|
|
366
|
+
*/
|
|
367
|
+
verifySession( session ) {
|
|
368
|
+
// TODO: Implement this!
|
|
369
|
+
return Boolean( session && session.user );
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Hook for the application to augment the freshly-authenticated session (e.g. derive domain roles from an
|
|
374
|
+
* identity store or the org chart). Runs synchronously, once per login, before the framework's additive `admin`
|
|
375
|
+
* role is applied. The default is a no-op. Any test-user role injection is an override of whatever the app derives.
|
|
376
|
+
*
|
|
377
|
+
* @method
|
|
378
|
+
* @virtual
|
|
379
|
+
* @param {TiSession} session
|
|
380
|
+
* @param {Object} [request] Optional Express request object that can be used to read body/cookies/query data.
|
|
381
|
+
* @returns {TiSession}
|
|
382
|
+
* @public
|
|
383
|
+
*/
|
|
384
|
+
augmentSession( session, request ) {
|
|
385
|
+
return session;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Used to authenticate a user via the specified auth method.
|
|
390
|
+
*
|
|
391
|
+
* @method
|
|
392
|
+
* @param {TiAuthMethod} authMethod
|
|
393
|
+
* @param {Object} [authDetails={}]
|
|
394
|
+
* @returns {Promise}
|
|
395
|
+
* @public
|
|
396
|
+
*/
|
|
397
|
+
authenticate( authMethod, authDetails = {} ) {
|
|
398
|
+
return this.#authManager.authenticate( authMethod, authDetails );
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Used to set up user authorization according to the specified auth method.
|
|
403
|
+
*
|
|
404
|
+
* @method
|
|
405
|
+
* @param {TiAuthMethod} authMethod
|
|
406
|
+
* @param {URL} currentUrl
|
|
407
|
+
* @param {Object} oidc
|
|
408
|
+
* @returns {Promise<User>}
|
|
409
|
+
* @public
|
|
410
|
+
*/
|
|
411
|
+
authorize( authMethod, currentUrl, oidc ) {
|
|
412
|
+
return this.#authManager.authorize( authMethod, currentUrl, oidc );
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Used to get a service mapping if such exists.
|
|
417
|
+
*
|
|
418
|
+
* @method
|
|
419
|
+
* @param {string} serviceVersion
|
|
420
|
+
* @param {string} serviceName
|
|
421
|
+
* @returns {ServiceAddress}
|
|
422
|
+
* @public
|
|
423
|
+
*/
|
|
424
|
+
getServiceAddress( serviceVersion, serviceName ) {
|
|
425
|
+
let serviceAddress = undefined;
|
|
426
|
+
if ( this.serviceConfig.api && this.serviceConfig.api.inventory ) {
|
|
427
|
+
serviceAddress = ( this.serviceConfig.api.inventory[ serviceVersion ] ) ? this.serviceConfig.api.inventory[ serviceVersion ][ serviceName ] : undefined;
|
|
428
|
+
}
|
|
429
|
+
return serviceAddress;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Used to check if the specified hostname is allowed to access the web server.
|
|
434
|
+
*
|
|
435
|
+
* @method
|
|
436
|
+
* @param {string} hostname
|
|
437
|
+
* @returns {boolean}
|
|
438
|
+
* @public
|
|
439
|
+
*/
|
|
440
|
+
isAllowedHost( hostname ) {
|
|
441
|
+
return this.#allowedHosts.includes( hostname );
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Used to check if the specified route is unprotected (i.e., does not require authentication). The default unprotected routes are:
|
|
446
|
+
* - /
|
|
447
|
+
* - /static/...
|
|
448
|
+
* - /.well-known/...
|
|
449
|
+
* - /not-found
|
|
450
|
+
* - /app
|
|
451
|
+
* - /app/enter
|
|
452
|
+
* - /app/config
|
|
453
|
+
* - /logout
|
|
454
|
+
* - /login/:method
|
|
455
|
+
* <br/>
|
|
456
|
+
* NOTE: You can define custom unprotected routes by overriding the {@link TiWebServer#defineUnprotectedRoutes} method.
|
|
457
|
+
*
|
|
458
|
+
* @method
|
|
459
|
+
* @param {string} route
|
|
460
|
+
* @returns {boolean}
|
|
461
|
+
* @public
|
|
462
|
+
*/
|
|
463
|
+
isUnprotectedRoute( route ) {
|
|
464
|
+
const pathOnly = String( route || "" ).split( "?" )[ 0 ];
|
|
465
|
+
let result = false;
|
|
466
|
+
for ( let idx = 0; idx < this.#unprotectedRoutes.length; idx++ ) {
|
|
467
|
+
const pattern = this.#unprotectedRoutes[ idx ];
|
|
468
|
+
if ( _.isRegExp( pattern ) ) {
|
|
469
|
+
// Avoid stateful RegExp behavior when 'g' or 'y' flags are present:
|
|
470
|
+
pattern.lastIndex = 0;
|
|
471
|
+
result = pattern.test( pathOnly );
|
|
472
|
+
} else {
|
|
473
|
+
result = ( pattern === pathOnly );
|
|
474
|
+
}
|
|
475
|
+
if ( result ) {
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Used to define the web application routes.
|
|
484
|
+
* <br/>
|
|
485
|
+
* NOTE: Override this to define custom web application routes. Remember to call the base method if you want to preserve the default behavior as well.
|
|
486
|
+
*
|
|
487
|
+
* @method
|
|
488
|
+
* @virtual
|
|
489
|
+
* @public
|
|
490
|
+
*/
|
|
491
|
+
defineWebApplicationRoutes() {
|
|
492
|
+
this.#webServer.get( "/", webHandlers.webAppHandler( this ) );
|
|
493
|
+
this.#webServer.get( "/not-found", webHandlers.webAppHandler( this ) );
|
|
494
|
+
this.#webServer.get( "/app", webHandlers.webAppHandler( this ) );
|
|
495
|
+
this.#webServer.get( "/app/:view", webHandlers.webAppHandler( this ) );
|
|
496
|
+
this.#webServer.post( "/app/:service", webHandlers.webAppHandler( this ) );
|
|
497
|
+
this.#webServer.get( "/login/:method", webHandlers.authenticationHandler( this ) );
|
|
498
|
+
this.#webServer.post( "/login/:method", webHandlers.authenticationHandler( this ) );
|
|
499
|
+
this.#webServer.post( "/logout", webHandlers.logoutHandler() );
|
|
500
|
+
this.#webServer.get( "/me", webHandlers.userInformationHandler() );
|
|
501
|
+
if ( this.#authManager.isAuthEnabled( authMethod.OPENID_GOOGLE ) ) {
|
|
502
|
+
this.#webServer.get( this.#authManager.getOAuth2CallbackUrl( authMethod.OPENID_GOOGLE ), webHandlers.authorizedOAuth2CallbackHandler( this, authMethod.OPENID_GOOGLE ) );
|
|
503
|
+
}
|
|
504
|
+
if ( this.#authManager.isAuthEnabled( authMethod.OPENID_AZURE ) ) {
|
|
505
|
+
this.#webServer.get( this.#authManager.getOAuth2CallbackUrl( authMethod.OPENID_AZURE ), webHandlers.authorizedOAuth2CallbackHandler( this, authMethod.OPENID_AZURE ) );
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Admin configuration-management API. Gated by the admin role; these paths are not in the unprotected-routes
|
|
509
|
+
// list, so they also inherit the server's global authentication + CSRF middleware.
|
|
510
|
+
const requireAdmin = authorization.requireAdmin;
|
|
511
|
+
const service = configService.instance;
|
|
512
|
+
this.#webServer.get( "/admin/config/editors", requireAdmin, adminConfigHandlers.listEditors( service ) );
|
|
513
|
+
this.#webServer.get( "/admin/config/editors/:editorKey", requireAdmin, adminConfigHandlers.composeView( service ) );
|
|
514
|
+
this.#webServer.post( "/admin/config/editors/:editorKey", requireAdmin, adminConfigHandlers.saveEditorEdit( service ) );
|
|
515
|
+
this.#webServer.get( "/admin/config/documents/:configKey", requireAdmin, adminConfigHandlers.getCurrent( service ) );
|
|
516
|
+
this.#webServer.get( "/admin/config/documents/:configKey/history", requireAdmin, adminConfigHandlers.getHistory( service ) );
|
|
517
|
+
this.#webServer.get( "/admin/config/changes", requireAdmin, adminConfigHandlers.listChanges( service ) );
|
|
518
|
+
this.#webServer.get( "/admin/config/changes/:changeSetID", requireAdmin, adminConfigHandlers.getChange( service ) );
|
|
519
|
+
this.#webServer.post( "/admin/config/changes/:changeSetID/restore", requireAdmin, adminConfigHandlers.restoreChangeSet( service ) );
|
|
520
|
+
this.#webServer.get( "/admin/config/export", requireAdmin, adminConfigHandlers.exportBundle( service ) );
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Used to define the unprotected routes (i.e., routes that do not require authentication).
|
|
525
|
+
* <br/>
|
|
526
|
+
* NOTE: Override this to define custom unprotected routes. Remember to call the base method if you want to preserve the default behavior as well.
|
|
527
|
+
*
|
|
528
|
+
* @method
|
|
529
|
+
* @virtual
|
|
530
|
+
* @public
|
|
531
|
+
*/
|
|
532
|
+
defineUnprotectedRoutes() {
|
|
533
|
+
this.#unprotectedRoutes.push( "/" );
|
|
534
|
+
this.#unprotectedRoutes.push( "/not-found" );
|
|
535
|
+
this.#unprotectedRoutes.push( "/app" );
|
|
536
|
+
this.#unprotectedRoutes.push( "/app/enter" );
|
|
537
|
+
this.#unprotectedRoutes.push( "/app/config" );
|
|
538
|
+
this.#unprotectedRoutes.push( /^\/login\/[^/]+$/i );
|
|
539
|
+
this.#unprotectedRoutes.push( "/logout" );
|
|
540
|
+
this.#unprotectedRoutes.push( /^\/static\/(?:.+\/)*[^/]+\.[^/]+$/i );
|
|
541
|
+
this.#unprotectedRoutes.push( /^\/\.well-known\/(?:.+\/)*[^/]+\.[^/]+$/i );
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* Private interface */
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Used to start listening for requests on the specified port and host and on the specified server.
|
|
548
|
+
*
|
|
549
|
+
* @method
|
|
550
|
+
* @param {NodeServer} server The server instance to listen on.
|
|
551
|
+
* @param {number} port The port to listen on.
|
|
552
|
+
* @param {string} host The host to listen on.
|
|
553
|
+
* @returns {Promise<NodeServer>}
|
|
554
|
+
* @private
|
|
555
|
+
*/
|
|
556
|
+
#beginListening( server, port, host ) {
|
|
557
|
+
return new Promise( ( resolve, reject ) => {
|
|
558
|
+
server.once( "error", ( error ) => {
|
|
559
|
+
reject( exceptions.raise( error ) );
|
|
560
|
+
} );
|
|
561
|
+
server.once( "listening", () => {
|
|
562
|
+
resolve( server );
|
|
563
|
+
} );
|
|
564
|
+
server.listen( port, host );
|
|
565
|
+
} );
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Used to stop listening for requests on the specified server.
|
|
570
|
+
*
|
|
571
|
+
* @method
|
|
572
|
+
* @param {NodeServer} server The server instance to stop listening on.
|
|
573
|
+
* @returns {Promise}
|
|
574
|
+
* @private
|
|
575
|
+
*/
|
|
576
|
+
#endListening( server ) {
|
|
577
|
+
return new Promise( ( resolve, reject ) => {
|
|
578
|
+
if ( !server ) {
|
|
579
|
+
resolve();
|
|
580
|
+
} else {
|
|
581
|
+
// Close all connections after a short delay to allow all requests to complete:
|
|
582
|
+
setTimeout( () => {
|
|
583
|
+
if ( typeof server.closeIdleConnections === "function" ) {
|
|
584
|
+
server.closeIdleConnections();
|
|
585
|
+
}
|
|
586
|
+
if ( typeof server.closeAllConnections === "function" ) {
|
|
587
|
+
server.closeAllConnections();
|
|
588
|
+
}
|
|
589
|
+
}, 1000 );
|
|
590
|
+
|
|
591
|
+
server.close( ( error ) => {
|
|
592
|
+
if ( error ) {
|
|
593
|
+
reject( exceptions.raise( error ) );
|
|
594
|
+
} else {
|
|
595
|
+
resolve();
|
|
596
|
+
}
|
|
597
|
+
} );
|
|
598
|
+
}
|
|
599
|
+
} );
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
module.exports = TiWebServer;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"api": {
|
|
3
|
+
"endpointEnabled": true,
|
|
4
|
+
"inventory": {
|
|
5
|
+
"v1": {
|
|
6
|
+
"test-1": {
|
|
7
|
+
"serviceAlias": "service1",
|
|
8
|
+
"serviceDomainName": "ti-tester-service",
|
|
9
|
+
"serviceVersion": 1
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"requestTimeout": 120000
|
|
14
|
+
},
|
|
15
|
+
"auth": {
|
|
16
|
+
"enabledMethods": [
|
|
17
|
+
"local",
|
|
18
|
+
"openid-google"
|
|
19
|
+
],
|
|
20
|
+
"admins": [],
|
|
21
|
+
"oauth2": {
|
|
22
|
+
"azure": {
|
|
23
|
+
"tokenEndpointAuthMethod": "client_secret_post",
|
|
24
|
+
"callbackUrl": "/login/azure-callback",
|
|
25
|
+
"isPublic": false
|
|
26
|
+
},
|
|
27
|
+
"google": {
|
|
28
|
+
"tokenEndpointAuthMethod": "client_secret_post",
|
|
29
|
+
"callbackUrl": "/login/google-callback",
|
|
30
|
+
"discoveryUrl": "https://accounts.google.com/.well-known/openid-configuration",
|
|
31
|
+
"isPublic": false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"cookies": {
|
|
36
|
+
"path": "/",
|
|
37
|
+
"httpOnly": true,
|
|
38
|
+
"sameSite": "lax",
|
|
39
|
+
"maxAge": 604800
|
|
40
|
+
},
|
|
41
|
+
"host": "127.0.0.1",
|
|
42
|
+
"language": "en",
|
|
43
|
+
"port": 3000,
|
|
44
|
+
"publicPath": "bin/static",
|
|
45
|
+
"requestTimeout": 180000,
|
|
46
|
+
"tlsCertPath": "bin/tls/localhost+2.pem",
|
|
47
|
+
"tlsKeyPath": "bin/tls/localhost+2-key.pem",
|
|
48
|
+
"useTLS": true
|
|
49
|
+
}
|