1b09d2daf104a22426dc2fbd10f422d1f4f3094f
[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) {
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);
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: common.requestLogData(req), ctx });
111
112 this.setResponseType(this.responseTypes, req, res, ctx);
113 await this.ingestBody(req, res, ctx);
114 await this._postRootAuth(req, res, ctx);
115
116 await this.manager.postRoot(res, ctx);
117 }
118
119
120 /**
121 * @param {http.ClientRequest} req
122 * @param {http.ServerResponse} res
123 * @param {object} ctx
124 */
125 async handlerGetRoot(req, res, ctx) {
126 const _scope = _fileScope('handlerGetRoot');
127 const responseTypes = [
128 Enum.ContentType.TextHTML,
129 ];
130 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
131
132 Dingus.setHeadHandler(req, res, ctx);
133
134 this.setResponseType(responseTypes, req, res, ctx);
135 this.authenticator.optional(req, res, ctx);
136
137 // NB special case for this handler, pass in req so it can check headers
138 await this.manager.getRoot(req, res, ctx);
139 }
140
141
142 /**
143 * @param {http.ClientRequest} req
144 * @param {http.ServerResponse} res
145 * @param {object} ctx
146 */
147 async handlerGetId(req, res, ctx) {
148 const _scope = _fileScope('handlerGetId');
149 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
150
151 Dingus.setHeadHandler(req, res, ctx);
152
153 this.setResponseType(this.responseTypes, req, res, ctx);
154 this.authenticator.optional(req, res, ctx);
155
156 try {
157 await this.manager.getById(res, ctx);
158 } catch (e) {
159 if (e instanceof ServeStaticFile
160 && e.file) {
161 return await this.serveFile(req, res, ctx, this.staticDirectory, e.file);
162 }
163 throw e;
164 }
165 }
166
167
168 /**
169 * @param {http.ClientRequest} req
170 * @param {http.ServerResponse} res
171 * @param {object} ctx
172 */
173 async handlerDeleteId(req, res, ctx) {
174 const _scope = _fileScope('handlerDeleteId');
175 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
176
177 this.setResponseType(this.responseTypes, req, res, ctx);
178 await this.authenticator.required(req, res, ctx);
179
180 await this.manager.deleteById(res, ctx);
181 }
182
183
184 /**
185 * @param {http.ClientRequest} req
186 * @param {http.ServerResponse} res
187 * @param {object} ctx
188 */
189 async handlerPutId(req, res, ctx) {
190 const _scope = _fileScope('handlerPutId');
191 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
192
193 this.setResponseType(this.responseTypes, req, res, ctx);
194 await this.ingestBody(req, res, ctx);
195 await this.authenticator.required(req, res, ctx);
196
197 await this.manager.putById(res, ctx);
198 }
199
200
201 /**
202 * @param {http.ClientRequest} req
203 * @param {http.ServerResponse} res
204 * @param {object} ctx
205 */
206 async handlerGetIdInfo(req, res, ctx) {
207 const _scope = _fileScope('handlerGetIdInfo');
208 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
209
210 Dingus.setHeadHandler(req, res, ctx);
211
212 this.setResponseType(this.responseTypes, req, res, ctx);
213 this.authenticator.optional(req, res, ctx);
214
215 await this.manager.getByIdInfo(res, ctx);
216 }
217
218
219 /**
220 * @param {http.ClientRequest} req
221 * @param {http.ServerResponse} res
222 * @param {object} ctx
223 */
224 async handlerGetStatic(req, res, ctx) {
225 const _scope = _fileScope('handlerGetStatic');
226 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
227
228 Dingus.setHeadHandler(req, res, ctx);
229
230 // We set default response type to handle any errors, but will override for actual static content type.
231 this.setResponseType(this.responseTypes, req, res, ctx);
232 this.authenticator.optional(req, res, ctx);
233
234 await this.serveFile(req, res, ctx, this.staticDirectory, ctx.params.file);
235 }
236
237
238 /**
239 * @param {http.ClientRequest} req
240 * @param {http.ServerResponse} res
241 * @param {object} ctx
242 */
243 async handlerGetAdminReport(req, res, ctx) {
244 const _scope = _fileScope('handlerAdminReport');
245 this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
246
247 Dingus.setHeadHandler(req, res, ctx);
248
249 this.setResponseType(this.responseTypes, req, res, ctx);
250 await this.authenticator.required(req, res, ctx);
251
252 await this.manager.getAdminReport(res, ctx);
253 }
254
255
256 }
257
258 module.exports = Service;