UNPKG

11.5 kBJavaScriptView Raw
1
2'use strict';
3
4const _ = require('lodash');
5const jwt = require('jsonwebtoken');
6const path = require('path');
7
8const debug = require( 'debug' )( 'motif:controller' );
9
10const Model = require('./Model');
11
12class Controller {
13
14 get service() {
15 return this._service;
16 }
17
18 get config() {
19 return this._service._config;
20 }
21
22 get contentType() {
23 return this._service.contentType;
24 }
25
26 get db() {
27 return this._dbConnection ? this._dbConnection.client : null;
28 }
29
30 registerModels( modelClasses ) {
31 modelClasses.forEach( (modelClass) => {
32 let className = modelClass.prototype.constructor.name;
33 let key = className.replace("Model", "");
34 let model = new modelClass();
35 let design = model.createDesign();
36
37 this._models[key] = {
38 name: key,
39 constructor: modelClass,
40 design: design
41 };
42 });
43 }
44
45 model( key, connection ) {
46 let modelData = this._models[key];
47 if (!modelData) {
48 return null;
49 }
50
51 let design = modelData.design;
52 let model = new modelData.constructor();
53 model.setCollection( design.createCollection( connection ) );
54 return model;
55 }
56
57 constructor( { service, dbConnection, models } ) {
58
59 debug( "controller", service, dbConnection );
60
61 this._service = service;
62 this._dbConnection = typeof dbConnection === "string" ? Model.dbConnection(dbConnection) : dbConnection;
63 this._models = {};
64
65 this._authConfig = {
66 tokenSecret: this._service.config.tokenSecret,
67 tokenAlgorithm: this._service.config.tokenAlgorithm,
68 refreshsTokenIn: this._service.config.refreshsTokenIn,
69 expiresTokenIn: this._service.config.expiresTokenIn,
70 };
71
72 // Auto Binding
73 const controller = this;
74 const propertyNames = Object.getOwnPropertyNames(this.constructor.prototype);
75 _.each(propertyNames, (propertyName) => {
76 if (propertyName.indexOf('on') === 0) {
77 this[propertyName] = this[propertyName].bind(controller);
78 }
79 });
80 }
81
82 verifyParams( request, fields ) {
83
84 let errorFields = [];
85 let params = request.params || {};
86
87 debug( "verifyParams", fields, params );
88
89 _.each(fields, (field) => {
90 if ( typeof field === 'string') {
91 let param = request.method.toUpperCase() === 'GET' || request.method.toUpperCase() === 'DELETE'
92 ? ( request.query[name] ? request.query[name] : params[name] )
93 : ( request.body[field] ? request.body[field] : params[field] );
94
95 // debug( "request.method", request.method );
96 // debug( "request.query", request.query );
97 // debug( "request.body", request.body );
98 debug( "param", name, param );
99
100 if (field.optional !== true && !param) {
101 errorFields.push(field);
102 }
103 params[field] = param;
104 }
105 else {
106 let { name, type } = field;
107 let param = request.method.toUpperCase() === 'GET' || request.method.toUpperCase() === 'DELETE'
108 ? ( request.query[name] ? request.query[name] : params[name] )
109 : ( request.body[name] ? request.body[name] : params[name] );
110
111 // debug( "request.method", request.method );
112 // debug( "request.query", request.query );
113 // debug( "request.body", request.body );
114 debug( "param", name, param );
115
116 if (field.optional !== true ) {
117 if (!param) {
118 errorFields.push(name);
119 }
120 else {
121 let isValid = true;
122 switch(type) {
123 default:
124 case 'string':
125 if (!_.isString(param) ) isValid = false;
126 break;
127 case 'number':
128 if (!_.isNumber(param) ) isValid = false;
129 break;
130 case 'array':
131 if (!_.isArray(param) ) isValid = false;
132 break;
133 case 'object':
134 if (!_.isObject(param) ) isValid = false;
135 break;
136 case 'file':
137 case 'boolean':
138 break;
139 break;
140 }
141 if (!isValid) {
142 errorFields.push(name);
143 }
144 }
145 }
146
147 params[name] = param;
148 }
149 });
150
151 request.params = params;
152
153 debug( "errorFields", errorFields, params );
154
155 return errorFields.length > 0 ? false : true;
156 }
157
158 checkParams(request, response, params, next) {
159 debug( "checkParams", params );
160
161 if ( !this.verifyParams( request, params ) ) {
162 return this.onError( response, "INSUFFICIENT_PARAMS", {} );
163 }
164
165 next();
166 }
167
168 generateToken( userInfo ) {
169
170 const config = this._authConfig;
171
172 debug( "generateToken", userInfo, config );
173
174 const token = jwt.sign(userInfo, config.tokenSecret, {
175 algorithm: config.tokenAlgorithm,
176 expiresIn: config.expiresTokenIn
177 });
178
179 return token;
180 }
181
182 isValidToken( token, refresh, callback ) {
183
184 debug( "isValidToken", token, refresh );
185
186 const config = this._authConfig;
187
188 jwt.verify( token, config.tokenSecret, ( err, decode ) => {
189 if ( err ) {
190 callback( { isValid: false, error: err } );
191 return;
192 }
193
194 const exp = new Date( decode.exp * 1000 );
195 const now = Date.now();
196
197 if ( exp < now ) {
198 let error = new Error( 'Expired Token' );
199 callback( { isValid: false, error } );
200 return;
201 }
202
203 if ( refresh === true ) {
204 const refreshsIn = (config.expiresTokenIn - config.refreshsTokenIn);
205 const isNeedToRefresh = (exp - refreshsIn) < now ? true : false;
206
207 if (isNeedToRefresh) {
208 const newToken = this.generateToken( decode );
209 callback( { isValid: true, newToken, userInfo: decode } );
210 return;
211 }
212 }
213
214 callback( { isValid:true, userInfo: decode } );
215 });
216 }
217
218 getAccessToken( request ) {
219
220 const authorization = request.headers['authorization'];
221
222 debug( "getAccessToken", authorization );
223
224 if (authorization) {
225 if (typeof authorization !== 'string') {
226 return null;
227 }
228
229 const matches = authorization.match(/(\S+)\s+(\S+)/);
230 const authHeaders = matches && { scheme: matches[1], value: matches[2] };
231
232 debug( "authHeaders", authHeaders );
233
234 if (authHeaders.scheme.toLowerCase() === 'bearer') {
235 return authHeaders.value;
236 }
237 }
238 else if (request.headers['x-access-token']){
239 return request.headers['x-access-token'];
240 }
241
242 return null;
243 }
244
245 parseToken(request, response, next) {
246
247 let token = this.getAccessToken(request);
248
249 if (token) {
250 request.token = token;
251
252 this.isValidToken(token, false, ({error, isValid, newToken, userInfo }) => {
253 if (userInfo) {
254 request.userInfo = userInfo;
255 }
256 next();
257 });
258 }
259 else {
260 next();
261 }
262 }
263
264 checkToken(request, response, refresh, next) {
265
266 let token = this.getAccessToken(request);
267
268 debug( "checkToken", token );
269
270 if (!token) {
271 return this.onError( response, "NEED_LOGIN", {} );
272 }
273
274 request.token = token;
275
276 this.isValidToken(token, refresh, ({error, isValid, newToken, userInfo }) => {
277 if (error) {
278 return this.onError( response, error.message, {} );
279 }
280
281 if (isValid === false) {
282 return this.onError( response, "INVALID_TOKEN", {} );
283 }
284
285 if (!userInfo) {
286 return this.onError( response, "NEED_LOGIN", {} );
287 }
288
289 if (newToken) {
290 request.newToken = newToken;
291 }
292
293 request.userInfo = userInfo;
294
295 next();
296 });
297 }
298
299 generateRouters( options ) {
300 let { router, service, routers } = options;
301
302 routers.forEach((routerOption) => {
303 const { controllers, params, permissions, description } = routerOption;
304
305 const routePath = routerOption.path;
306 const method = routerOption.method.toLowerCase();
307
308 if (!router[method] || !['get', 'post', 'put', 'delete'].includes(method)) {
309 throw new Error('invalid method ' + routePath);
310 }
311
312 if (controllers.length === 0 || typeof controllers[0] !== 'function') {
313 throw new Error('invalid controller ' + routePath + " method " + method);
314 }
315
316 let middlewares = [];
317
318 if (params) {
319 middlewares.push((request, response, next) => {
320 this.checkParams(request, response, params, next );
321 });
322 }
323
324 if (permissions) {
325 if (permissions.includes("token") || permissions.includes("admin")) {
326 middlewares.push((request, response, next) => {
327 this.checkToken(request, response, permissions.includes("refreshToken"), next );
328 });
329 }
330 else {
331 middlewares.push((request, response, next) => {
332 this.parseToken(request, response, next );
333 });
334 }
335 }
336
337 const args = [routePath, ...middlewares, ...controllers];
338
339 debug( "register router", method, args );
340
341 router[method].apply(router, args);
342
343 routerOption.routePath = path.join(this._service.path, routePath);
344 routerOption.group = this._service.name;
345 routerOption.tags = this._tags || routerOption.tags || [];
346
347 process.motif._routerPaths.push( routerOption );
348 });
349
350 debug( "generateRouters", process.motif._routerPaths.length );
351 }
352
353 jsonResponse( response, data ) {
354 data = data || {};
355
356 debug( "jsonResponse", data );
357
358 response.set({'content-type': this.contentType});
359 response.json(data);
360 }
361
362 onSuccess( response, data ) {
363 this.jsonResponse(response, {status: "success", data: data});
364 }
365
366 onError( response, message, data ) {
367
368 debug("error", message );
369
370 this.jsonResponse(response, {status: "error", error: message, data: data});
371 }
372}
373
374module.exports = Controller;