@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.
Files changed (42) hide show
  1. package/.env +4 -0
  2. package/CHANGELOG.md +274 -0
  3. package/README.md +26 -0
  4. package/bin/build/post-install.js +18 -0
  5. package/bin/localization/web-server-labels.json +28 -0
  6. package/bin/static/.well-known/appspecific/com.chrome.devtools.json +6 -0
  7. package/bin/static/favicon.ico +0 -0
  8. package/bin/static/fragments/components/component-notification-bar.html +21 -0
  9. package/bin/static/fragments/components/component-sidebar-flyout.html +37 -0
  10. package/bin/static/fragments/components/component-sidebar.html +33 -0
  11. package/bin/static/fragments/components/component-tooltip.html +11 -0
  12. package/bin/static/fragments/components/component-topbar.html +6 -0
  13. package/bin/static/fragments/frame-administration.html +3 -0
  14. package/bin/static/fragments/frame-application.html +19 -0
  15. package/bin/static/fragments/frame-dashboard.html +3 -0
  16. package/bin/static/fragments/frame-login.html +105 -0
  17. package/bin/static/fragments/frame-not-found.html +3 -0
  18. package/bin/static/fragments/frame-profile.html +3 -0
  19. package/bin/static/index.html +23 -0
  20. package/bin/static/scripts/lib/alpinejs-csp.min.js +7 -0
  21. package/bin/static/scripts/lib/htmx.min.js +1 -0
  22. package/bin/static/scripts/lib/safe-nonce.min.js +1 -0
  23. package/bin/static/scripts/ti-charts.js +1591 -0
  24. package/bin/static/scripts/ti-framework.css +3195 -0
  25. package/bin/static/scripts/ti-framework.js +1427 -0
  26. package/bin/static/scripts/ti-theme-black-glass.css +216 -0
  27. package/bin/static/scripts/ti-theme-daylight.css +87 -0
  28. package/bin/web-app-manager.js +564 -0
  29. package/bin/web-server.js +604 -0
  30. package/bin/web-server.json +49 -0
  31. package/components/admin-config-handlers.js +92 -0
  32. package/components/auth-manager.js +344 -0
  33. package/components/authorization.js +135 -0
  34. package/components/config-change-notifier.js +98 -0
  35. package/components/config-registry.js +246 -0
  36. package/components/config-service.js +349 -0
  37. package/components/config-store.js +246 -0
  38. package/components/definitions.types.js +26 -0
  39. package/components/session-store.js +111 -0
  40. package/components/user.js +133 -0
  41. package/components/web-handlers.js +765 -0
  42. 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
+ }