1 | "use strict";
|
2 | const _ = require("lodash");
|
3 | const jwt = require("jsonwebtoken");
|
4 | const path = require("path");
|
5 | const debug = require("debug")("motif:controller");
|
6 | const Model = require("./Model");
|
7 |
|
8 | class 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 |
|
72 |
|
73 |
|
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 |
|
91 |
|
92 |
|
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 |
|
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 |
|
358 | module.exports = Controller;
|