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', (req
, res
, ctx
) => this.handlerRedirect(req
, res
, ctx
, `${options.dingus.proxyPrefix}/static/`));
44 this.on(['GET', 'HEAD'], '/static/', (req
, res
, ctx
) => this.handlerGetStaticFile(req
, res
, ctx
, 'index.html'));
45 this.on(['GET', 'HEAD'], '/static/:file', this.handlerGetStaticFile
.bind(this));
46 this.on(['GET', 'HEAD'], '/favicon.ico', (req
, res
, ctx
) => this.handlerGetStaticFile(req
, res
, ctx
, 'favicon.ico'));
47 this.on(['GET', 'HEAD'], '/robots.txt', (req
, res
, ctx
) => this.handlerGetStaticFile(req
, res
, ctx
, 'robots.txt'));
49 // Private informational endpoints
50 this.on(['GET', 'HEAD'], '/admin', (req
, res
, ctx
) => this.handlerRedirect(req
, res
, ctx
, `${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
67 * @param {String} newPath
69 async
handlerRedirect(req
, res
, ctx
, newPath
) {
70 const _scope
= _fileScope('handlerRedirect');
71 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
73 res
.setHeader(Enum
.Header
.Location
, newPath
);
74 res
.statusCode
= 307; // Temporary Redirect
80 * @param {http.ClientRequest} req
81 * @param {http.ServerResponse} res
84 async
handlerPostRoot(req
, res
, ctx
) {
85 const _scope
= _fileScope('handlerPostRoot');
86 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
88 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
89 await
this.ingestBody(req
, res
, ctx
);
91 await
this.manager
.postRoot(req
, res
, ctx
);
96 * @param {http.ClientRequest} req
97 * @param {http.ServerResponse} res
100 async
handlerGetRoot(req
, res
, ctx
) {
101 const _scope
= _fileScope('handlerGetRoot');
102 const responseTypes
= [
103 Enum
.ContentType
.TextHTML
,
105 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
107 Dingus
.setHeadHandler(req
, res
, ctx
);
109 this.setResponseType(responseTypes
, req
, res
, ctx
);
111 await
this.manager
.getRoot(req
, res
, ctx
);
116 * @param {http.ClientRequest} req
117 * @param {http.ServerResponse} res
118 * @param {object} ctx
120 async
handlerGetHealthcheck(req
, res
, ctx
) {
121 const _scope
= _fileScope('handlerGetHealthcheck');
122 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
124 Dingus
.setHeadHandler(req
, res
, ctx
);
126 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
128 await
this.manager
.getHealthcheck(res
, ctx
);
133 * @param {http.ClientRequest} req
134 * @param {http.ServerResponse} res
135 * @param {object} ctx
137 async
handlerGetInfo(req
, res
, ctx
) {
138 const _scope
= _fileScope('handlerGetInfo');
139 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
141 const responseTypes
= [...this.responseTypes
, Enum
.ContentType
.ImageSVG
];
143 Dingus
.setHeadHandler(req
, res
, ctx
);
145 this.setResponseType(responseTypes
, req
, res
, ctx
);
147 await
this.manager
.getInfo(res
, ctx
);
152 * @param {http.ClientRequest} req
153 * @param {http.ServerResponse} res
154 * @param {object} ctx
156 async
handlerGetAdminOverview(req
, res
, ctx
) {
157 const _scope
= _fileScope('handlerGetAdminOverview');
158 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
160 Dingus
.setHeadHandler(req
, res
, ctx
);
162 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
164 await
this.authenticator
.required(req
, res
, ctx
);
166 await
this.manager
.getAdminOverview(res
, ctx
);
171 * @param {http.ClientRequest} req
172 * @param {http.ServerResponse} res
173 * @param {object} ctx
175 async
handlerGetAdminTopicDetails(req
, res
, ctx
) {
176 const _scope
= _fileScope('handlerGetAdminTopicDetails');
177 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
179 Dingus
.setHeadHandler(req
, res
, ctx
);
181 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
183 await
this.authenticator
.required(req
, res
, ctx
);
185 await
this.manager
.getTopicDetails(res
, ctx
);
190 * Same as super.ingestBody, but if no body was sent, do not parse (and
191 * thus avoid possible unsupported media type error).
192 * @param {http.ClientRequest} req
193 * @param {http.ServerResponse} res
194 * @param {Object} ctx
196 async
maybeIngestBody(req
, res
, ctx
) {
197 ctx
.rawBody
= await
this.bodyData(req
);
198 const contentType
= Dingus
.getRequestContentType(req
);
200 this.parseBody(contentType
, ctx
);
206 * @param {http.ClientRequest} req
207 * @param {http.ServerResponse} res
208 * @param {Object} ctx
210 async
handlerUpdateTopic(req
, res
, ctx
) {
211 const _scope
= _fileScope('handlerUpdateTopic');
212 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
214 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
216 await
this.authenticator
.required(req
, res
, ctx
);
218 await
this.maybeIngestBody(req
, res
, ctx
);
219 ctx
.method
= req
.method
;
220 await
this.manager
.updateTopic(res
, ctx
);
225 * @param {http.ClientRequest} req
226 * @param {http.ServerResponse} res
227 * @param {Object} ctx
229 async
handlerUpdateSubscription(req
, res
, ctx
) {
230 const _scope
= _fileScope('handlerUpdateSubscription');
231 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
233 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
235 await
this.authenticator
.required(req
, res
, ctx
);
237 await
this.maybeIngestBody(req
, res
, ctx
);
238 ctx
.method
= req
.method
;
239 await
this.manager
.updateSubscription(res
, ctx
);
244 * @param {http.ClientRequest} req
245 * @param {http.ServerResponse} res
246 * @param {object} ctx
248 async
handlerGetStaticFile(req
, res
, ctx
, file
) {
249 const _scope
= _fileScope('handlerGetStaticFile');
250 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
, file
});
252 Dingus
.setHeadHandler(req
, res
, ctx
);
254 // Set a default response type to handle any errors; will be re-set to serve actual static content type.
255 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
257 await
this.serveFile(req
, res
, ctx
, this.staticPath
, file
|| ctx
.params
.file
);
258 this.logger
.info(_scope
, 'finished', { ctx: { ...ctx
, responseBody: common
.logTruncate((ctx
.responseBody
|| '').toString(), 100) } });
263 * @param {http.ClientRequest} req
264 * @param {http.ServerResponse} res
265 * @param {object} ctx
267 async
handlerPostAdminProcess(req
, res
, ctx
) {
268 const _scope
= _fileScope('handlerPostAdminProcess');
269 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
271 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
273 await
this.authenticator
.required(req
, res
, ctx
);
275 await
this.manager
.processTasks(res
, ctx
);
279 module
.exports
= Service
;