update dependency, fixes for logger update
[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 */
76 async preHandler(req, res, ctx) {
77 await super.preHandler(req, res, ctx);
78 const logObject = this.asyncLocalStorage.getStore();
79 logObject.requestId = ctx.requestId;
80 delete ctx.requestId;
81 }
82
83
84 /**
85 * @param {http.ClientRequest} req
86 * @param {http.ServerResponse} res
87 * @param {Object} ctx
88 */
89 async handlerPostRoot(req, res, ctx) {
90 const _scope = _fileScope('handlerPostRoot');
91 this.logger.debug(_scope, 'called', { req, ctx });
92
93 this.setResponseType(this.responseTypes, req, res, ctx);
94 await this.ingestBody(req, res, ctx);
95
96 await this.manager.postRoot(req, res, ctx);
97 }
98
99
100 /**
101 * @param {http.ClientRequest} req
102 * @param {http.ServerResponse} res
103 * @param {Object} ctx
104 */
105 async handlerGetRoot(req, res, ctx) {
106 const _scope = _fileScope('handlerGetRoot');
107 const responseTypes = [
108 Enum.ContentType.TextHTML,
109 ];
110 this.logger.debug(_scope, 'called', { req, ctx });
111
112 Dingus.setHeadHandler(req, res, ctx);
113
114 this.setResponseType(responseTypes, req, res, ctx);
115
116 await this.authenticator.sessionOptional(req, res, ctx, this.loginPath);
117
118 await this.manager.getRoot(req, res, ctx);
119 }
120
121
122 /**
123 * @param {http.ClientRequest} req
124 * @param {http.ServerResponse} res
125 * @param {Object} ctx
126 */
127 async handlerGetHealthcheck(req, res, ctx) {
128 const _scope = _fileScope('handlerGetHealthcheck');
129 this.logger.debug(_scope, 'called', { req, ctx });
130
131 Dingus.setHeadHandler(req, res, ctx);
132
133 this.setResponseType(this.responseTypes, req, res, ctx);
134
135 await this.manager.getHealthcheck(res, ctx);
136 }
137
138
139 /**
140 * @param {http.ClientRequest} req
141 * @param {http.ServerResponse} res
142 * @param {Object} ctx
143 */
144 async handlerGetInfo(req, res, ctx) {
145 const _scope = _fileScope('handlerGetInfo');
146 this.logger.debug(_scope, 'called', { req, ctx });
147
148 const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
149
150 Dingus.setHeadHandler(req, res, ctx);
151
152 this.setResponseType(responseTypes, req, res, ctx);
153
154 await this.manager.getInfo(res, ctx);
155 }
156
157
158 async handlerGetHistorySVG(req, res, ctx) {
159 const _scope = _fileScope('handlerGetHist');
160 this.logger.debug(_scope, 'called', { req, ctx });
161
162 const responseTypes = [Enum.ContentType.ImageSVG];
163
164 Dingus.setHeadHandler(req, res, ctx);
165
166 this.setResponseType(responseTypes, req, res, ctx);
167
168 await this.manager.getHistorySVG(res, ctx);
169 }
170
171
172 /**
173 * @param {http.ClientRequest} req
174 * @param {http.ServerResponse} res
175 * @param {Object} ctx
176 */
177 async handlerGetAdminOverview(req, res, ctx) {
178 const _scope = _fileScope('handlerGetAdminOverview');
179 this.logger.debug(_scope, 'called', { req, ctx });
180
181 Dingus.setHeadHandler(req, res, ctx);
182
183 this.setResponseType(this.responseTypes, req, res, ctx);
184
185 if (await this.authenticator.sessionRequired(req, res, ctx, this.loginPath)) {
186 await this.manager.getAdminOverview(res, ctx);
187 }
188 }
189
190
191 /**
192 * @param {http.ClientRequest} req
193 * @param {http.ServerResponse} res
194 * @param {Object} ctx
195 */
196 async handlerGetAdminTopicDetails(req, res, ctx) {
197 const _scope = _fileScope('handlerGetAdminTopicDetails');
198 this.logger.debug(_scope, 'called', { req, ctx });
199
200 Dingus.setHeadHandler(req, res, ctx);
201
202 this.setResponseType(this.responseTypes, req, res, ctx);
203
204 if (await this.authenticator.sessionRequired(req, res, ctx, this.loginPath)) {
205 await this.manager.getTopicDetails(res, ctx);
206 }
207 }
208
209
210 /**
211 * If no body was sent, do not parse (and thus avoid possible unsupported media type error).
212 * @param {http.ClientRequest} req
213 * @param {http.ServerResponse} res
214 * @param {Object} ctx
215 */
216 async maybeIngestBody(req, res, ctx) {
217 return super.ingestBody(req, res, ctx, {
218 parseEmptyBody: false,
219 });
220 }
221
222
223 /**
224 * @param {http.ClientRequest} req
225 * @param {http.ServerResponse} res
226 * @param {Object} ctx
227 */
228 async handlerUpdateTopic(req, res, ctx) {
229 const _scope = _fileScope('handlerUpdateTopic');
230 this.logger.debug(_scope, 'called', { req, ctx });
231
232 this.setResponseType(this.responseTypes, req, res, ctx);
233
234 await this.authenticator.apiRequiredLocal(req, res, ctx);
235
236 await this.maybeIngestBody(req, res, ctx);
237 ctx.method = req.method;
238 await this.manager.updateTopic(res, ctx);
239 }
240
241
242 /**
243 * @param {http.ClientRequest} req
244 * @param {http.ServerResponse} res
245 * @param {Object} ctx
246 */
247 async handlerUpdateSubscription(req, res, ctx) {
248 const _scope = _fileScope('handlerUpdateSubscription');
249 this.logger.debug(_scope, 'called', { req, ctx });
250
251 this.setResponseType(this.responseTypes, req, res, ctx);
252
253 await this.authenticator.apiRequiredLocal(req, res, ctx);
254
255 await this.maybeIngestBody(req, res, ctx);
256 ctx.method = req.method;
257 await this.manager.updateSubscription(res, ctx);
258 }
259
260
261 /**
262 * @param {http.ClientRequest} req
263 * @param {http.ServerResponse} res
264 * @param {Object} ctx
265 */
266 async handlerPostAdminProcess(req, res, ctx) {
267 const _scope = _fileScope('handlerPostAdminProcess');
268 this.logger.debug(_scope, 'called', { req, ctx });
269
270 this.setResponseType(this.responseTypes, req, res, ctx);
271
272 await this.authenticator.apiRequiredLocal(req, res, ctx);
273
274 await this.manager.processTasks(res, ctx);
275 }
276
277
278 /**
279 * Delegate login to authentication module.
280 * @param {http.ClientRequest} req
281 * @param {http.ServerResponse} res
282 * @param {Object} ctx
283 */
284 async handlerGetAdminLogin(req, res, ctx) {
285 const _scope = _fileScope('handlerGetAdminLogin');
286 this.logger.debug(_scope, 'called', { req, ctx });
287
288 Dingus.setHeadHandler(req, res, ctx);
289
290 this.setResponseType(this.responseTypes, req, res, ctx);
291
292 await this.sessionManager.getAdminLogin(res, ctx);
293 }
294
295
296 /**
297 * Delegate login to authentication module.
298 * @param {http.ClientRequest} req
299 * @param {http.ServerResponse} res
300 * @param {Object} ctx
301 */
302 async handlerPostAdminLogin(req, res, ctx) {
303 const _scope = _fileScope('handlerPostAdminLogin');
304 this.logger.debug(_scope, 'called', { req, ctx });
305
306 this.setResponseType(this.responseTypes, req, res, ctx);
307
308 await this.authenticator.sessionOptionalLocal(req, res, ctx);
309
310 await this.maybeIngestBody(req, res, ctx);
311
312 await this.sessionManager.postAdminLogin(res, ctx);
313 }
314
315
316 /**
317 * Delegate login to authentication module.
318 * @param {http.ClientRequest} req
319 * @param {http.ServerResponse} res
320 * @param {Object} ctx
321 */
322 async handlerGetAdminLogout(req, res, ctx) {
323 const _scope = _fileScope('handlerGetAdminLogout');
324 this.logger.debug(_scope, 'called', { req, ctx });
325
326 this.setResponseType(this.responseTypes, req, res, ctx);
327
328 await this.authenticator.sessionOptionalLocal(req, res, ctx);
329
330 await this.sessionManager.getAdminLogout(res, ctx);
331 }
332
333
334 /**
335 * Delegate login to authentication module.
336 * @param {http.ClientRequest} req
337 * @param {http.ServerResponse} res
338 * @param {Object} ctx
339 */
340 async handlerGetAdminIA(req, res, ctx) {
341 const _scope = _fileScope('handlerGetAdminIA');
342 this.logger.debug(_scope, 'called', { req, ctx });
343
344 this.setResponseType(this.responseTypes, req, res, ctx);
345
346 // Special case here, to see cookie before session established
347 ctx.cookie = req.getHeader(Enum.Header.Cookie);
348
349 await this.sessionManager.getAdminIA(res, ctx);
350 }
351
352 }
353
354 module.exports = Service;