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
= require('./authenticator');
13 const path
= require('path');
15 const _fileScope
= common
.fileScope(__filename
);
17 class Service
extends Dingus
{
18 constructor(logger
, db
, options
) {
21 ignoreTrailingSlash: false,
24 this.manager
= new Manager(logger
, db
, options
);
25 this.authenticator
= new Authenticator(logger
, db
, options
);
26 this.staticPath
= path
.join(__dirname
, '..', 'static');
28 // Primary API endpoint
29 this.on('POST', '/', this.handlerPostRoot
.bind(this));
31 // Information page about service
32 this.on(['GET', 'HEAD'], '/', this.handlerGetRoot
.bind(this));
34 // Give load-balancers something to check
35 this.on(['GET', 'HEAD'], '/healthcheck', this.handlerGetHealthcheck
.bind(this));
37 // Public information about topics
38 this.on('GET', '/info', this.handlerGetInfo
.bind(this));
39 this.on('GET', '/info/', this.handlerGetInfo
.bind(this));
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');
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));
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));
58 // Private server-action endpoints
59 this.on('POST', '/admin/process', this.handlerPostAdminProcess
.bind(this));
64 * @param {http.ClientRequest} req
65 * @param {http.ServerResponse} res
68 async
handlerPostRoot(req
, res
, ctx
) {
69 const _scope
= _fileScope('handlerPostRoot');
70 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
72 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
73 await
this.ingestBody(req
, res
, ctx
);
75 await
this.manager
.postRoot(req
, res
, ctx
);
80 * @param {http.ClientRequest} req
81 * @param {http.ServerResponse} res
84 async
handlerGetRoot(req
, res
, ctx
) {
85 const _scope
= _fileScope('handlerGetRoot');
86 const responseTypes
= [
87 Enum
.ContentType
.TextHTML
,
89 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
91 Dingus
.setHeadHandler(req
, res
, ctx
);
93 this.setResponseType(responseTypes
, req
, res
, ctx
);
95 await
this.manager
.getRoot(req
, res
, ctx
);
100 * @param {http.ClientRequest} req
101 * @param {http.ServerResponse} res
102 * @param {object} ctx
104 async
handlerGetHealthcheck(req
, res
, ctx
) {
105 const _scope
= _fileScope('handlerGetHealthcheck');
106 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
108 Dingus
.setHeadHandler(req
, res
, ctx
);
110 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
112 await
this.manager
.getHealthcheck(res
, ctx
);
117 * @param {http.ClientRequest} req
118 * @param {http.ServerResponse} res
119 * @param {object} ctx
121 async
handlerGetInfo(req
, res
, ctx
) {
122 const _scope
= _fileScope('handlerGetInfo');
123 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
125 const responseTypes
= [...this.responseTypes
, Enum
.ContentType
.ImageSVG
];
127 Dingus
.setHeadHandler(req
, res
, ctx
);
129 this.setResponseType(responseTypes
, req
, res
, ctx
);
131 await
this.manager
.getInfo(res
, ctx
);
136 * @param {http.ClientRequest} req
137 * @param {http.ServerResponse} res
138 * @param {object} ctx
140 async
handlerGetAdminOverview(req
, res
, ctx
) {
141 const _scope
= _fileScope('handlerGetAdminOverview');
142 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
144 Dingus
.setHeadHandler(req
, res
, ctx
);
146 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
148 await
this.authenticator
.required(req
, res
, ctx
);
150 await
this.manager
.getAdminOverview(res
, ctx
);
155 * @param {http.ClientRequest} req
156 * @param {http.ServerResponse} res
157 * @param {object} ctx
159 async
handlerGetAdminTopicDetails(req
, res
, ctx
) {
160 const _scope
= _fileScope('handlerGetAdminTopicDetails');
161 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
163 Dingus
.setHeadHandler(req
, res
, ctx
);
165 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
167 await
this.authenticator
.required(req
, res
, ctx
);
169 await
this.manager
.getTopicDetails(res
, ctx
);
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
180 async
maybeIngestBody(req
, res
, ctx
) {
181 ctx
.rawBody
= await
this.bodyData(req
);
182 const contentType
= Dingus
.getRequestContentType(req
);
184 this.parseBody(contentType
, ctx
);
190 * @param {http.ClientRequest} req
191 * @param {http.ServerResponse} res
192 * @param {Object} ctx
194 async
handlerUpdateTopic(req
, res
, ctx
) {
195 const _scope
= _fileScope('handlerUpdateTopic');
196 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
198 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
200 await
this.authenticator
.required(req
, res
, ctx
);
202 await
this.maybeIngestBody(req
, res
, ctx
);
203 ctx
.method
= req
.method
;
204 await
this.manager
.updateTopic(res
, ctx
);
209 * @param {http.ClientRequest} req
210 * @param {http.ServerResponse} res
211 * @param {Object} ctx
213 async
handlerUpdateSubscription(req
, res
, ctx
) {
214 const _scope
= _fileScope('handlerUpdateSubscription');
215 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
217 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
219 await
this.authenticator
.required(req
, res
, ctx
);
221 await
this.maybeIngestBody(req
, res
, ctx
);
222 ctx
.method
= req
.method
;
223 await
this.manager
.updateSubscription(res
, ctx
);
228 * @param {http.ClientRequest} req
229 * @param {http.ServerResponse} res
230 * @param {object} ctx
232 async
handlerPostAdminProcess(req
, res
, ctx
) {
233 const _scope
= _fileScope('handlerPostAdminProcess');
234 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
236 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
238 await
this.authenticator
.required(req
, res
, ctx
);
240 await
this.manager
.processTasks(res
, ctx
);
244 module
.exports
= Service
;