UNPKG

17.6 kBJavaScriptView Raw
1"use strict"
2
3const _ = require("lodash")
4const jwt = require("jsonwebtoken")
5const path = require("path")
6const glob = require("glob")
7const Model = require("./Model")
8const Debugger = require("./Debugger")
9
10const debug = new Debugger('motif:controller')
11
12class 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 // Auto Binding
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
695module.exports = Controller