1 | "use strict"
|
2 |
|
3 | const _ = require("lodash")
|
4 | const jwt = require("jsonwebtoken")
|
5 | const path = require("path")
|
6 | const glob = require("glob")
|
7 | const Model = require("./Model")
|
8 | const Debugger = require("./Debugger")
|
9 |
|
10 | const debug = new Debugger('motif:controller')
|
11 |
|
12 | class Controller {
|
13 | get service() {
|
14 | return this._service
|
15 | }
|
16 |
|
17 | get config() {
|
18 | return this._service._config
|
19 | }
|
20 |
|
21 | get contentType() {
|
22 | return this._service.contentType
|
23 | }
|
24 |
|
25 | constants(key) {
|
26 | let constants = process.motif.constants || {}
|
27 | return constants[key.toUpperCase()] || {}
|
28 | }
|
29 |
|
30 | model(key, connection = null) {
|
31 | let models = process.motif.models
|
32 | let modelData = models[key]
|
33 | if (!modelData) {
|
34 | return null
|
35 | }
|
36 |
|
37 | let config = this.config
|
38 | let design = modelData.design
|
39 | let model = new modelData.constructor({design, config})
|
40 | if (connection) {
|
41 | model.setCollection(design.createCollection(connection))
|
42 | }
|
43 |
|
44 | return model
|
45 | }
|
46 |
|
47 | verifyParams(request, fields, errorFields = []) {
|
48 | let params = request.params || {}
|
49 |
|
50 | debug.log("verifyParams", fields, params)
|
51 |
|
52 | _.each(fields, field => {
|
53 | if (typeof field === "string") {
|
54 | let name = field
|
55 | let param =
|
56 | params[name] ?
|
57 | params[name] :
|
58 | request.method.toUpperCase() === "GET" ||
|
59 | request.method.toUpperCase() === "DELETE" ?
|
60 | request.query[name] :
|
61 | request.body[name]
|
62 |
|
63 | debug.log("param", name, param)
|
64 | if (param === undefined) {
|
65 | errorFields.push(name)
|
66 | }
|
67 | params[field] = param
|
68 | } else {
|
69 | let { name, type } = field
|
70 | let param =
|
71 | params[name] ?
|
72 | params[name] :
|
73 | request.method.toUpperCase() === "GET" ||
|
74 | request.method.toUpperCase() === "DELETE" ?
|
75 | request.query[name] :
|
76 | request.body[name]
|
77 |
|
78 | debug.log("param", name, param)
|
79 | if (field.optional !== true) {
|
80 | if (param === undefined) {
|
81 | errorFields.push(name)
|
82 | } else {
|
83 | let isValid = true
|
84 | switch (type) {
|
85 | default:
|
86 | case "string":
|
87 | if (!_.isString(param) || param === '') isValid = false
|
88 | break
|
89 | case "number":
|
90 | if (!_.isNumber(param)) isValid = false
|
91 | break
|
92 | case "array":
|
93 | if (!_.isArray(param)) isValid = false
|
94 | break
|
95 | case "object":
|
96 | if (!_.isObject(param)) isValid = false
|
97 | break
|
98 | case "file":
|
99 | case "boolean":
|
100 | break
|
101 | }
|
102 | if (!isValid) {
|
103 | debug.debug("invalid type param", name, typeof param, param)
|
104 | errorFields.push(name)
|
105 | }
|
106 | }
|
107 | }
|
108 | params[name] = param
|
109 | }
|
110 | })
|
111 |
|
112 | request.params = params
|
113 |
|
114 | debug.log("errorFields", errorFields, params)
|
115 | return errorFields.length > 0 ? false : true
|
116 | }
|
117 |
|
118 | checkParams(request, response, params, next) {
|
119 | debug.log("checkParams", params)
|
120 |
|
121 | let invalidParams = []
|
122 |
|
123 | if (!this.verifyParams(request, params, invalidParams)) {
|
124 | return this.onError(response, "INSUFFICIENT_PARAMS", {
|
125 | 'invalid-params': invalidParams.join(',')
|
126 | })
|
127 | }
|
128 |
|
129 | next()
|
130 | }
|
131 |
|
132 | generateToken(userInfo) {
|
133 | const config = this._authConfig
|
134 |
|
135 | debug.log("generateToken", userInfo, config)
|
136 |
|
137 | const token = jwt.sign(userInfo, config.tokenSecret, {
|
138 | algorithm: config.tokenAlgorithm,
|
139 | expiresIn: config.expiresTokenIn
|
140 | })
|
141 |
|
142 | return token
|
143 | }
|
144 |
|
145 | isValidToken(token, refresh, callback) {
|
146 | debug.log("isValidToken", token, refresh)
|
147 |
|
148 | const config = this._authConfig
|
149 | jwt.verify(token, config.tokenSecret, (err, decode) => {
|
150 | if (err) {
|
151 | if (err.message === 'jwt malformed') {
|
152 | callback({ isValid: false, error: new Error('INVALID_TOKEN'), status: 401 })
|
153 | }
|
154 | else {
|
155 | callback({ isValid: false, error: err })
|
156 | }
|
157 | return
|
158 | }
|
159 |
|
160 | const exp = +new Date(decode.exp * 1000)
|
161 | const now = Date.now()
|
162 |
|
163 | if (exp < now) {
|
164 | let error = new Error("Expired Token")
|
165 | callback({ isValid: false, error })
|
166 | return
|
167 | }
|
168 |
|
169 | if (refresh === true) {
|
170 | const refreshsIn = exp - now
|
171 | const isNeedToRefresh = ((config.expiresTokenIn * 1000) - refreshsIn) > config.refreshsTokenIn * 1000 ? true : false
|
172 |
|
173 | if (isNeedToRefresh) {
|
174 | let userInfo = Object.assign(decode)
|
175 |
|
176 | delete userInfo["exp"]
|
177 | delete userInfo["iat"]
|
178 |
|
179 | const newToken = this.generateToken(userInfo)
|
180 | callback({ isValid: true, newToken, userInfo: userInfo })
|
181 | return
|
182 | }
|
183 | }
|
184 |
|
185 | callback({ isValid: true, userInfo: decode })
|
186 | })
|
187 | }
|
188 |
|
189 | getAccessToken(request) {
|
190 | const authorization = request.headers["authorization"]
|
191 | const jwtConfig = this.config.get('jwt')
|
192 |
|
193 | debug.log("getAccessToken", authorization, jwtConfig)
|
194 |
|
195 | if (authorization) {
|
196 | if (typeof authorization !== "string") {
|
197 | return null
|
198 | }
|
199 | const matches = authorization.match(/(\S+)\s+(\S+)/)
|
200 | const authHeaders = matches && { scheme: matches[1], value: matches[2] }
|
201 | if (authHeaders && authHeaders.scheme.toLowerCase() === "bearer") {
|
202 | debug.log("authHeaders", authHeaders)
|
203 | return authHeaders.value
|
204 | }
|
205 | } else if (request.headers["X-API-Key"]) {
|
206 | debug.log("X-API-Key", request.headers["X-API-Key"])
|
207 | return request.headers["X-API-Key"]
|
208 | } else if (request.headers["x-access-token"]) {
|
209 | debug.log("x-access-token", request.headers["x-access-token"])
|
210 | return request.headers["x-access-token"]
|
211 | }
|
212 | else if (this.config.mode !== 'production' && jwtConfig && jwtConfig.testToken) {
|
213 | return jwtConfig.testToken
|
214 | }
|
215 |
|
216 | return null
|
217 | }
|
218 |
|
219 | checkToken(request, response, refresh, next) {
|
220 | let token = this.getAccessToken(request)
|
221 | debug.log("checkToken", token, refresh)
|
222 |
|
223 | if (!token) {
|
224 | return this.onUnauthorized(response)
|
225 | }
|
226 |
|
227 | request.token = token
|
228 |
|
229 | this.isValidToken( token, refresh, ({ status, error, isValid, newToken, userInfo }) => {
|
230 | if (error) {
|
231 | if (status) { response.status(status) }
|
232 | return this.onError(response, error.message, {})
|
233 | }
|
234 |
|
235 | if (isValid === false) {
|
236 | return this.onError(response, "INVALID_TOKEN", {})
|
237 | }
|
238 |
|
239 | if (!userInfo) {
|
240 | return this.onUnauthorized(response)
|
241 | }
|
242 |
|
243 | if (newToken) {
|
244 | request.newToken = newToken
|
245 | }
|
246 |
|
247 | request.userInfo = userInfo || {}
|
248 | next()
|
249 | })
|
250 | }
|
251 |
|
252 | parseToken(request, response, next) {
|
253 | let token = this.getAccessToken(request)
|
254 | debug.log("parseToken", token)
|
255 |
|
256 | if (token) {
|
257 | request.token = token
|
258 |
|
259 | this.isValidToken( token, false, ({ error, isValid, newToken, userInfo }) => {
|
260 | request.userInfo = userInfo || {}
|
261 | next()
|
262 | })
|
263 | } else {
|
264 | request.userInfo = {}
|
265 | next()
|
266 | }
|
267 | }
|
268 |
|
269 | parseDeviceInfo(request, response, next) {
|
270 |
|
271 | if (request.headers["x-device-info"]) {
|
272 | debug.log("parseDeviceInfo", request.headers["x-device-info"])
|
273 |
|
274 | try {
|
275 | let jsonString = request.headers["x-device-info"]
|
276 | let deviceInfo = JSON.parse(jsonString)
|
277 |
|
278 | request.deviceInfo = deviceInfo
|
279 | }
|
280 | catch(e) {
|
281 | request.deviceInfo = {}
|
282 | }
|
283 | }
|
284 | else {
|
285 | request.deviceInfo = {}
|
286 | }
|
287 |
|
288 | next()
|
289 | }
|
290 |
|
291 | onUnauthorized(response) {
|
292 | debug.log("onUnauthorized")
|
293 |
|
294 | response.status(401)
|
295 | return this.onError(response, "Unauthorized Error", {})
|
296 | }
|
297 |
|
298 | generateRouters(options) {
|
299 | let { router, service, routers } = options
|
300 |
|
301 | routers.forEach(routerOption => {
|
302 | const { controllers, params, permissions, description } = routerOption
|
303 | const routePath = routerOption.path
|
304 | const method = routerOption.method && routerOption.method.toLowerCase() || "GET"
|
305 |
|
306 | if (!router[method] || !["get", "post", "put", "delete"].includes(method)) {
|
307 | throw new Error("invalid method " + routePath)
|
308 | }
|
309 |
|
310 | if (controllers.length === 0 || typeof controllers[0] !== "function") {
|
311 | throw new Error("invalid controller " + routePath + " method " + method)
|
312 | }
|
313 |
|
314 | let middlewares = []
|
315 |
|
316 | if (params) {
|
317 | middlewares.push((request, response, next) => {
|
318 | this.checkParams(request, response, params, next)
|
319 | })
|
320 | }
|
321 |
|
322 | if (permissions) {
|
323 | if (permissions.includes("token") || permissions.includes("admin")) {
|
324 | middlewares.push((request, response, next) => {
|
325 | this.checkToken( request, response, permissions.includes("refresh"), next )
|
326 | })
|
327 | } else {
|
328 | middlewares.push((request, response, next) => {
|
329 | this.parseToken(request, response, next)
|
330 | })
|
331 | }
|
332 |
|
333 | if (permissions.includes("device")) {
|
334 | middlewares.push((request, response, next) => {
|
335 | this.parseDeviceInfo(request, response, next)
|
336 | })
|
337 | }
|
338 | }
|
339 |
|
340 | const handlers = controllers.map((handler) => {
|
341 | try {
|
342 | return async (request, response, next) => {
|
343 | let requestId = debug.requestStart(request)
|
344 |
|
345 | try {
|
346 | const result = await handler.call(this, request, response, next)
|
347 |
|
348 | if (typeof result === 'undefined' || typeof result === 'number') {
|
349 | this.onStatus(response, result)
|
350 | return debug.requestEnd(requestId, "status")
|
351 | }
|
352 |
|
353 | this.onSuccess(response, result)
|
354 | return debug.requestEnd(requestId, "success")
|
355 | } catch (e) {
|
356 | this.onError(response, e, {})
|
357 | return debug.requestEnd(requestId, "error")
|
358 | }
|
359 | }
|
360 | }
|
361 | catch(e) {
|
362 | return this.onError(response, 'Unknown Error', {})
|
363 | }
|
364 | })
|
365 |
|
366 | const args = [path.join(options.path || "", routePath), ...middlewares, ...handlers]
|
367 |
|
368 | debug.log("register router", method, args)
|
369 |
|
370 | router[method].apply(router, args)
|
371 |
|
372 | routerOption.routePath = path.join(this._service.path, options.path || "", routePath)
|
373 | routerOption.group = this._service.name
|
374 | routerOption.tags = _.uniq(_.merge(_.clone(this._tags || []), routerOption.tags || []))
|
375 |
|
376 | process.motif._routerPaths.push(routerOption)
|
377 | })
|
378 |
|
379 | debug.log("generateRouters", process.motif._routerPaths.length)
|
380 | }
|
381 |
|
382 | jsonResponse(response, data) {
|
383 | data = data || {}
|
384 |
|
385 | debug.log("jsonResponse", this.contentType, data)
|
386 |
|
387 | response.set({ "content-type": this.contentType })
|
388 | response.json(data)
|
389 | }
|
390 |
|
391 | onStatus(response, status) {
|
392 | let message
|
393 |
|
394 | switch(status) {
|
395 | case 100:
|
396 | message = 'Continue'
|
397 | break
|
398 | case 101:
|
399 | message = 'Switching Protocol'
|
400 | break
|
401 | case 102:
|
402 | message = 'Processing (WebDAV)'
|
403 | break
|
404 | case 103:
|
405 | message = 'Early Hints'
|
406 | break
|
407 | case 200:
|
408 | message = 'OK'
|
409 | break
|
410 | case 201:
|
411 | message = 'Created'
|
412 | break
|
413 | case 202:
|
414 | message = 'Accepted'
|
415 | break
|
416 | case 203:
|
417 | message = 'Non-Authoritative Information'
|
418 | break
|
419 | case 204:
|
420 | message = 'No Content'
|
421 | break
|
422 | case 205:
|
423 | message = 'Reset Content'
|
424 | break
|
425 | case 206:
|
426 | message = 'Partial Content'
|
427 | break
|
428 | case 207:
|
429 | message = 'Multi-Status (WebDAV)'
|
430 | break
|
431 | case 208:
|
432 | message = 'Multi-Status (WebDAV)'
|
433 | break
|
434 | case 226:
|
435 | message = 'IM Used (HTTP Delta encoding)'
|
436 | break
|
437 | case 300:
|
438 | message = 'Multiple Choice'
|
439 | break
|
440 | case 301:
|
441 | message = 'Moved Permanently'
|
442 | break
|
443 | case 302:
|
444 | message = 'Found'
|
445 | break
|
446 | case 303:
|
447 | message = 'See Other'
|
448 | break
|
449 | case 304:
|
450 | message = 'Not Modified'
|
451 | break
|
452 | case 305:
|
453 | message = 'Use Proxy'
|
454 | break
|
455 | case 306:
|
456 | message = 'Unused'
|
457 | break
|
458 | case 307:
|
459 | message = 'Temporary Redirect'
|
460 | break
|
461 | case 308:
|
462 | message = 'Permanent Redirect'
|
463 | break
|
464 | case 400:
|
465 | message = 'Bad Request'
|
466 | break
|
467 | case 401:
|
468 | message = 'Unauthorized'
|
469 | break
|
470 | case 402:
|
471 | message = 'Payment Required'
|
472 | break
|
473 | case 403:
|
474 | message = 'Forbidden'
|
475 | break
|
476 | case 404:
|
477 | message = 'Not Found'
|
478 | break
|
479 | case 405:
|
480 | message = 'Method Not Allowed'
|
481 | break
|
482 | case 406:
|
483 | message = 'Not Acceptable'
|
484 | break
|
485 | case 407:
|
486 | message = 'Proxy Authentication Required'
|
487 | break
|
488 | case 408:
|
489 | message = 'Request Timeout'
|
490 | break
|
491 | case 409:
|
492 | message = 'Conflict'
|
493 | break
|
494 | case 410:
|
495 | message = 'Gone'
|
496 | break
|
497 | case 411:
|
498 | message = 'Length Required'
|
499 | break
|
500 | case 412:
|
501 | message = 'Precondition Failed'
|
502 | break
|
503 | case 413:
|
504 | message = 'Payload Too Large'
|
505 | break
|
506 | case 414:
|
507 | message = 'URI Too Long'
|
508 | break
|
509 | case 415:
|
510 | message = 'Unsupported Media Type'
|
511 | break
|
512 | case 416:
|
513 | message = 'Requested Range Not Satisfiable'
|
514 | break
|
515 | case 417:
|
516 | message = 'Expectation Failed'
|
517 | break
|
518 | case 418:
|
519 | message = 'I\'m a teapot'
|
520 | break
|
521 | case 421:
|
522 | message = 'Misdirected Request'
|
523 | break
|
524 | case 422:
|
525 | message = 'Unprocessable Entity (WebDAV)'
|
526 | break
|
527 | case 423:
|
528 | message = 'Locked (WebDAV)'
|
529 | break
|
530 | case 424:
|
531 | message = 'Failed Dependency (WebDAV)'
|
532 | break
|
533 | case 425:
|
534 | message = 'Too Early'
|
535 | break
|
536 | case 426:
|
537 | message = 'Upgrade Required'
|
538 | break
|
539 | case 428:
|
540 | message = 'Precondition Required'
|
541 | break
|
542 | case 429:
|
543 | message = 'Too Many Requests'
|
544 | break
|
545 | case 431:
|
546 | message = 'Request Header Fields Too Large'
|
547 | break
|
548 | case 451:
|
549 | message = 'Unavailable For Legal Reasons'
|
550 | break
|
551 | case 500:
|
552 | message = 'Internal Server Error'
|
553 | break
|
554 | case 501:
|
555 | message = 'Not Implemented'
|
556 | break
|
557 | case 502:
|
558 | message = 'Bad Gateway'
|
559 | break
|
560 | case 503:
|
561 | message = 'Service Unavailable'
|
562 | break
|
563 | case 504:
|
564 | message = 'Gateway Timeout'
|
565 | break
|
566 | case 505:
|
567 | message = 'HTTP Version Not Supported'
|
568 | break
|
569 | case 506:
|
570 | message = 'Variant Also Negotiates'
|
571 | break
|
572 | case 507:
|
573 | message = 'Insufficient Storage'
|
574 | break
|
575 | case 508:
|
576 | message = 'Loop Detected (WebDAV)'
|
577 | break
|
578 | case 510:
|
579 | message = 'Not Extended'
|
580 | break
|
581 | case 511:
|
582 | message = 'Network Authentication Required'
|
583 | break
|
584 | default:
|
585 | message = 'Unknown'
|
586 | break
|
587 | }
|
588 |
|
589 | debug.log("status", status, message)
|
590 | }
|
591 |
|
592 | onSuccess(response, data) {
|
593 | debug.log("success", data)
|
594 |
|
595 | this.jsonResponse(response, { status: "success", data: data })
|
596 | }
|
597 |
|
598 | onError(response, error, data) {
|
599 | debug.log("error", error)
|
600 |
|
601 | let errorMessage = _.isError(error) ? error.message : error
|
602 |
|
603 | this.jsonResponse(response, { status: "error", error: errorMessage, data: data })
|
604 | }
|
605 |
|
606 | constructor({ service }) {
|
607 | debug.log("controller", service)
|
608 |
|
609 | let config = service.config.get("jwt")
|
610 |
|
611 | this._service = service
|
612 | this._authConfig = {
|
613 | tokenSecret: config.tokenSecret || 'secure',
|
614 | tokenAlgorithm: config.tokenAlgorithm || 'HS256',
|
615 | expiresTokenIn: config.expiresTokenIn || (60 * 60 * 24 * 7),
|
616 | refreshsTokenIn: config.refreshsTokenIn || (60 * 60 * 24 * 2)
|
617 | }
|
618 |
|
619 |
|
620 |
|
621 | const controller = this
|
622 | const propertyNames = Object.getOwnPropertyNames( this.constructor.prototype )
|
623 |
|
624 | _.each(propertyNames, propertyName => {
|
625 | if (propertyName.indexOf("on") === 0) {
|
626 | this[propertyName] = this[propertyName].bind(controller)
|
627 | }
|
628 | })
|
629 | }
|
630 |
|
631 | static __autoLoad( cwd, options = {}) {
|
632 | let handlers = []
|
633 | let { excludesAll, excludes, includes } = options
|
634 | let rule = options.rule || "handlers/*.js"
|
635 | let files = glob.sync(rule, { cwd: cwd })
|
636 |
|
637 | let priorityHandlers = {
|
638 | high: [],
|
639 | medium: [],
|
640 | low: []
|
641 | }
|
642 |
|
643 | _.each(files, file => {
|
644 | let filePath = path.resolve(cwd, file)
|
645 |
|
646 | let isIgnore = excludesAll === true
|
647 |
|
648 | if (excludes) {
|
649 | _.forEach(excludes, exclude => {
|
650 | if (filePath.indexOf(exclude) !== -1) {
|
651 | isIgnore = true
|
652 | }
|
653 | })
|
654 | }
|
655 |
|
656 | if (includes) {
|
657 | _.forEach(includes || [], include => {
|
658 | if (filePath.indexOf(include) !== -1) {
|
659 | isIgnore = false
|
660 | }
|
661 | })
|
662 | }
|
663 |
|
664 | if (!isIgnore) {
|
665 | delete require.cache[filePath]
|
666 | let handler = require(filePath)
|
667 | if (handler.priority === 'high') {
|
668 | priorityHandlers.high.push(handler)
|
669 | }
|
670 | else if (handler.defer === 'low' || handler.defer === true) {
|
671 | priorityHandlers.low.push(handler)
|
672 | }
|
673 | else {
|
674 | priorityHandlers.medium.push(handler)
|
675 | }
|
676 | }
|
677 | })
|
678 |
|
679 | _.each(priorityHandlers.high, handler => {
|
680 | handlers.push(handler)
|
681 | })
|
682 |
|
683 | _.each(priorityHandlers.medium, handler => {
|
684 | handlers.push(handler)
|
685 | })
|
686 |
|
687 | _.each(priorityHandlers.low, handler => {
|
688 | handlers.push(handler)
|
689 | })
|
690 |
|
691 | return handlers
|
692 | }
|
693 | }
|
694 |
|
695 | module.exports = Controller
|