4 * Here we extend the base API server to define our routes and any route-specific
5 * behavior (middlewares) before handing off to the manager.
8 const path
= require('path');
9 const { Dingus
} = require('@squeep/api-dingus');
10 const common
= require('./common');
11 const Manager
= require('./manager');
12 const { Authenticator
, ResourceAuthenticator
, SessionManager
} = require('@squeep/authentication-module');
13 const { initContext
, navLinks
} = require('./template/template-helper');
14 const Enum
= require('./enum');
15 const { ResponseError
} = require('./errors');
17 const _fileScope
= common
.fileScope(__filename
);
20 * @typedef {import('node:http')} http
23 class Service
extends Dingus
{
24 constructor(logger
, db
, options
, asyncLocalStorage
) {
27 ignoreTrailingSlash: false,
29 this.options
= options
;
30 this.asyncLocalStorage
= asyncLocalStorage
;
31 this.staticPath
= path
.normalize(path
.join(__dirname
, '..', 'static'));
32 this.manager
= new Manager(logger
, db
, options
);
33 this.authenticator
= new Authenticator(logger
, db
, options
);
34 this.sessionManager
= new SessionManager(logger
, this.authenticator
, options
);
35 this.resourceAuthenticator
= new ResourceAuthenticator(logger
, db
, options
);
36 this.loginPath
= `${options.dingus.proxyPrefix}/admin/login`;
38 // N.B. /admin routes not currently configurable
39 const route
= (r
) => `/${options.route[r]}`; // eslint-disable-line security/detect-object-injection
42 this.on(['GET'], route('metadata'), this.handlerGetMeta
.bind(this));
43 // Also respond with metadata on well-known oauth2 endpoint if base has no prefix
44 if ((options
?.dingus
?.selfBaseUrl
?.match(/\//g) || []).length
=== 3) {
45 this.on(['GET'], '/.well-known/oauth-authorization-server', this.handlerGetMeta
.bind(this));
49 this.on(['GET'], route('authorization'), this.handlerGetAuthorization
.bind(this));
50 this.on(['POST'], route('authorization'), this.handlerPostAuthorization
.bind(this));
51 this.on(['POST'], route('consent'), this.handlerPostConsent
.bind(this));
52 this.on(['POST'], route('revocation'), this.handlerPostRevocation
.bind(this));
53 this.on(['POST'], route('ticket'), this.handlerPostTicket
.bind(this));
54 this.on(['POST'], route('token'), this.handlerPostToken
.bind(this));
57 this.on('POST', route('introspection'), this.handlerPostIntrospection
.bind(this));
58 this.on('POST', route('userinfo'), this.handlerPostUserInfo
.bind(this));
60 // Information page about service
61 this.on(['GET'], '/', this.handlerGetRoot
.bind(this));
63 // Temmporary to see what rando payload someone is sending us unsolicited
64 this.on(['POST'], '/', this.handlerWhaGwan
.bind(this));
66 // Give load-balancers something to check
67 this.on(['GET'], route('healthcheck'), this.handlerGetHealthcheck
.bind(this));
69 // These routes are intended for accessing static content during development.
70 // In production, a proxy server would likely handle these first.
71 this.on(['GET'], '/static', this.handlerRedirect
.bind(this), `${options.dingus.proxyPrefix}/static/`);
72 this.on(['GET'], '/static/', this.handlerGetStaticFile
.bind(this), 'index.html');
73 this.on(['GET'], '/static/:file', this.handlerGetStaticFile
.bind(this));
74 this.on(['GET'], '/favicon.ico', this.handlerGetStaticFile
.bind(this), 'favicon.ico');
75 this.on(['GET'], '/robots.txt', this.handlerGetStaticFile
.bind(this), 'robots.txt');
77 // Profile and token management for authenticated sessions
78 this.on(['GET'], '/admin', this.handlerRedirect
.bind(this), `${options.dingus.proxyPrefix}/admin/`);
79 this.on(['GET'], '/admin/', this.handlerGetAdmin
.bind(this));
80 this.on(['POST'], '/admin/', this.handlerPostAdmin
.bind(this));
82 // Ticket-proffering interface for authenticated sessions
83 this.on(['GET'], '/admin/ticket', this.handlerGetAdminTicket
.bind(this));
84 this.on(['POST'], '/admin/ticket', this.handlerPostAdminTicket
.bind(this));
86 // User authentication and session establishment
87 this.on(['GET'], '/admin/login', this.handlerGetAdminLogin
.bind(this));
88 this.on(['POST'], '/admin/login', this.handlerPostAdminLogin
.bind(this));
89 this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout
.bind(this));
90 this.on(['GET'], '/admin/settings', this.handlerGetAdminSettings
.bind(this));
91 this.on(['POST'], '/admin/settings', this.handlerPostAdminSettings
.bind(this));
93 // Page for upkeep info et cetera
94 this.on(['GET'], '/admin/maintenance', this.handlerGetAdminMaintenance
.bind(this));
100 * Perform any async startup tasks.
103 await
this.manager
.initialize();
108 * Do a little more on each request.
109 * @param {http.IncomingMessage} req request
110 * @param {http.ServerResponse} res response
111 * @param {object} ctx context
113 async
preHandler(req
, res
, ctx
) {
114 const _scope
= _fileScope('preHandler');
116 await
super.preHandler(req
, res
, ctx
);
117 ctx
.url
= req
.url
; // Persist this for logout redirect
119 const logObject
= this.asyncLocalStorage
.getStore();
120 // istanbul ignore else
121 if (logObject
) { // Debugging in vscode seems to kill ALS, work around
122 logObject
.requestId
= ctx
.requestId
;
123 delete ctx
.requestId
;
125 this.logger
.debug(_scope
, 'no async local store');
131 * @param {http.IncomingMessage} req request
132 * @param {http.ServerResponse} res response
133 * @param {object} ctx context
135 async
handlerGetAdminLogin(req
, res
, ctx
) {
136 const _scope
= _fileScope('handlerGetAdminLogin');
137 this.logger
.debug(_scope
, 'called', { req
, ctx
});
139 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
141 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
143 await
this.sessionManager
.getAdminLogin(res
, ctx
, navLinks
);
148 * @param {http.IncomingMessage} req request
149 * @param {http.ServerResponse} res response
150 * @param {object} ctx context
152 async
handlerPostAdminLogin(req
, res
, ctx
) {
153 const _scope
= _fileScope('handlerPostAdminLogin');
154 this.logger
.debug(_scope
, 'called', { req
, ctx
});
158 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
160 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
162 await
this.ingestBody(req
, res
, ctx
);
164 await
this.sessionManager
.postAdminLogin(res
, ctx
, navLinks
);
169 * @param {http.IncomingMessage} req request
170 * @param {http.ServerResponse} res response
171 * @param {object} ctx context
173 async
handlerGetAdminSettings(req
, res
, ctx
) {
174 const _scope
= _fileScope('handlerGetAdminSettings');
175 this.logger
.debug(_scope
, 'called', { req
, ctx
});
179 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
181 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
)) {
182 await
this.sessionManager
.getAdminSettings(res
, ctx
, navLinks
);
188 * @param {http.IncomingMessage} req request
189 * @param {http.ServerResponse} res response
190 * @param {object} ctx context
192 async
handlerPostAdminSettings(req
, res
, ctx
) {
193 const _scope
= _fileScope('handlerPostAdminSettings');
194 this.logger
.debug(_scope
, 'called', { req
, ctx
});
198 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
200 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
)) {
201 await
this.ingestBody(req
, res
, ctx
);
202 await
this.sessionManager
.postAdminSettings(res
, ctx
, navLinks
);
208 * @param {http.IncomingMessage} req request
209 * @param {http.ServerResponse} res response
210 * @param {object} ctx context
212 async
handlerGetAdminLogout(req
, res
, ctx
) {
213 const _scope
= _fileScope('handlerGetAdminLogout');
214 this.logger
.debug(_scope
, 'called', { req
, ctx
});
218 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
220 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
222 await
this.sessionManager
.getAdminLogout(res
, ctx
);
227 * @param {http.IncomingMessage} req request
228 * @param {http.ServerResponse} res response
229 * @param {object} ctx context
231 async
handlerGetAdmin(req
, res
, ctx
) {
232 const _scope
= _fileScope('handlerGetAdmin');
233 this.logger
.debug(_scope
, 'called', { req
, ctx
});
237 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
239 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
240 await
this.manager
.getAdmin(res
, ctx
);
246 * @param {http.IncomingMessage} req request
247 * @param {http.ServerResponse} res response
248 * @param {object} ctx context
250 async
handlerPostAdmin(req
, res
, ctx
) {
251 const _scope
= _fileScope('handlerPostAdmin');
252 this.logger
.debug(_scope
, 'called', { req
, ctx
});
256 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
258 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
259 await
this.ingestBody(req
, res
, ctx
);
260 await
this.manager
.postAdmin(res
, ctx
);
266 * @param {http.IncomingMessage} req request
267 * @param {http.ServerResponse} res response
268 * @param {object} ctx context
270 async
handlerGetAdminTicket(req
, res
, ctx
) {
271 const _scope
= _fileScope('handlerGetAdminTicket');
272 this.logger
.debug(_scope
, 'called', { req
, ctx
});
276 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
278 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
279 await
this.manager
.getAdminTicket(res
, ctx
);
285 * @param {http.IncomingMessage} req request
286 * @param {http.ServerResponse} res response
287 * @param {object} ctx context
289 async
handlerPostAdminTicket(req
, res
, ctx
) {
290 const _scope
= _fileScope('handlerPostAdminTicket');
291 this.logger
.debug(_scope
, 'called', { req
, ctx
});
295 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
297 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
298 await
this.ingestBody(req
, res
, ctx
);
299 await
this.manager
.postAdminTicket(res
, ctx
);
305 * @param {http.IncomingMessage} req request
306 * @param {http.ServerResponse} res response
307 * @param {object} ctx context
309 async
handlerGetMeta(req
, res
, ctx
) {
310 const _scope
= _fileScope('handlerGetMeta');
311 this.logger
.debug(_scope
, 'called', { req
, ctx
});
313 const responseTypes
= [
314 Enum
.ContentType
.ApplicationJson
,
315 Enum
.ContentType
.TextPlain
,
318 this.setResponseType(responseTypes
, req
, res
, ctx
);
320 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
322 await
this.manager
.getMeta(res
, ctx
);
327 * @param {http.IncomingMessage} req request
328 * @param {http.ServerResponse} res response
329 * @param {object} ctx context
331 async
handlerGetAuthorization(req
, res
, ctx
) {
332 const _scope
= _fileScope('handlerGetAuthorization');
333 this.logger
.debug(_scope
, 'called', { req
, ctx
});
337 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
339 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
340 await
this.manager
.getAuthorization(res
, ctx
);
346 * @param {http.IncomingMessage} req request
347 * @param {http.ServerResponse} res response
348 * @param {object} ctx context
350 async
handlerPostAuthorization(req
, res
, ctx
) {
351 const _scope
= _fileScope('handlerPostAuthorization');
352 this.logger
.debug(_scope
, 'called', { req
, ctx
});
354 const responseTypes
= [
355 Enum
.ContentType
.ApplicationJson
,
356 Enum
.ContentType
.TextPlain
,
359 this.setResponseType(responseTypes
, req
, res
, ctx
);
361 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
363 await
this.ingestBody(req
, res
, ctx
);
365 await
this.manager
.postAuthorization(res
, ctx
);
370 * @param {http.IncomingMessage} req request
371 * @param {http.ServerResponse} res response
372 * @param {object} ctx context
374 async
handlerPostConsent(req
, res
, ctx
) {
375 const _scope
= _fileScope('handlerPostConsent');
376 this.logger
.debug(_scope
, 'called', { req
, ctx
});
380 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
382 // This isn't specified as required as any valid payload carries intrinsic auth data.
383 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
385 await
this.ingestBody(req
, res
, ctx
);
387 await
this.manager
.postConsent(res
, ctx
);
392 * @param {http.IncomingMessage} req request
393 * @param {http.ServerResponse} res response
394 * @param {object} ctx context
396 async
handlerPostTicket(req
, res
, ctx
) {
397 const _scope
= _fileScope('handlerPostTicket');
398 this.logger
.debug(_scope
, 'called', { req
, ctx
});
400 const responseTypes
= [
401 Enum
.ContentType
.ApplicationJson
,
402 Enum
.ContentType
.TextPlain
,
405 this.setResponseType(responseTypes
, req
, res
, ctx
);
407 await
this.ingestBody(req
, res
, ctx
);
409 await
this.manager
.postTicket(req
, res
, ctx
);
414 * @param {http.IncomingMessage} req request
415 * @param {http.ServerResponse} res response
416 * @param {object} ctx context
418 async
handlerPostToken(req
, res
, ctx
) {
419 const _scope
= _fileScope('handlerPostToken');
420 this.logger
.debug(_scope
, 'called', { req
, ctx
});
422 const responseTypes
= [
423 Enum
.ContentType
.ApplicationJson
,
424 Enum
.ContentType
.TextPlain
,
427 this.setResponseType(responseTypes
, req
, res
, ctx
);
429 await
this.ingestBody(req
, res
, ctx
);
431 await
this.manager
.postToken(req
, res
, ctx
);
436 * @param {http.IncomingMessage} req request
437 * @param {http.ServerResponse} res response
438 * @param {object} ctx context
440 async
handlerPostRevocation(req
, res
, ctx
) {
441 const _scope
= _fileScope('handlerPostRevocation');
442 this.logger
.debug(_scope
, 'called', { req
, ctx
});
444 const responseTypes
= [
445 Enum
.ContentType
.ApplicationJson
,
446 Enum
.ContentType
.TextPlain
,
449 this.setResponseType(responseTypes
, req
, res
, ctx
);
451 await
this.ingestBody(req
, res
, ctx
);
453 await
this.manager
.postRevocation(req
, res
, ctx
);
458 * @param {http.IncomingMessage} req request
459 * @param {http.ServerResponse} res response
460 * @param {object} ctx context
462 async
handlerPostIntrospection(req
, res
, ctx
) {
463 const _scope
= _fileScope('handlerPostIntrospection');
464 this.logger
.debug(_scope
, 'called', { req
, ctx
});
466 const responseTypes
= [
467 Enum
.ContentType
.ApplicationJson
,
468 Enum
.ContentType
.TextPlain
,
471 await
this.resourceAuthenticator
.required(req
, res
, ctx
);
473 this.setResponseType(responseTypes
, req
, res
, ctx
);
475 await
this.ingestBody(req
, res
, ctx
);
477 await
this.manager
.postIntrospection(res
, ctx
);
482 * @param {http.IncomingMessage} req request
483 * @param {http.ServerResponse} res response
484 * @param {object} ctx context
486 async
handlerPostUserInfo(req
, res
, ctx
) {
487 const _scope
= _fileScope('handlerPostUserInfo');
488 this.logger
.debug(_scope
, 'called', { req
, ctx
});
490 const responseTypes
= [
491 Enum
.ContentType
.ApplicationJson
,
492 Enum
.ContentType
.TextPlain
,
495 this.setResponseType(responseTypes
, req
, res
, ctx
);
497 await
this.ingestBody(req
, res
, ctx
);
499 await
this.manager
.postUserInfo(res
, ctx
);
504 * @param {http.IncomingMessage} req request
505 * @param {http.ServerResponse} res response
506 * @param {object} ctx context
508 async
handlerGetRoot(req
, res
, ctx
) {
509 const _scope
= _fileScope('handlerGetRoot');
510 const responseTypes
= [
511 Enum
.ContentType
.TextHTML
,
513 this.logger
.debug(_scope
, 'called', { req
, ctx
});
517 this.setResponseType(responseTypes
, req
, res
, ctx
);
519 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
521 await
this.manager
.getRoot(res
, ctx
);
526 * Temporary to see what an unsolicited payload contains.
527 * @param {http.IncomingMessage} req request
528 * @param {http.ServerResponse} res response
529 * @param {object} ctx context
531 async
handlerWhaGwan(req
, res
, ctx
) {
532 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
533 await
this.ingestBody(req
, res
, ctx
);
534 throw new ResponseError(Enum
.ErrorResponse
.MethodNotAllowed
);
538 * @param {http.IncomingMessage} req request
539 * @param {http.ServerResponse} res response
540 * @param {object} ctx context
542 async
handlerGetHealthcheck(req
, res
, ctx
) {
543 const _scope
= _fileScope('handlerGetHealthcheck');
544 this.logger
.debug(_scope
, 'called', { req
, ctx
});
546 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
548 await
this.manager
.getHealthcheck(res
, ctx
);
553 * @param {http.IncomingMessage} req request
554 * @param {http.ServerResponse} res response
555 * @param {object} ctx context
557 async
handlerGetAdminMaintenance(req
, res
, ctx
) {
558 const _scope
= _fileScope('handlerGetAdminMaintenance');
559 this.logger
.debug(_scope
, 'called', { req
, ctx
});
563 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
565 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
566 await
this.manager
.getAdminMaintenance(res
, ctx
);
572 * FIXME: This doesn't seem to be working as envisioned. Maybe override render error method instead???
573 * Intercept this and redirect if we have enough information, otherwise default to framework.
574 * Fixing this will likely have to wait until an e2e test framework is in place.
575 * The redirect attempt should probably be contained in a Manager method, but here it is for now.
576 * @param {http.IncomingMessage} req request
577 * @param {http.ServerResponse} res response
578 * @param {object} ctx context
580 async
handlerInternalServerError(req
, res
, ctx
) {
581 const _scope
= _fileScope('handlerInternalServerError');
582 this.logger
.debug(_scope
, 'called', { req
, ctx
});
584 if (ctx
?.session
?.redirectUri
&& ctx
?.session
?.clientIdentifier
) {
586 ...(ctx
.session
.state
&& { 'state': ctx
.session
.state
}),
587 'error': 'server_error',
588 'error_description': 'An internal server error occurred',
589 }).forEach(([name
, value
]) => ctx
.session
.redirectUri
.searchParams
.set(name
, value
));
590 res
.statusCode
= 302; // Found
591 res
.setHeader(Enum
.Header
.Location
, ctx
.session
.redirectUri
.href
);
596 await
super.handlerInternalServerError(req
, res
, ctx
);
602 module
.exports
= Service
;