minor updates
[urlittler] / src / service.js
1 'use strict';
2
3 /**
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.
6 */
7
8 const { Dingus } = require('@squeep/api-dingus');
9 const path = require('path');
10 const common = require('./common');
11 const Enum = require('./enum');
12 const { ServeStaticFile } = require('./errors');
13 const Authenticator = require('./authenticator');
14 const Manager = require('./manager');
15
16 const _fileScope = common.fileScope(__filename);
17
18 const defaultOptions = {
19 ignoreTrailingSlash: true,
20 staticDirectory: path.join(__dirname, '..', 'static'),
21 createRequiresAuth: false,
22 proxyPrefix: '',
23 };
24
25 class Service extends Dingus {
26 constructor(logger, db, options = {}) {
27 super(logger, { ...defaultOptions, ...options });
28 common.setOptions(this, defaultOptions, options);
29
30 this.authenticator = new Authenticator(logger, db, options.authenticator);
31 this._postRootAuth = this.authenticator[this.createRequiresAuth ? 'required' : 'optional'].bind(this.authenticator);
32
33 this.manager = new Manager(logger, db, options.manager);
34
35 this.responseTypes.push(
36 // 'other/handled-type',
37 );
38
39 this.on('POST', '/', this.handlerPostRoot.bind(this));
40 this.on(['GET', 'HEAD'], '/', this.handlerGetRoot.bind(this));
41 this.on(['GET', 'HEAD'], '/:id', this.handlerGetId.bind(this));
42 this.on('DELETE', '/:id', this.handlerDeleteId.bind(this));
43 this.on('PUT', '/:id', this.handlerPutId.bind(this));
44 this.on(['GET', 'HEAD'], '/:id/info', this.handlerGetIdInfo.bind(this));
45 this.on(['GET', 'HEAD'], '/static/:file', this.handlerGetStatic.bind(this));
46 this.on(['GET', 'HEAD'], '/admin/report', this.handlerGetAdminReport.bind(this));
47 }
48
49
50 /**
51 * Return a content-type appropriate rendering of an errorResponse object.
52 * @param {string} contentType content type of response
53 * @param {ResponseError|Exception} err
54 */
55 renderError(contentType, err) {
56 // eslint-disable-next-line sonarjs/no-small-switch
57 switch (contentType) {
58 // Handle any additional content types here
59
60 default:
61 return super.renderError(contentType, err);
62 }
63 }
64
65
66 /**
67 * Parse rawBody from ctx as contentType into parsedBody.
68 * @param {string} contentType
69 * @param {object} ctx
70 */
71 parseBody(contentType, ctx, rawBody) {
72 // eslint-disable-next-line sonarjs/no-small-switch
73 switch (contentType) {
74 // Handle any additional content types here
75
76 default:
77 super.parseBody(contentType, ctx, rawBody);
78 }
79 }
80
81
82 /**
83 * Called before every request handler.
84 * @param {http.ClientRequest} req
85 * @param {http.ServerResponse} res
86 * @param {object} ctx
87 */
88 async preHandler(req, res, ctx) {
89 super.preHandler(req, res, ctx);
90 Dingus.setEndBodyHandler(req, res, ctx, this._endHandler.bind(this));
91 }
92
93 /**
94 * Do anything needed before sending.
95 * @param {http.ClientRequest} req
96 * @param {http.ServerResponse} res
97 * @param {object} ctx
98 */
99 _endHandler(req, res, ctx) {
100 this.authenticator.signResponse(req, res, ctx);
101 }
102
103 /**
104 * @param {http.ClientRequest} req
105 * @param {http.ServerResponse} res
106 * @param {object} ctx
107 */
108 async handlerPostRoot(req, res, ctx) {
109 const _scope = _fileScope('handlerPostRoot');
110 this.logger.debug(_scope, 'called', { req, ctx });
111
112 this.setResponseType(this.responseTypes, req, res, ctx);
113 await this.ingestBody(req, res, ctx, {
114 maximumBodySize: 1024 * 8,
115 });
116 await this._postRootAuth(req, res, ctx);
117
118 await this.manager.postRoot(res, ctx);
119 }
120
121
122 /**
123 * @param {http.ClientRequest} req
124 * @param {http.ServerResponse} res
125 * @param {object} ctx
126 */
127 async handlerGetRoot(req, res, ctx) {
128 const _scope = _fileScope('handlerGetRoot');
129 const responseTypes = [
130 Enum.ContentType.TextHTML,
131 ];
132 this.logger.debug(_scope, 'called', { req, ctx });
133
134 Dingus.setHeadHandler(req, res, ctx);
135
136 this.setResponseType(responseTypes, req, res, ctx);
137 this.authenticator.optional(req, res, ctx);
138
139 // NB special case for this handler, pass in req so it can check headers
140 await this.manager.getRoot(req, res, ctx);
141 }
142
143
144 /**
145 * @param {http.ClientRequest} req
146 * @param {http.ServerResponse} res
147 * @param {object} ctx
148 */
149 async handlerGetId(req, res, ctx) {
150 const _scope = _fileScope('handlerGetId');
151 this.logger.debug(_scope, 'called', { req, ctx });
152
153 Dingus.setHeadHandler(req, res, ctx);
154
155 this.setResponseType(this.responseTypes, req, res, ctx);
156 this.authenticator.optional(req, res, ctx);
157
158 try {
159 await this.manager.getById(res, ctx);
160 } catch (e) {
161 if (e instanceof ServeStaticFile
162 && e.file) {
163 return await this.serveFile(req, res, ctx, this.staticDirectory, e.file);
164 }
165 throw e;
166 }
167 }
168
169
170 /**
171 * @param {http.ClientRequest} req
172 * @param {http.ServerResponse} res
173 * @param {object} ctx
174 */
175 async handlerDeleteId(req, res, ctx) {
176 const _scope = _fileScope('handlerDeleteId');
177 this.logger.debug(_scope, 'called', { req, ctx });
178
179 this.setResponseType(this.responseTypes, req, res, ctx);
180 await this.authenticator.required(req, res, ctx);
181
182 await this.manager.deleteById(res, ctx);
183 }
184
185
186 /**
187 * @param {http.ClientRequest} req
188 * @param {http.ServerResponse} res
189 * @param {object} ctx
190 */
191 async handlerPutId(req, res, ctx) {
192 const _scope = _fileScope('handlerPutId');
193 this.logger.debug(_scope, 'called', { req, ctx });
194
195 this.setResponseType(this.responseTypes, req, res, ctx);
196 await this.ingestBody(req, res, ctx);
197 await this.authenticator.required(req, res, ctx);
198
199 await this.manager.putById(res, ctx);
200 }
201
202
203 /**
204 * @param {http.ClientRequest} req
205 * @param {http.ServerResponse} res
206 * @param {object} ctx
207 */
208 async handlerGetIdInfo(req, res, ctx) {
209 const _scope = _fileScope('handlerGetIdInfo');
210 this.logger.debug(_scope, 'called', { req, ctx });
211
212 Dingus.setHeadHandler(req, res, ctx);
213
214 this.setResponseType(this.responseTypes, req, res, ctx);
215 this.authenticator.optional(req, res, ctx);
216
217 await this.manager.getByIdInfo(res, ctx);
218 }
219
220
221 /**
222 * @param {http.ClientRequest} req
223 * @param {http.ServerResponse} res
224 * @param {object} ctx
225 */
226 async handlerGetStatic(req, res, ctx) {
227 const _scope = _fileScope('handlerGetStatic');
228 this.logger.debug(_scope, 'called', { req, ctx });
229
230 Dingus.setHeadHandler(req, res, ctx);
231
232 // We set default response type to handle any errors, but will override for actual static content type.
233 this.setResponseType(this.responseTypes, req, res, ctx);
234 this.authenticator.optional(req, res, ctx);
235
236 await this.serveFile(req, res, ctx, this.staticDirectory, ctx.params.file);
237 }
238
239
240 /**
241 * @param {http.ClientRequest} req
242 * @param {http.ServerResponse} res
243 * @param {object} ctx
244 */
245 async handlerGetAdminReport(req, res, ctx) {
246 const _scope = _fileScope('handlerAdminReport');
247 this.logger.debug(_scope, 'called', { req, ctx });
248
249 Dingus.setHeadHandler(req, res, ctx);
250
251 this.setResponseType(this.responseTypes, req, res, ctx);
252 await this.authenticator.required(req, res, ctx);
253
254 await this.manager.getAdminReport(res, ctx);
255 }
256
257
258 }
259
260 module.exports = Service;