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