update dependencies and remove now-redundant functions
[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 = require('./authenticator');
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.staticPath = path.join(__dirname, '..', 'static');
27
28 // Primary API endpoint
29 this.on('POST', '/', this.handlerPostRoot.bind(this));
30
31 // Information page about service
32 this.on(['GET', 'HEAD'], '/', this.handlerGetRoot.bind(this));
33
34 // Give load-balancers something to check
35 this.on(['GET', 'HEAD'], '/healthcheck', this.handlerGetHealthcheck.bind(this));
36
37 // Public information about topics
38 this.on('GET', '/info', this.handlerGetInfo.bind(this));
39 this.on('GET', '/info/', this.handlerGetInfo.bind(this));
40
41 // These routes are intended for accessing static content during development.
42 // In production, a proxy server would likely handle these first.
43 this.on(['GET', 'HEAD'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`);
44 this.on(['GET', 'HEAD'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html');
45 this.on(['GET', 'HEAD'], '/static/:file', this.handlerGetStaticFile.bind(this));
46 this.on(['GET', 'HEAD'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico');
47 this.on(['GET', 'HEAD'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt');
48
49 // Private informational endpoints
50 this.on(['GET', 'HEAD'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
51 this.on(['GET', 'HEAD'], '/admin/', this.handlerGetAdminOverview.bind(this));
52 this.on(['GET', 'HEAD'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this));
53
54 // Private data-editing endpoints
55 this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic.bind(this));
56 this.on(['PATCH', 'DELETE'], '/admin/subscription/:subscriptionId', this.handlerUpdateSubscription.bind(this));
57
58 // Private server-action endpoints
59 this.on('POST', '/admin/process', this.handlerPostAdminProcess.bind(this));
60 }
61
62
63 /**
64 * @param {http.ClientRequest} req
65 * @param {http.ServerResponse} res
66 * @param {object} ctx
67 */
68 async handlerPostRoot(req, res, ctx) {
69 const _scope = _fileScope('handlerPostRoot');
70 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
71
72 this.setResponseType(this.responseTypes, req, res, ctx);
73 await this.ingestBody(req, res, ctx);
74
75 await this.manager.postRoot(req, res, ctx);
76 }
77
78
79 /**
80 * @param {http.ClientRequest} req
81 * @param {http.ServerResponse} res
82 * @param {object} ctx
83 */
84 async handlerGetRoot(req, res, ctx) {
85 const _scope = _fileScope('handlerGetRoot');
86 const responseTypes = [
87 Enum.ContentType.TextHTML,
88 ];
89 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
90
91 Dingus.setHeadHandler(req, res, ctx);
92
93 this.setResponseType(responseTypes, req, res, ctx);
94
95 await this.manager.getRoot(req, res, ctx);
96 }
97
98
99 /**
100 * @param {http.ClientRequest} req
101 * @param {http.ServerResponse} res
102 * @param {object} ctx
103 */
104 async handlerGetHealthcheck(req, res, ctx) {
105 const _scope = _fileScope('handlerGetHealthcheck');
106 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
107
108 Dingus.setHeadHandler(req, res, ctx);
109
110 this.setResponseType(this.responseTypes, req, res, ctx);
111
112 await this.manager.getHealthcheck(res, ctx);
113 }
114
115
116 /**
117 * @param {http.ClientRequest} req
118 * @param {http.ServerResponse} res
119 * @param {object} ctx
120 */
121 async handlerGetInfo(req, res, ctx) {
122 const _scope = _fileScope('handlerGetInfo');
123 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
124
125 const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
126
127 Dingus.setHeadHandler(req, res, ctx);
128
129 this.setResponseType(responseTypes, req, res, ctx);
130
131 await this.manager.getInfo(res, ctx);
132 }
133
134
135 /**
136 * @param {http.ClientRequest} req
137 * @param {http.ServerResponse} res
138 * @param {object} ctx
139 */
140 async handlerGetAdminOverview(req, res, ctx) {
141 const _scope = _fileScope('handlerGetAdminOverview');
142 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
143
144 Dingus.setHeadHandler(req, res, ctx);
145
146 this.setResponseType(this.responseTypes, req, res, ctx);
147
148 await this.authenticator.required(req, res, ctx);
149
150 await this.manager.getAdminOverview(res, ctx);
151 }
152
153
154 /**
155 * @param {http.ClientRequest} req
156 * @param {http.ServerResponse} res
157 * @param {object} ctx
158 */
159 async handlerGetAdminTopicDetails(req, res, ctx) {
160 const _scope = _fileScope('handlerGetAdminTopicDetails');
161 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
162
163 Dingus.setHeadHandler(req, res, ctx);
164
165 this.setResponseType(this.responseTypes, req, res, ctx);
166
167 await this.authenticator.required(req, res, ctx);
168
169 await this.manager.getTopicDetails(res, ctx);
170 }
171
172
173 /**
174 * Same as super.ingestBody, but if no body was sent, do not parse (and
175 * thus avoid possible unsupported media type error).
176 * @param {http.ClientRequest} req
177 * @param {http.ServerResponse} res
178 * @param {Object} ctx
179 */
180 async maybeIngestBody(req, res, ctx) {
181 ctx.rawBody = await this.bodyData(req);
182 const contentType = Dingus.getRequestContentType(req);
183 if (ctx.rawBody) {
184 this.parseBody(contentType, ctx);
185 }
186 }
187
188
189 /**
190 * @param {http.ClientRequest} req
191 * @param {http.ServerResponse} res
192 * @param {Object} ctx
193 */
194 async handlerUpdateTopic(req, res, ctx) {
195 const _scope = _fileScope('handlerUpdateTopic');
196 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
197
198 this.setResponseType(this.responseTypes, req, res, ctx);
199
200 await this.authenticator.required(req, res, ctx);
201
202 await this.maybeIngestBody(req, res, ctx);
203 ctx.method = req.method;
204 await this.manager.updateTopic(res, ctx);
205 }
206
207
208 /**
209 * @param {http.ClientRequest} req
210 * @param {http.ServerResponse} res
211 * @param {Object} ctx
212 */
213 async handlerUpdateSubscription(req, res, ctx) {
214 const _scope = _fileScope('handlerUpdateSubscription');
215 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
216
217 this.setResponseType(this.responseTypes, req, res, ctx);
218
219 await this.authenticator.required(req, res, ctx);
220
221 await this.maybeIngestBody(req, res, ctx);
222 ctx.method = req.method;
223 await this.manager.updateSubscription(res, ctx);
224 }
225
226
227 /**
228 * @param {http.ClientRequest} req
229 * @param {http.ServerResponse} res
230 * @param {object} ctx
231 */
232 async handlerPostAdminProcess(req, res, ctx) {
233 const _scope = _fileScope('handlerPostAdminProcess');
234 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
235
236 this.setResponseType(this.responseTypes, req, res, ctx);
237
238 await this.authenticator.required(req, res, ctx);
239
240 await this.manager.processTasks(res, ctx);
241 }
242 }
243
244 module.exports = Service;