UNPKG

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