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 { Dingus
} = require('@squeep/api-dingus');
9 const common
= require('./common');
10 const Enum
= require('./enum');
11 const Manager
= require('./manager');
12 const { Authenticator
, SessionManager
} = require('@squeep/authentication-module');
13 const { initContext
, navLinks
} = require('./template/template-helper');
14 const path
= require('path');
16 const _fileScope
= common
.fileScope(__filename
);
18 class Service
extends Dingus
{
19 constructor(logger
, db
, options
, asyncLocalStorage
) {
22 ignoreTrailingSlash: false,
24 this.asyncLocalStorage
= asyncLocalStorage
;
25 this.manager
= new Manager(logger
, db
, options
);
26 this.authenticator
= new Authenticator(logger
, db
, options
);
27 this.sessionManager
= new SessionManager(logger
, this.authenticator
, options
);
28 this.staticPath
= path
.join(__dirname
, '..', 'static');
29 this.loginPath
= `${options.dingus.proxyPrefix}/admin/login`;
31 // Primary API endpoint
32 this.on('POST', '/', this.handlerPostRoot
.bind(this));
34 // Information page about service
35 this.on(['GET'], '/', this.handlerGetRoot
.bind(this));
37 // Give load-balancers something to check
38 this.on(['GET'], '/healthcheck', this.handlerGetHealthcheck
.bind(this));
40 // Public information about topics
41 this.on('GET', '/info', this.handlerGetInfo
.bind(this));
42 this.on('GET', '/info/', this.handlerGetInfo
.bind(this));
44 // These routes are intended for accessing static content during development.
45 // In production, a proxy server would likely handle these first.
46 this.on(['GET'], '/static', this.handlerRedirect
.bind(this), `${options.dingus.proxyPrefix}/static/`);
47 this.on(['GET'], '/static/', this.handlerGetStaticFile
.bind(this), 'index.html');
48 this.on(['GET'], '/static/:file', this.handlerGetStaticFile
.bind(this));
49 this.on(['GET'], '/favicon.ico', this.handlerGetStaticFile
.bind(this), 'favicon.ico');
50 this.on(['GET'], '/robots.txt', this.handlerGetStaticFile
.bind(this), 'robots.txt');
52 // Private informational endpoints
53 this.on(['GET'], '/admin', this.handlerRedirect
.bind(this), `${options.dingus.proxyPrefix}/admin/`);
54 this.on(['GET'], '/admin/', this.handlerGetAdminOverview
.bind(this));
55 this.on(['GET'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails
.bind(this));
56 this.on(['GET'], '/admin/topic/:topicId/history.svg', this.handlerGetHistorySVG
.bind(this));
58 // Private data-editing endpoints
59 this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic
.bind(this));
60 this.on(['PATCH', 'DELETE'], '/admin/subscription/:subscriptionId', this.handlerUpdateSubscription
.bind(this));
62 // Private server-action endpoints
63 this.on('POST', '/admin/process', this.handlerPostAdminProcess
.bind(this));
66 this.on(['GET'], '/admin/login', this.handlerGetAdminLogin
.bind(this));
67 this.on(['POST'], '/admin/login', this.handlerPostAdminLogin
.bind(this));
68 this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout
.bind(this));
69 this.on(['GET'], '/admin/_ia', this.handlerGetAdminIA
.bind(this));
70 this.on(['GET'], '/admin/settings', this.handlerGetAdminSettings
.bind(this));
71 this.on(['POST'], '/admin/settings', this.handlerPostAdminSettings
.bind(this));
76 * @typedef {import('node:http')} http
80 * Rearrange logging data.
81 * @param {http.ClientRequest} req request
82 * @param {http.ServerResponse} res response
83 * @param {object} ctx context
85 async
preHandler(req
, res
, ctx
) {
86 await
super.preHandler(req
, res
, ctx
);
87 ctx
.url
= req
.url
; // Persisted for logout redirect
89 const logObject
= this.asyncLocalStorage
.getStore();
90 // FIXME: for some reason, returning from the super.preHandler sometimes loses async context?
91 // Workaround until cause and solution are found.
93 logObject
.requestId
= ctx
.requestId
;
96 this.logger
.debug(_fileScope('preHandler'), 'lost async context', { req
, ctx
});
102 * @param {http.ClientRequest} req request
103 * @param {http.ServerResponse} res response
104 * @param {object} ctx context
106 async
handlerPostRoot(req
, res
, ctx
) {
107 const _scope
= _fileScope('handlerPostRoot');
108 this.logger
.debug(_scope
, 'called', { req
, ctx
});
112 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
113 await
this.ingestBody(req
, res
, ctx
);
115 await
this.manager
.postRoot(req
, res
, ctx
);
120 * @param {http.ClientRequest} req request
121 * @param {http.ServerResponse} res response
122 * @param {object} ctx context
124 async
handlerGetRoot(req
, res
, ctx
) {
125 const _scope
= _fileScope('handlerGetRoot');
126 const responseTypes
= [
127 Enum
.ContentType
.TextHTML
,
129 this.logger
.debug(_scope
, 'called', { req
, ctx
});
133 this.setResponseType(responseTypes
, req
, res
, ctx
);
135 await
this.authenticator
.sessionOptional(req
, res
, ctx
, this.loginPath
);
137 await
this.manager
.getRoot(req
, res
, ctx
);
142 * @param {http.ClientRequest} req request
143 * @param {http.ServerResponse} res response
144 * @param {object} ctx context
146 async
handlerGetHealthcheck(req
, res
, ctx
) {
147 const _scope
= _fileScope('handlerGetHealthcheck');
148 this.logger
.debug(_scope
, 'called', { req
, ctx
});
150 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
152 await
this.manager
.getHealthcheck(res
, ctx
);
157 * @param {http.ClientRequest} req request
158 * @param {http.ServerResponse} res response
159 * @param {object} ctx context
161 async
handlerGetInfo(req
, res
, ctx
) {
162 const _scope
= _fileScope('handlerGetInfo');
163 this.logger
.debug(_scope
, 'called', { req
, ctx
});
165 const responseTypes
= [...this.responseTypes
, Enum
.ContentType
.ImageSVG
];
167 this.setResponseType(responseTypes
, req
, res
, ctx
);
169 await
this.manager
.getInfo(res
, ctx
);
173 async
handlerGetHistorySVG(req
, res
, ctx
) {
174 const _scope
= _fileScope('handlerGetHist');
175 this.logger
.debug(_scope
, 'called', { req
, ctx
});
177 const responseTypes
= [Enum
.ContentType
.ImageSVG
];
179 this.setResponseType(responseTypes
, req
, res
, ctx
);
181 await
this.manager
.getHistorySVG(res
, ctx
);
186 * @param {http.ClientRequest} req request
187 * @param {http.ServerResponse} res response
188 * @param {object} ctx context
190 async
handlerGetAdminOverview(req
, res
, ctx
) {
191 const _scope
= _fileScope('handlerGetAdminOverview');
192 this.logger
.debug(_scope
, 'called', { req
, ctx
});
196 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
198 if (await
this.authenticator
.sessionRequired(req
, res
, ctx
, this.loginPath
)) {
199 await
this.manager
.getAdminOverview(res
, ctx
);
205 * @param {http.ClientRequest} req request
206 * @param {http.ServerResponse} res response
207 * @param {object} ctx context
209 async
handlerGetAdminTopicDetails(req
, res
, ctx
) {
210 const _scope
= _fileScope('handlerGetAdminTopicDetails');
211 this.logger
.debug(_scope
, 'called', { req
, ctx
});
215 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
217 if (await
this.authenticator
.sessionRequired(req
, res
, ctx
, this.loginPath
)) {
218 await
this.manager
.getTopicDetails(res
, ctx
);
224 * If no body was sent, do not parse (and thus avoid possible unsupported media type error).
225 * @param {http.ClientRequest} req request
226 * @param {http.ServerResponse} res response
227 * @param {object} ctx context
228 * @returns {Promise<object>} parsed body
230 async
maybeIngestBody(req
, res
, ctx
) {
231 return super.ingestBody(req
, res
, ctx
, {
232 parseEmptyBody: false,
238 * @param {http.ClientRequest} req request
239 * @param {http.ServerResponse} res response
240 * @param {object} ctx context
242 async
handlerUpdateTopic(req
, res
, ctx
) {
243 const _scope
= _fileScope('handlerUpdateTopic');
244 this.logger
.debug(_scope
, 'called', { req
, ctx
});
246 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
248 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
250 await
this.maybeIngestBody(req
, res
, ctx
);
251 ctx
.method
= req
.method
;
252 await
this.manager
.updateTopic(res
, ctx
);
257 * @param {http.ClientRequest} req request
258 * @param {http.ServerResponse} res response
259 * @param {object} ctx context
261 async
handlerUpdateSubscription(req
, res
, ctx
) {
262 const _scope
= _fileScope('handlerUpdateSubscription');
263 this.logger
.debug(_scope
, 'called', { req
, ctx
});
265 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
267 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
269 await
this.maybeIngestBody(req
, res
, ctx
);
270 ctx
.method
= req
.method
;
271 await
this.manager
.updateSubscription(res
, ctx
);
276 * @param {http.ClientRequest} req request
277 * @param {http.ServerResponse} res response
278 * @param {object} ctx context
280 async
handlerPostAdminProcess(req
, res
, ctx
) {
281 const _scope
= _fileScope('handlerPostAdminProcess');
282 this.logger
.debug(_scope
, 'called', { req
, ctx
});
284 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
286 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
288 await
this.manager
.processTasks(res
, ctx
);
293 * Delegate login to authentication module.
294 * @param {http.ClientRequest} req request
295 * @param {http.ServerResponse} res response
296 * @param {object} ctx context
298 async
handlerGetAdminLogin(req
, res
, ctx
) {
299 const _scope
= _fileScope('handlerGetAdminLogin');
300 this.logger
.debug(_scope
, 'called', { req
, ctx
});
304 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
306 await
this.sessionManager
.getAdminLogin(res
, ctx
);
311 * Delegate login to authentication module.
312 * @param {http.ClientRequest} req request
313 * @param {http.ServerResponse} res response
314 * @param {object} ctx context
316 async
handlerPostAdminLogin(req
, res
, ctx
) {
317 const _scope
= _fileScope('handlerPostAdminLogin');
318 this.logger
.debug(_scope
, 'called', { req
, ctx
});
322 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
324 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
326 await
this.maybeIngestBody(req
, res
, ctx
);
328 await
this.sessionManager
.postAdminLogin(res
, ctx
);
333 * Delegate account settings to authentication module.
334 * @param {http.ClientRequest} req request
335 * @param {http.ServerResponse} res response
336 * @param {object} ctx context
338 async
handlerGetAdminSettings(req
, res
, ctx
) {
339 const _scope
= _fileScope('handlerGetAdminSettings');
340 this.logger
.debug(_scope
, 'called', { req
, ctx
});
344 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
346 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
)) {
347 await
this.sessionManager
.getAdminSettings(res
, ctx
, navLinks
);
353 * Delegate account settings to authentication module.
354 * @param {http.ClientRequest} req request
355 * @param {http.ServerResponse} res response
356 * @param {object} ctx context
358 async
handlerPostAdminSettings(req
, res
, ctx
) {
359 const _scope
= _fileScope('handlerPostAdminSettings');
360 this.logger
.debug(_scope
, 'called', { req
, ctx
});
364 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
366 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
)) {
367 await
this.maybeIngestBody(req
, res
, ctx
);
368 await
this.sessionManager
.postAdminSettings(res
, ctx
, navLinks
);
374 * Delegate login to authentication module.
375 * @param {http.ClientRequest} req request
376 * @param {http.ServerResponse} res response
377 * @param {object} ctx context
379 async
handlerGetAdminLogout(req
, res
, ctx
) {
380 const _scope
= _fileScope('handlerGetAdminLogout');
381 this.logger
.debug(_scope
, 'called', { req
, ctx
});
385 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
387 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
389 await
this.sessionManager
.getAdminLogout(res
, ctx
);
394 * Delegate login to authentication module.
395 * @param {http.ClientRequest} req request
396 * @param {http.ServerResponse} res response
397 * @param {object} ctx context
399 async
handlerGetAdminIA(req
, res
, ctx
) {
400 const _scope
= _fileScope('handlerGetAdminIA');
401 this.logger
.debug(_scope
, 'called', { req
, ctx
});
405 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
407 await
this.sessionManager
.getAdminIA(res
, ctx
);
412 module
.exports
= Service
;