devdep update, log async context loss
[websub-hub] / src / service.js
1 'use strict';
2
3 /**
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.
6 */
7
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 path = require('path');
14
15 const _fileScope = common.fileScope(__filename);
16
17 class Service extends Dingus {
18 constructor(logger, db, options, asyncLocalStorage) {
19 super(logger, {
20 ...options.dingus,
21 ignoreTrailingSlash: false,
22 });
23 this.asyncLocalStorage = asyncLocalStorage;
24 this.manager = new Manager(logger, db, options);
25 this.authenticator = new Authenticator(logger, db, options);
26 this.sessionManager = new SessionManager(logger, this.authenticator, options);
27 this.staticPath = path.join(__dirname, '..', 'static');
28 this.loginPath = `${options.dingus.proxyPrefix}/admin/login`;
29
30 // Primary API endpoint
31 this.on('POST', '/', this.handlerPostRoot.bind(this));
32
33 // Information page about service
34 this.on(['GET', 'HEAD'], '/', this.handlerGetRoot.bind(this));
35
36 // Give load-balancers something to check
37 this.on(['GET', 'HEAD'], '/healthcheck', this.handlerGetHealthcheck.bind(this));
38
39 // Public information about topics
40 this.on('GET', '/info', this.handlerGetInfo.bind(this));
41 this.on('GET', '/info/', this.handlerGetInfo.bind(this));
42
43 // These routes are intended for accessing static content during development.
44 // In production, a proxy server would likely handle these first.
45 this.on(['GET', 'HEAD'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`);
46 this.on(['GET', 'HEAD'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html');
47 this.on(['GET', 'HEAD'], '/static/:file', this.handlerGetStaticFile.bind(this));
48 this.on(['GET', 'HEAD'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico');
49 this.on(['GET', 'HEAD'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt');
50
51 // Private informational endpoints
52 this.on(['GET', 'HEAD'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
53 this.on(['GET', 'HEAD'], '/admin/', this.handlerGetAdminOverview.bind(this));
54 this.on(['GET', 'HEAD'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this));
55 this.on(['GET', 'HEAD'], '/admin/topic/:topicId/history.svg', this.handlerGetHistorySVG.bind(this));
56
57 // Private data-editing endpoints
58 this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic.bind(this));
59 this.on(['PATCH', 'DELETE'], '/admin/subscription/:subscriptionId', this.handlerUpdateSubscription.bind(this));
60
61 // Private server-action endpoints
62 this.on('POST', '/admin/process', this.handlerPostAdminProcess.bind(this));
63
64 // Admin login
65 this.on(['GET', 'HEAD'], '/admin/login', this.handlerGetAdminLogin.bind(this));
66 this.on(['POST'], '/admin/login', this.handlerPostAdminLogin.bind(this));
67 this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout.bind(this));
68 this.on(['GET'], '/admin/_ia', this.handlerGetAdminIA.bind(this));
69
70 }
71
72
73 /**
74 * Rearrange logging data.
75 * @param {http.ClientRequest} req
76 * @param {http.ServerResponse} res
77 * @param {Object} ctx
78 */
79 async preHandler(req, res, ctx) {
80 await super.preHandler(req, res, ctx);
81 const logObject = this.asyncLocalStorage.getStore();
82 // FIXME: for some reason, returning from the super.preHandler sometimes loses async context?
83 // Workaround until cause and solution are found.
84 if (logObject) {
85 logObject.requestId = ctx.requestId;
86 delete ctx.requestId;
87 } else {
88 this.logger.debug(_fileScope('preHandler'), 'lost async context', { req, ctx });
89 }
90 }
91
92
93 /**
94 * @param {http.ClientRequest} req
95 * @param {http.ServerResponse} res
96 * @param {Object} ctx
97 */
98 async handlerPostRoot(req, res, ctx) {
99 const _scope = _fileScope('handlerPostRoot');
100 this.logger.debug(_scope, 'called', { req, ctx });
101
102 this.setResponseType(this.responseTypes, req, res, ctx);
103 await this.ingestBody(req, res, ctx);
104
105 await this.manager.postRoot(req, res, ctx);
106 }
107
108
109 /**
110 * @param {http.ClientRequest} req
111 * @param {http.ServerResponse} res
112 * @param {Object} ctx
113 */
114 async handlerGetRoot(req, res, ctx) {
115 const _scope = _fileScope('handlerGetRoot');
116 const responseTypes = [
117 Enum.ContentType.TextHTML,
118 ];
119 this.logger.debug(_scope, 'called', { req, ctx });
120
121 Dingus.setHeadHandler(req, res, ctx);
122
123 this.setResponseType(responseTypes, req, res, ctx);
124
125 await this.authenticator.sessionOptional(req, res, ctx, this.loginPath);
126
127 await this.manager.getRoot(req, res, ctx);
128 }
129
130
131 /**
132 * @param {http.ClientRequest} req
133 * @param {http.ServerResponse} res
134 * @param {Object} ctx
135 */
136 async handlerGetHealthcheck(req, res, ctx) {
137 const _scope = _fileScope('handlerGetHealthcheck');
138 this.logger.debug(_scope, 'called', { req, ctx });
139
140 Dingus.setHeadHandler(req, res, ctx);
141
142 this.setResponseType(this.responseTypes, req, res, ctx);
143
144 await this.manager.getHealthcheck(res, ctx);
145 }
146
147
148 /**
149 * @param {http.ClientRequest} req
150 * @param {http.ServerResponse} res
151 * @param {Object} ctx
152 */
153 async handlerGetInfo(req, res, ctx) {
154 const _scope = _fileScope('handlerGetInfo');
155 this.logger.debug(_scope, 'called', { req, ctx });
156
157 const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
158
159 Dingus.setHeadHandler(req, res, ctx);
160
161 this.setResponseType(responseTypes, req, res, ctx);
162
163 await this.manager.getInfo(res, ctx);
164 }
165
166
167 async handlerGetHistorySVG(req, res, ctx) {
168 const _scope = _fileScope('handlerGetHist');
169 this.logger.debug(_scope, 'called', { req, ctx });
170
171 const responseTypes = [Enum.ContentType.ImageSVG];
172
173 Dingus.setHeadHandler(req, res, ctx);
174
175 this.setResponseType(responseTypes, req, res, ctx);
176
177 await this.manager.getHistorySVG(res, ctx);
178 }
179
180
181 /**
182 * @param {http.ClientRequest} req
183 * @param {http.ServerResponse} res
184 * @param {Object} ctx
185 */
186 async handlerGetAdminOverview(req, res, ctx) {
187 const _scope = _fileScope('handlerGetAdminOverview');
188 this.logger.debug(_scope, 'called', { req, ctx });
189
190 Dingus.setHeadHandler(req, res, ctx);
191
192 this.setResponseType(this.responseTypes, req, res, ctx);
193
194 if (await this.authenticator.sessionRequired(req, res, ctx, this.loginPath)) {
195 await this.manager.getAdminOverview(res, ctx);
196 }
197 }
198
199
200 /**
201 * @param {http.ClientRequest} req
202 * @param {http.ServerResponse} res
203 * @param {Object} ctx
204 */
205 async handlerGetAdminTopicDetails(req, res, ctx) {
206 const _scope = _fileScope('handlerGetAdminTopicDetails');
207 this.logger.debug(_scope, 'called', { req, ctx });
208
209 Dingus.setHeadHandler(req, res, ctx);
210
211 this.setResponseType(this.responseTypes, req, res, ctx);
212
213 if (await this.authenticator.sessionRequired(req, res, ctx, this.loginPath)) {
214 await this.manager.getTopicDetails(res, ctx);
215 }
216 }
217
218
219 /**
220 * If no body was sent, do not parse (and thus avoid possible unsupported media type error).
221 * @param {http.ClientRequest} req
222 * @param {http.ServerResponse} res
223 * @param {Object} ctx
224 */
225 async maybeIngestBody(req, res, ctx) {
226 return super.ingestBody(req, res, ctx, {
227 parseEmptyBody: false,
228 });
229 }
230
231
232 /**
233 * @param {http.ClientRequest} req
234 * @param {http.ServerResponse} res
235 * @param {Object} ctx
236 */
237 async handlerUpdateTopic(req, res, ctx) {
238 const _scope = _fileScope('handlerUpdateTopic');
239 this.logger.debug(_scope, 'called', { req, ctx });
240
241 this.setResponseType(this.responseTypes, req, res, ctx);
242
243 await this.authenticator.apiRequiredLocal(req, res, ctx);
244
245 await this.maybeIngestBody(req, res, ctx);
246 ctx.method = req.method;
247 await this.manager.updateTopic(res, ctx);
248 }
249
250
251 /**
252 * @param {http.ClientRequest} req
253 * @param {http.ServerResponse} res
254 * @param {Object} ctx
255 */
256 async handlerUpdateSubscription(req, res, ctx) {
257 const _scope = _fileScope('handlerUpdateSubscription');
258 this.logger.debug(_scope, 'called', { req, ctx });
259
260 this.setResponseType(this.responseTypes, req, res, ctx);
261
262 await this.authenticator.apiRequiredLocal(req, res, ctx);
263
264 await this.maybeIngestBody(req, res, ctx);
265 ctx.method = req.method;
266 await this.manager.updateSubscription(res, ctx);
267 }
268
269
270 /**
271 * @param {http.ClientRequest} req
272 * @param {http.ServerResponse} res
273 * @param {Object} ctx
274 */
275 async handlerPostAdminProcess(req, res, ctx) {
276 const _scope = _fileScope('handlerPostAdminProcess');
277 this.logger.debug(_scope, 'called', { req, ctx });
278
279 this.setResponseType(this.responseTypes, req, res, ctx);
280
281 await this.authenticator.apiRequiredLocal(req, res, ctx);
282
283 await this.manager.processTasks(res, ctx);
284 }
285
286
287 /**
288 * Delegate login to authentication module.
289 * @param {http.ClientRequest} req
290 * @param {http.ServerResponse} res
291 * @param {Object} ctx
292 */
293 async handlerGetAdminLogin(req, res, ctx) {
294 const _scope = _fileScope('handlerGetAdminLogin');
295 this.logger.debug(_scope, 'called', { req, ctx });
296
297 Dingus.setHeadHandler(req, res, ctx);
298
299 this.setResponseType(this.responseTypes, req, res, ctx);
300
301 await this.sessionManager.getAdminLogin(res, ctx);
302 }
303
304
305 /**
306 * Delegate login to authentication module.
307 * @param {http.ClientRequest} req
308 * @param {http.ServerResponse} res
309 * @param {Object} ctx
310 */
311 async handlerPostAdminLogin(req, res, ctx) {
312 const _scope = _fileScope('handlerPostAdminLogin');
313 this.logger.debug(_scope, 'called', { req, ctx });
314
315 this.setResponseType(this.responseTypes, req, res, ctx);
316
317 await this.authenticator.sessionOptionalLocal(req, res, ctx);
318
319 await this.maybeIngestBody(req, res, ctx);
320
321 await this.sessionManager.postAdminLogin(res, ctx);
322 }
323
324
325 /**
326 * Delegate login to authentication module.
327 * @param {http.ClientRequest} req
328 * @param {http.ServerResponse} res
329 * @param {Object} ctx
330 */
331 async handlerGetAdminLogout(req, res, ctx) {
332 const _scope = _fileScope('handlerGetAdminLogout');
333 this.logger.debug(_scope, 'called', { req, ctx });
334
335 this.setResponseType(this.responseTypes, req, res, ctx);
336
337 await this.authenticator.sessionOptionalLocal(req, res, ctx);
338
339 await this.sessionManager.getAdminLogout(res, ctx);
340 }
341
342
343 /**
344 * Delegate login to authentication module.
345 * @param {http.ClientRequest} req
346 * @param {http.ServerResponse} res
347 * @param {Object} ctx
348 */
349 async handlerGetAdminIA(req, res, ctx) {
350 const _scope = _fileScope('handlerGetAdminIA');
351 this.logger.debug(_scope, 'called', { req, ctx });
352
353 this.setResponseType(this.responseTypes, req, res, ctx);
354
355 // Special case here, to see cookie before session established
356 ctx.cookie = req.getHeader(Enum.Header.Cookie);
357
358 await this.sessionManager.getAdminIA(res, ctx);
359 }
360
361 }
362
363 module.exports = Service;