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 SessionManager
= require('./session-manager');
13 const Authenticator
= require('./authenticator');
14 const path
= require('path');
16 const _fileScope
= common
.fileScope(__filename
);
18 class Service
extends Dingus
{
19 constructor(logger
, db
, options
) {
22 ignoreTrailingSlash: false,
25 this.manager
= new Manager(logger
, db
, options
);
26 this.authenticator
= new Authenticator(logger
, db
, options
);
27 this.sessionManager
= new SessionManager(logger
, this.authenticator
, options
);
28 this.staticPath
= path
.join(__dirname
, '..', 'static');
29 this.loginPath
= `${options.dingus.proxyPrefix}/admin/login`;
31 // Primary API endpoint
32 this.on('POST', '/', this.handlerPostRoot
.bind(this));
34 // Information page about service
35 this.on(['GET', 'HEAD'], '/', this.handlerGetRoot
.bind(this));
37 // Give load-balancers something to check
38 this.on(['GET', 'HEAD'], '/healthcheck', this.handlerGetHealthcheck
.bind(this));
40 // Public information about topics
41 this.on('GET', '/info', this.handlerGetInfo
.bind(this));
42 this.on('GET', '/info/', this.handlerGetInfo
.bind(this));
44 // These routes are intended for accessing static content during development.
45 // In production, a proxy server would likely handle these first.
46 this.on(['GET', 'HEAD'], '/static', this.handlerRedirect
.bind(this), `${options.dingus.proxyPrefix}/static/`);
47 this.on(['GET', 'HEAD'], '/static/', this.handlerGetStaticFile
.bind(this), 'index.html');
48 this.on(['GET', 'HEAD'], '/static/:file', this.handlerGetStaticFile
.bind(this));
49 this.on(['GET', 'HEAD'], '/favicon.ico', this.handlerGetStaticFile
.bind(this), 'favicon.ico');
50 this.on(['GET', 'HEAD'], '/robots.txt', this.handlerGetStaticFile
.bind(this), 'robots.txt');
52 // Private informational endpoints
53 this.on(['GET', 'HEAD'], '/admin', this.handlerRedirect
.bind(this), `${options.dingus.proxyPrefix}/admin/`);
54 this.on(['GET', 'HEAD'], '/admin/', this.handlerGetAdminOverview
.bind(this));
55 this.on(['GET', 'HEAD'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails
.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: common
.requestLogData(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: common
.requestLogData(req
), ctx
});
101 Dingus
.setHeadHandler(req
, res
, ctx
);
103 this.setResponseType(responseTypes
, req
, res
, ctx
);
105 await
this.manager
.getRoot(req
, res
, ctx
);
110 * @param {http.ClientRequest} req
111 * @param {http.ServerResponse} res
112 * @param {Object} ctx
114 async
handlerGetHealthcheck(req
, res
, ctx
) {
115 const _scope
= _fileScope('handlerGetHealthcheck');
116 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
118 Dingus
.setHeadHandler(req
, res
, ctx
);
120 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
122 await
this.manager
.getHealthcheck(res
, ctx
);
127 * @param {http.ClientRequest} req
128 * @param {http.ServerResponse} res
129 * @param {Object} ctx
131 async
handlerGetInfo(req
, res
, ctx
) {
132 const _scope
= _fileScope('handlerGetInfo');
133 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
135 const responseTypes
= [...this.responseTypes
, Enum
.ContentType
.ImageSVG
];
137 Dingus
.setHeadHandler(req
, res
, ctx
);
139 this.setResponseType(responseTypes
, req
, res
, ctx
);
141 await
this.manager
.getInfo(res
, ctx
);
146 * @param {http.ClientRequest} req
147 * @param {http.ServerResponse} res
148 * @param {Object} ctx
150 async
handlerGetAdminOverview(req
, res
, ctx
) {
151 const _scope
= _fileScope('handlerGetAdminOverview');
152 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
154 Dingus
.setHeadHandler(req
, res
, ctx
);
156 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
158 await
this.authenticator
.required(req
, res
, ctx
, this.loginPath
);
160 await
this.manager
.getAdminOverview(res
, ctx
);
165 * @param {http.ClientRequest} req
166 * @param {http.ServerResponse} res
167 * @param {Object} ctx
169 async
handlerGetAdminTopicDetails(req
, res
, ctx
) {
170 const _scope
= _fileScope('handlerGetAdminTopicDetails');
171 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
173 Dingus
.setHeadHandler(req
, res
, ctx
);
175 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
177 await
this.authenticator
.required(req
, res
, ctx
, this.loginPath
);
179 await
this.manager
.getTopicDetails(res
, ctx
);
184 * Similar to super.ingestBody, but if no body was sent, do not parse (and
185 * thus avoid possible unsupported media type error).
186 * Also removes raw body from context, to simplify scrubbing sensitive data from logs.
187 * @param {http.ClientRequest} req
188 * @param {http.ServerResponse} res
189 * @param {Object} ctx
191 async
maybeIngestBody(req
, res
, ctx
) {
192 ctx
.rawBody
= await
this.bodyData(req
);
193 const contentType
= Dingus
.getRequestContentType(req
);
195 this.parseBody(contentType
, ctx
);
202 * @param {http.ClientRequest} req
203 * @param {http.ServerResponse} res
204 * @param {Object} ctx
206 async
handlerUpdateTopic(req
, res
, ctx
) {
207 const _scope
= _fileScope('handlerUpdateTopic');
208 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
210 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
212 await
this.authenticator
.requiredLocal(req
, res
, ctx
, this.loginPath
);
214 await
this.maybeIngestBody(req
, res
, ctx
);
215 ctx
.method
= req
.method
;
216 await
this.manager
.updateTopic(res
, ctx
);
221 * @param {http.ClientRequest} req
222 * @param {http.ServerResponse} res
223 * @param {Object} ctx
225 async
handlerUpdateSubscription(req
, res
, ctx
) {
226 const _scope
= _fileScope('handlerUpdateSubscription');
227 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
229 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
231 await
this.authenticator
.requiredLocal(req
, res
, ctx
, this.loginPath
);
233 await
this.maybeIngestBody(req
, res
, ctx
);
234 ctx
.method
= req
.method
;
235 await
this.manager
.updateSubscription(res
, ctx
);
240 * @param {http.ClientRequest} req
241 * @param {http.ServerResponse} res
242 * @param {Object} ctx
244 async
handlerPostAdminProcess(req
, res
, ctx
) {
245 const _scope
= _fileScope('handlerPostAdminProcess');
246 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
248 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
250 await
this.authenticator
.requiredLocal(req
, res
, ctx
, this.loginPath
);
252 await
this.manager
.processTasks(res
, ctx
);
257 * @param {http.ClientRequest} req
258 * @param {http.ServerResponse} res
259 * @param {Object} ctx
261 async
handlerGetAdminLogin(req
, res
, ctx
) {
262 const _scope
= _fileScope('handlerGetAdminLogin');
263 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
265 Dingus
.setHeadHandler(req
, res
, ctx
);
267 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
269 await
this.sessionManager
.getAdminLogin(res
, ctx
);
274 * @param {http.ClientRequest} req
275 * @param {http.ServerResponse} res
276 * @param {Object} ctx
278 async
handlerPostAdminLogin(req
, res
, ctx
) {
279 const _scope
= _fileScope('handlerPostAdminLogin');
280 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
282 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
284 await
this.maybeIngestBody(req
, res
, ctx
);
286 await
this.sessionManager
.postAdminLogin(res
, ctx
);
291 * @param {http.ClientRequest} req
292 * @param {http.ServerResponse} res
293 * @param {Object} ctx
295 async
handlerGetAdminLogout(req
, res
, ctx
) {
296 const _scope
= _fileScope('handlerGetAdminLogout');
297 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
299 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
301 await
this.sessionManager
.getAdminLogout(res
, ctx
);
306 * @param {http.ClientRequest} req
307 * @param {http.ServerResponse} res
308 * @param {Object} ctx
310 async
handlerGetAdminIA(req
, res
, ctx
) {
311 const _scope
= _fileScope('handlerGetAdminIA');
312 this.logger
.debug(_scope
, 'called', { req: common
.requestLogData(req
), ctx
});
314 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
316 // Special case here, to see cookie before session established
317 ctx
.cookie
= req
.getHeader(Enum
.Header
.Cookie
);
319 await
this.sessionManager
.getAdminIA(res
, ctx
);
324 module
.exports
= Service
;