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
, asyncLocalStorage
) {
21 ignoreTrailingSlash: false,
23 this.asyncLocalStorage
= asyncLocalStorage
;
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));
73 * @typedef {import('node:http')} http
77 * Rearrange logging data.
78 * @param {http.ClientRequest} req request
79 * @param {http.ServerResponse} res response
80 * @param {object} ctx context
82 async
preHandler(req
, res
, ctx
) {
83 await
super.preHandler(req
, res
, ctx
);
84 const logObject
= this.asyncLocalStorage
.getStore();
85 // FIXME: for some reason, returning from the super.preHandler sometimes loses async context?
86 // Workaround until cause and solution are found.
88 logObject
.requestId
= ctx
.requestId
;
91 this.logger
.debug(_fileScope('preHandler'), 'lost async context', { req
, ctx
});
97 * @param {http.ClientRequest} req request
98 * @param {http.ServerResponse} res response
99 * @param {object} ctx context
101 async
handlerPostRoot(req
, res
, ctx
) {
102 const _scope
= _fileScope('handlerPostRoot');
103 this.logger
.debug(_scope
, 'called', { req
, ctx
});
105 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
106 await
this.ingestBody(req
, res
, ctx
);
108 await
this.manager
.postRoot(req
, res
, ctx
);
113 * @param {http.ClientRequest} req request
114 * @param {http.ServerResponse} res response
115 * @param {object} ctx context
117 async
handlerGetRoot(req
, res
, ctx
) {
118 const _scope
= _fileScope('handlerGetRoot');
119 const responseTypes
= [
120 Enum
.ContentType
.TextHTML
,
122 this.logger
.debug(_scope
, 'called', { req
, ctx
});
124 Dingus
.setHeadHandler(req
, res
, ctx
);
126 this.setResponseType(responseTypes
, req
, res
, ctx
);
128 await
this.authenticator
.sessionOptional(req
, res
, ctx
, this.loginPath
);
130 await
this.manager
.getRoot(req
, res
, ctx
);
135 * @param {http.ClientRequest} req request
136 * @param {http.ServerResponse} res response
137 * @param {object} ctx context
139 async
handlerGetHealthcheck(req
, res
, ctx
) {
140 const _scope
= _fileScope('handlerGetHealthcheck');
141 this.logger
.debug(_scope
, 'called', { req
, ctx
});
143 Dingus
.setHeadHandler(req
, res
, ctx
);
145 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
147 await
this.manager
.getHealthcheck(res
, ctx
);
152 * @param {http.ClientRequest} req request
153 * @param {http.ServerResponse} res response
154 * @param {object} ctx context
156 async
handlerGetInfo(req
, res
, ctx
) {
157 const _scope
= _fileScope('handlerGetInfo');
158 this.logger
.debug(_scope
, 'called', { req
, ctx
});
160 const responseTypes
= [...this.responseTypes
, Enum
.ContentType
.ImageSVG
];
162 Dingus
.setHeadHandler(req
, res
, ctx
);
164 this.setResponseType(responseTypes
, req
, res
, ctx
);
166 await
this.manager
.getInfo(res
, ctx
);
170 async
handlerGetHistorySVG(req
, res
, ctx
) {
171 const _scope
= _fileScope('handlerGetHist');
172 this.logger
.debug(_scope
, 'called', { req
, ctx
});
174 const responseTypes
= [Enum
.ContentType
.ImageSVG
];
176 Dingus
.setHeadHandler(req
, res
, ctx
);
178 this.setResponseType(responseTypes
, req
, res
, ctx
);
180 await
this.manager
.getHistorySVG(res
, ctx
);
185 * @param {http.ClientRequest} req request
186 * @param {http.ServerResponse} res response
187 * @param {object} ctx context
189 async
handlerGetAdminOverview(req
, res
, ctx
) {
190 const _scope
= _fileScope('handlerGetAdminOverview');
191 this.logger
.debug(_scope
, 'called', { req
, ctx
});
193 Dingus
.setHeadHandler(req
, res
, ctx
);
195 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
197 if (await
this.authenticator
.sessionRequired(req
, res
, ctx
, this.loginPath
)) {
198 await
this.manager
.getAdminOverview(res
, ctx
);
204 * @param {http.ClientRequest} req request
205 * @param {http.ServerResponse} res response
206 * @param {object} ctx context
208 async
handlerGetAdminTopicDetails(req
, res
, ctx
) {
209 const _scope
= _fileScope('handlerGetAdminTopicDetails');
210 this.logger
.debug(_scope
, 'called', { req
, ctx
});
212 Dingus
.setHeadHandler(req
, res
, ctx
);
214 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
216 if (await
this.authenticator
.sessionRequired(req
, res
, ctx
, this.loginPath
)) {
217 await
this.manager
.getTopicDetails(res
, ctx
);
223 * If no body was sent, do not parse (and thus avoid possible unsupported media type error).
224 * @param {http.ClientRequest} req request
225 * @param {http.ServerResponse} res response
226 * @param {object} ctx context
227 * @returns {Promise<object>} parsed body
229 async
maybeIngestBody(req
, res
, ctx
) {
230 return super.ingestBody(req
, res
, ctx
, {
231 parseEmptyBody: false,
237 * @param {http.ClientRequest} req request
238 * @param {http.ServerResponse} res response
239 * @param {object} ctx context
241 async
handlerUpdateTopic(req
, res
, ctx
) {
242 const _scope
= _fileScope('handlerUpdateTopic');
243 this.logger
.debug(_scope
, 'called', { req
, ctx
});
245 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
247 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
249 await
this.maybeIngestBody(req
, res
, ctx
);
250 ctx
.method
= req
.method
;
251 await
this.manager
.updateTopic(res
, ctx
);
256 * @param {http.ClientRequest} req request
257 * @param {http.ServerResponse} res response
258 * @param {object} ctx context
260 async
handlerUpdateSubscription(req
, res
, ctx
) {
261 const _scope
= _fileScope('handlerUpdateSubscription');
262 this.logger
.debug(_scope
, 'called', { req
, ctx
});
264 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
266 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
268 await
this.maybeIngestBody(req
, res
, ctx
);
269 ctx
.method
= req
.method
;
270 await
this.manager
.updateSubscription(res
, ctx
);
275 * @param {http.ClientRequest} req request
276 * @param {http.ServerResponse} res response
277 * @param {object} ctx context
279 async
handlerPostAdminProcess(req
, res
, ctx
) {
280 const _scope
= _fileScope('handlerPostAdminProcess');
281 this.logger
.debug(_scope
, 'called', { req
, ctx
});
283 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
285 await
this.authenticator
.apiRequiredLocal(req
, res
, ctx
);
287 await
this.manager
.processTasks(res
, ctx
);
292 * Delegate login to authentication module.
293 * @param {http.ClientRequest} req request
294 * @param {http.ServerResponse} res response
295 * @param {object} ctx context
297 async
handlerGetAdminLogin(req
, res
, ctx
) {
298 const _scope
= _fileScope('handlerGetAdminLogin');
299 this.logger
.debug(_scope
, 'called', { req
, ctx
});
301 Dingus
.setHeadHandler(req
, res
, ctx
);
303 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
305 await
this.sessionManager
.getAdminLogin(res
, ctx
);
310 * Delegate login to authentication module.
311 * @param {http.ClientRequest} req request
312 * @param {http.ServerResponse} res response
313 * @param {object} ctx context
315 async
handlerPostAdminLogin(req
, res
, ctx
) {
316 const _scope
= _fileScope('handlerPostAdminLogin');
317 this.logger
.debug(_scope
, 'called', { req
, ctx
});
319 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
321 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
323 await
this.maybeIngestBody(req
, res
, ctx
);
325 await
this.sessionManager
.postAdminLogin(res
, ctx
);
330 * Delegate login to authentication module.
331 * @param {http.ClientRequest} req request
332 * @param {http.ServerResponse} res response
333 * @param {object} ctx context
335 async
handlerGetAdminLogout(req
, res
, ctx
) {
336 const _scope
= _fileScope('handlerGetAdminLogout');
337 this.logger
.debug(_scope
, 'called', { req
, ctx
});
339 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
341 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
343 await
this.sessionManager
.getAdminLogout(res
, ctx
);
348 * Delegate login to authentication module.
349 * @param {http.ClientRequest} req request
350 * @param {http.ServerResponse} res response
351 * @param {object} ctx context
353 async
handlerGetAdminIA(req
, res
, ctx
) {
354 const _scope
= _fileScope('handlerGetAdminIA');
355 this.logger
.debug(_scope
, 'called', { req
, ctx
});
357 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
359 // Special case here, to see cookie before session established
360 ctx
.cookie
= req
.getHeader(Enum
.Header
.Cookie
);
362 await
this.sessionManager
.getAdminIA(res
, ctx
);
367 module
.exports
= Service
;