/* eslint-disable no-bitwise */ const debug = require('debug')('ctrl:common'); const util = require('util'); const joi = require('joi'); const response = require('src/utils/response/index.js'); const config = require('src/config/index.js'); const { jwt } = require('src/utils/pkgs.js'); const { copyObject, toNumber } = require('src/utils/index.js'); const { Success, InternalError, DataFormat, Forbidden, Unauthorized } = response.resp; const controller = {}; module.exports = controller; /** * api reponse function * @param {import('src/utils/response/index.js').respObject} resp * @param {string|Object} body */ function responseFunc(resp, body) { /** @type {import('src/utils/response/index.js').respObject} */ const copy = copyObject(response.checkStruct(resp) ? resp : Success); if (typeof body === 'string') copy.object.message = body; else if (typeof body === 'object') copy.object = body; // @ts-ignore if (!('obj' in this)) this.obj = {}; this.obj.status = copy.status; this.obj.object = copy.object; } /** * api error response function * @param {import('src/utils/response/index.js').respObject} resp * @param {import('src/utils/response/index.js').codeMessage=} code */ function responseError(resp, code) { /** @type {import('src/utils/response/index.js').respObject} */ const copy = copyObject(response.checkStruct(resp) ? resp : InternalError); if (code && typeof code === 'object' && 'message' in code && 'code' in code) { copy.object = code; } const err = new response.APIError(copy.object.message, copy); throw err; } controller.apiHandler = () => async (ctx, next) => { ctx.obj = {}; ctx.token = {}; ctx.resp = responseFunc.bind(ctx); ctx.err = responseError; ctx.getBody = key => (ctx.request.body || {})[key]; ctx.getFile = key => (ctx.request.files || {})[key]; // run next try { await next(); } catch (err) { debug(`Get API Throw Error: ${util.inspect(err, false, null)}`); // debug(err.stack); if (!(err instanceof response.APIError)) { ctx.resp(InternalError); } else { ctx.obj = err.object; } if (process.env.NODE_ENV !== 'production') { ctx.obj.object.errorStack = err.stack; ctx.obj.object.errorMessage = err.message; } } if (Object.keys(ctx.obj).length > 0) { ctx.status = ctx.obj.status; ctx.body = ctx.obj.object; } }; /** * data validate middleware * @param {{query?: any, header?: any, body?: any}} schema body,query and header is joi.Schema */ controller.validate = schema => { if (typeof schema !== 'object') responseError(InternalError); const v = {}; if ('body' in schema) v.body = joi.isSchema(schema.body) ? schema.body : joi.object(schema.body).unknown(); if ('header' in schema) v.header = joi.isSchema(schema.header) ? schema.header : joi.object(schema.header).unknown(); if ('query' in schema) v.query = joi.isSchema(schema.query) ? schema.query : joi.object(schema.query).unknown(); return async (ctx, next) => { try { await joi.object(v).unknown().validateAsync({ query: ctx.query, header: ctx.headers, body: ctx.request.body }); } catch (err) { debug(`data validate error: ${util.inspect(err, false, null)}`); responseError(DataFormat); } return next(); }; }; /** * @param {boolean=} allowExpired * @return {import('koa').Middleware} */ controller.authorization = allowExpired => { return async (ctx, next) => { ctx.token = {}; /** @type {string} */ const token = ctx.get('authorization'); if (!token) ctx.err(Unauthorized); try { const strs = token.split(/\s/); debug(`Get Header: ${token}`); if (strs.length !== 2 || !/^bearer$/i.test(strs[0])) ctx.err(Unauthorized, response.codeMessage.CodeTokenInvalid); [, ctx.token.origin] = strs; let decoded = {}; let expired = false; try { decoded = jwt.verify(strs[1], config.server.jwt_secret); await joi .object({ user_id: joi.string().required(), }) .unknown() .validateAsync(decoded); } catch (err) { debug(`jwt token verify fail: ${util.inspect(err, false, null)}`); if (err instanceof jwt.TokenExpiredError) { decoded = jwt.decode(ctx.token.origin); expired = true; } else { throw err; } } ctx.token.user_id = decoded.user_id; ctx.token.sso = !!decoded.sso; if (expired) ctx.err(Forbidden, response.codeMessage.CodeTokenExpired); ctx.verified = true; } catch (err) { debug(`Token valid fail: ${util.inspect(err, false, null)}`); if (err instanceof response.APIError) { // 如果是過期的錯誤,判斷是否允許過期存取 // @ts-ignore // eslint-disable-next-line if (err._object?.object?.code === response.codeMessage.CodeTokenExpired.code) { if (!!allowExpired) return next(); } } throw err; } return next(); }; };