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
, SessionManager
} = require('@squeep/authentication-module');
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.sessionManager
= new SessionManager(logger
, this.authenticator
, options
);
27 this.staticPath
= path
.join(__dirname
, '..', 'static');
28 this.loginPath
= `${options.dingus.proxyPrefix}/admin/login`;
30 // Primary API endpoint
31 this.on('POST', '/', this.handlerPostRoot
.bind(this));
33 // Information page about service
34 this.on(['GET', 'HEAD'], '/', this.handlerGetRoot
.bind(this));
36 // Give load-balancers something to check
37 this.on(['GET', 'HEAD'], '/healthcheck', this.handlerGetHealthcheck
.bind(this));
39 // Public information about topics
40 this.on('GET', '/info', this.handlerGetInfo
.bind(this));
41 this.on('GET', '/info/', this.handlerGetInfo
.bind(this));
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');
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));
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));
61 // Private server-action endpoints
62 this.on('POST', '/admin/process', this.handlerPostAdminProcess
.bind(this));
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));
74 * @param {http.ClientRequest} req
75 * @param {http.ServerResponse} res
78 async
handlerPostRoot(req
, res
, ctx
) {
79 const _scope
= _fileScope('handlerPostRoot');
80 this.logger
.debug(_scope
, 'called', { req
, ctx
});
82 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
83 await
this.ingestBody(req
, res
, ctx
);
85 await
this.manager
.postRoot(req
, res
, ctx
);
90 * @param {http.ClientRequest} req
91 * @param {http.ServerResponse} res
94 async
handlerGetRoot(req
, res
, ctx
) {
95 const _scope
= _fileScope('handlerGetRoot');
96 const responseTypes
= [
97 Enum
.ContentType
.TextHTML
,
99 this.logger
.debug(_scope
, 'called', { req
, ctx
});
101 Dingus
.setHeadHandler(req
, res
, ctx
);
103 this.setResponseType(responseTypes
, req
, res
, ctx
);
105 await
this.authenticator
.sessionOptional(req
, res
, ctx
, this.loginPath
);
107 await
this.manager
.getRoot(req
, res
, ctx
);
112 * @param {http.ClientRequest} req
113 * @param {http.ServerResponse} res
114 * @param {Object} ctx
116 async
handlerGetHealthcheck(req
, res
, ctx
) {
117 const _scope
= _fileScope('handlerGetHealthcheck');
118 this.logger
.debug(_scope
, 'called', { req
, ctx
});
120 Dingus
.setHeadHandler(req
, res
, ctx
);
122 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
124 await
this.manager
.getHealthcheck(res
, ctx
);
129 * @param {http.ClientRequest} req
130 * @param {http.ServerResponse} res
131 * @param {Object} ctx
133 async
handlerGetInfo(req
, res
, ctx
) {
134 const _scope
= _fileScope('handlerGetInfo');
135 this.logger
.debug(_scope
, 'called', { req
, ctx
});
137 const responseTypes
= [...this.responseTypes
, Enum
.ContentType
.ImageSVG
];
139 Dingus
.setHeadHandler(req
, res
, ctx
);
141 this.setResponseType(responseTypes
, req
, res
, ctx
);
143 await
this.manager
.getInfo(res
, ctx
);
147 async
handlerGetHistorySVG(req
, res
, ctx
) {
148 const _scope
= _fileScope('handlerGetHist');
149 this.logger
.debug(_scope
, 'called', { req
, ctx
});
151 const responseTypes
= [Enum
.ContentType
.ImageSVG
];
153 Dingus
.setHeadHandler(req
, res
, ctx
);
155 this.setResponseType(responseTypes
, req
, res
, ctx
);
157 await
this.manager
.getHistorySVG(res
, ctx
);
162 * @param {http.ClientRequest} req
163 * @param {http.ServerResponse} res
164 * @param {Object} ctx
166 async
handlerGetAdminOverview(req
, res
, ctx
) {
167 const _scope
= _fileScope('handlerGetAdminOverview');
168 this.logger
.debug(_scope
, 'called', { req
, ctx
});
170 Dingus
.setHeadHandler(req
, res
, ctx
);
172 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
174 await
this.authenticator
.sessionRequired(req
, res
, ctx
, this.loginPath
);
176 await
this.manager
.getAdminOverview(res
, ctx
);
181 * @param {http.ClientRequest} req
182 * @param {http.ServerResponse} res
183 * @param {Object} ctx
185 async
handlerGetAdminTopicDetails(req
, res
, ctx
) {
186 const _scope
= _fileScope('handlerGetAdminTopicDetails');
187 this.logger
.debug(_scope
, 'called', { req
, ctx
});
189 Dingus
.setHeadHandler(req
, res
, ctx
);
191 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
193 await
this.authenticator
.sessionRequired(req
, res
, ctx
, this.loginPath
);
195 await
this.manager
.getTopicDetails(res
, ctx
);
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
205 async
maybeIngestBody(req
, res
, ctx
) {
206 return super.ingestBody(req
, res
, ctx
, {
207 parseEmptyBody: false,
213 * @param {http.ClientRequest} req
214 * @param {http.ServerResponse} res
215 * @param {Object} ctx
217 async
handlerUpdateTopic(req
, res
, ctx
) {
218 const _scope
= _fileScope('handlerUpdateTopic');
219 this.logger
.debug(_scope
, 'called', { req
, ctx
});
221 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
223 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
225 await
this.maybeIngestBody(req
, res
, ctx
);
226 ctx
.method
= req
.method
;
227 await
this.manager
.updateTopic(res
, ctx
);
232 * @param {http.ClientRequest} req
233 * @param {http.ServerResponse} res
234 * @param {Object} ctx
236 async
handlerUpdateSubscription(req
, res
, ctx
) {
237 const _scope
= _fileScope('handlerUpdateSubscription');
238 this.logger
.debug(_scope
, 'called', { req
, ctx
});
240 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
242 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
244 await
this.maybeIngestBody(req
, res
, ctx
);
245 ctx
.method
= req
.method
;
246 await
this.manager
.updateSubscription(res
, ctx
);
251 * @param {http.ClientRequest} req
252 * @param {http.ServerResponse} res
253 * @param {Object} ctx
255 async
handlerPostAdminProcess(req
, res
, ctx
) {
256 const _scope
= _fileScope('handlerPostAdminProcess');
257 this.logger
.debug(_scope
, 'called', { req
, ctx
});
259 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
261 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
263 await
this.manager
.processTasks(res
, ctx
);
268 * Delegate login to authentication module.
269 * @param {http.ClientRequest} req
270 * @param {http.ServerResponse} res
271 * @param {Object} ctx
273 async
handlerGetAdminLogin(req
, res
, ctx
) {
274 const _scope
= _fileScope('handlerGetAdminLogin');
275 this.logger
.debug(_scope
, 'called', { req
, ctx
});
277 Dingus
.setHeadHandler(req
, res
, ctx
);
279 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
281 await
this.sessionManager
.getAdminLogin(res
, ctx
);
286 * Delegate login to authentication module.
287 * @param {http.ClientRequest} req
288 * @param {http.ServerResponse} res
289 * @param {Object} ctx
291 async
handlerPostAdminLogin(req
, res
, ctx
) {
292 const _scope
= _fileScope('handlerPostAdminLogin');
293 this.logger
.debug(_scope
, 'called', { req
, ctx
});
295 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
297 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
299 await
this.maybeIngestBody(req
, res
, ctx
);
301 await
this.sessionManager
.postAdminLogin(res
, ctx
);
306 * Delegate login to authentication module.
307 * @param {http.ClientRequest} req
308 * @param {http.ServerResponse} res
309 * @param {Object} ctx
311 async
handlerGetAdminLogout(req
, res
, ctx
) {
312 const _scope
= _fileScope('handlerGetAdminLogout');
313 this.logger
.debug(_scope
, 'called', { req
, ctx
});
315 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
317 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
319 await
this.sessionManager
.getAdminLogout(res
, ctx
);
324 * Delegate login to authentication module.
325 * @param {http.ClientRequest} req
326 * @param {http.ServerResponse} res
327 * @param {Object} ctx
329 async
handlerGetAdminIA(req
, res
, ctx
) {
330 const _scope
= _fileScope('handlerGetAdminIA');
331 this.logger
.debug(_scope
, 'called', { req
, ctx
});
333 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
335 // Special case here, to see cookie before session established
336 ctx
.cookie
= req
.getHeader(Enum
.Header
.Cookie
);
338 await
this.sessionManager
.getAdminIA(res
, ctx
);
343 module
.exports
= Service
;