1 |
|
2 | 'use strict';
|
3 |
|
4 | const _ = require('lodash');
|
5 | const jwt = require('jsonwebtoken');
|
6 | const path = require('path');
|
7 |
|
8 | const debug = require( 'debug' )( 'motif:controller' );
|
9 |
|
10 | const Model = require('./Model');
|
11 |
|
12 | class 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 |
|
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 |
|
96 |
|
97 |
|
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 |
|
112 |
|
113 |
|
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 |
|
374 | module.exports = Controller;
|