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));
56 // Private data-editing endpoints
57 this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic
.bind(this));
58 this.on(['PATCH', 'DELETE'], '/admin/subscription/:subscriptionId', this.handlerUpdateSubscription
.bind(this));
60 // Private server-action endpoints
61 this.on('POST', '/admin/process', this.handlerPostAdminProcess
.bind(this));
64 this.on(['GET', 'HEAD'], '/admin/login', this.handlerGetAdminLogin
.bind(this));
65 this.on(['POST'], '/admin/login', this.handlerPostAdminLogin
.bind(this));
66 this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout
.bind(this));
67 this.on(['GET'], '/admin/_ia', this.handlerGetAdminIA
.bind(this));
73 * @param {http.ClientRequest} req
74 * @param {http.ServerResponse} res
77 async
handlerPostRoot(req
, res
, ctx
) {
78 const _scope
= _fileScope('handlerPostRoot');
79 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
81 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
82 await
this.ingestBody(req
, res
, ctx
);
84 await
this.manager
.postRoot(req
, res
, ctx
);
89 * @param {http.ClientRequest} req
90 * @param {http.ServerResponse} res
93 async
handlerGetRoot(req
, res
, ctx
) {
94 const _scope
= _fileScope('handlerGetRoot');
95 const responseTypes
= [
96 Enum
.ContentType
.TextHTML
,
98 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
100 Dingus
.setHeadHandler(req
, res
, ctx
);
102 this.setResponseType(responseTypes
, req
, res
, ctx
);
104 await
this.authenticator
.sessionOptional(req
, res
, ctx
, this.loginPath
);
106 await
this.manager
.getRoot(req
, res
, ctx
);
111 * @param {http.ClientRequest} req
112 * @param {http.ServerResponse} res
113 * @param {Object} ctx
115 async
handlerGetHealthcheck(req
, res
, ctx
) {
116 const _scope
= _fileScope('handlerGetHealthcheck');
117 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
119 Dingus
.setHeadHandler(req
, res
, ctx
);
121 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
123 await
this.manager
.getHealthcheck(res
, ctx
);
128 * @param {http.ClientRequest} req
129 * @param {http.ServerResponse} res
130 * @param {Object} ctx
132 async
handlerGetInfo(req
, res
, ctx
) {
133 const _scope
= _fileScope('handlerGetInfo');
134 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
136 const responseTypes
= [...this.responseTypes
, Enum
.ContentType
.ImageSVG
];
138 Dingus
.setHeadHandler(req
, res
, ctx
);
140 this.setResponseType(responseTypes
, req
, res
, ctx
);
142 await
this.manager
.getInfo(res
, ctx
);
147 * @param {http.ClientRequest} req
148 * @param {http.ServerResponse} res
149 * @param {Object} ctx
151 async
handlerGetAdminOverview(req
, res
, ctx
) {
152 const _scope
= _fileScope('handlerGetAdminOverview');
153 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
155 Dingus
.setHeadHandler(req
, res
, ctx
);
157 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
159 await
this.authenticator
.sessionRequired(req
, res
, ctx
, this.loginPath
);
161 await
this.manager
.getAdminOverview(res
, ctx
);
166 * @param {http.ClientRequest} req
167 * @param {http.ServerResponse} res
168 * @param {Object} ctx
170 async
handlerGetAdminTopicDetails(req
, res
, ctx
) {
171 const _scope
= _fileScope('handlerGetAdminTopicDetails');
172 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
174 Dingus
.setHeadHandler(req
, res
, ctx
);
176 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
178 await
this.authenticator
.sessionRequired(req
, res
, ctx
, this.loginPath
);
180 await
this.manager
.getTopicDetails(res
, ctx
);
185 * Similar to super.ingestBody, but if no body was sent, do not parse (and
186 * thus avoid possible unsupported media type error).
187 * Also removes raw body from context, to simplify scrubbing sensitive data from logs.
188 * @param {http.ClientRequest} req
189 * @param {http.ServerResponse} res
190 * @param {Object} ctx
192 async
maybeIngestBody(req
, res
, ctx
) {
193 ctx
.rawBody
= await
this.bodyData(req
);
194 const contentType
= Dingus
.getRequestContentType(req
);
196 this.parseBody(contentType
, ctx
);
203 * @param {http.ClientRequest} req
204 * @param {http.ServerResponse} res
205 * @param {Object} ctx
207 async
handlerUpdateTopic(req
, res
, ctx
) {
208 const _scope
= _fileScope('handlerUpdateTopic');
209 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
211 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
213 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
215 await
this.maybeIngestBody(req
, res
, ctx
);
216 ctx
.method
= req
.method
;
217 await
this.manager
.updateTopic(res
, ctx
);
222 * @param {http.ClientRequest} req
223 * @param {http.ServerResponse} res
224 * @param {Object} ctx
226 async
handlerUpdateSubscription(req
, res
, ctx
) {
227 const _scope
= _fileScope('handlerUpdateSubscription');
228 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
230 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
232 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
234 await
this.maybeIngestBody(req
, res
, ctx
);
235 ctx
.method
= req
.method
;
236 await
this.manager
.updateSubscription(res
, ctx
);
241 * @param {http.ClientRequest} req
242 * @param {http.ServerResponse} res
243 * @param {Object} ctx
245 async
handlerPostAdminProcess(req
, res
, ctx
) {
246 const _scope
= _fileScope('handlerPostAdminProcess');
247 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
249 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
251 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
253 await
this.manager
.processTasks(res
, ctx
);
258 * Delegate login to authentication module.
259 * @param {http.ClientRequest} req
260 * @param {http.ServerResponse} res
261 * @param {Object} ctx
263 async
handlerGetAdminLogin(req
, res
, ctx
) {
264 const _scope
= _fileScope('handlerGetAdminLogin');
265 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
267 Dingus
.setHeadHandler(req
, res
, ctx
);
269 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
271 await
this.sessionManager
.getAdminLogin(res
, ctx
);
276 * Delegate login to authentication module.
277 * @param {http.ClientRequest} req
278 * @param {http.ServerResponse} res
279 * @param {Object} ctx
281 async
handlerPostAdminLogin(req
, res
, ctx
) {
282 const _scope
= _fileScope('handlerPostAdminLogin');
283 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
285 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
287 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
289 await
this.maybeIngestBody(req
, res
, ctx
);
291 await
this.sessionManager
.postAdminLogin(res
, ctx
);
296 * Delegate login to authentication module.
297 * @param {http.ClientRequest} req
298 * @param {http.ServerResponse} res
299 * @param {Object} ctx
301 async
handlerGetAdminLogout(req
, res
, ctx
) {
302 const _scope
= _fileScope('handlerGetAdminLogout');
303 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
305 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
307 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
309 await
this.sessionManager
.getAdminLogout(res
, ctx
);
314 * Delegate login to authentication module.
315 * @param {http.ClientRequest} req
316 * @param {http.ServerResponse} res
317 * @param {Object} ctx
319 async
handlerGetAdminIA(req
, res
, ctx
) {
320 const _scope
= _fileScope('handlerGetAdminIA');
321 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
323 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
325 // Special case here, to see cookie before session established
326 ctx
.cookie
= req
.getHeader(Enum
.Header
.Cookie
);
328 await
this.sessionManager
.getAdminIA(res
, ctx
);
333 module
.exports
= Service
;