[feat] Update route format

This commit is contained in:
JasonWu 2021-09-01 15:20:53 +08:00
parent b1e9c5e62a
commit 9174b540fd
11 changed files with 152 additions and 76 deletions

View File

@ -4,6 +4,7 @@ const constants = {
OPENID_EXPIRE: 300, // 5min OPENID_EXPIRE: 300, // 5min
INTERNAL_REGULATION_CACHE_TTL: 1800, // 30min INTERNAL_REGULATION_CACHE_TTL: 1800, // 30min
REPORT_CACHE_TTL: 600, // 10 min REPORT_CACHE_TTL: 600, // 10 min
ALLOW_GROUP_ROLE: ['Ironman3']
}; };
module.exports = constants; module.exports = constants;

View File

@ -24,3 +24,17 @@ controller.loginSSO = () => async ctx => {
ctx.resp(resp.Success, { url: u.toString() }); ctx.resp(resp.Success, { url: u.toString() });
}; };
controller.logout = () => async ctx => {
let link = '';
if (ctx.token.sso) {
link = sso.getLogoutURL();
}
ctx.resp(resp.Success, { url: link });
};
controller.getInfo = () => async ctx => {
ctx.resp(resp.Success, {});
};

View File

@ -3,9 +3,11 @@ const debug = require('debug')('ctrl:common');
const util = require('util'); const util = require('util');
const joi = require('joi'); const joi = require('joi');
const response = require('src/utils/response/index.js'); 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 { copyObject, toNumber } = require('src/utils/index.js');
const { Success, InternalError, DataFormat } = response.resp; const { Success, InternalError, DataFormat, Forbidden, Unauthorized } = response.resp;
const controller = {}; const controller = {};
module.exports = controller; module.exports = controller;
@ -101,13 +103,66 @@ controller.validate = schema => {
}; };
}; };
controller.getAppVersion = () => async (ctx, next) => { /**
// appVersion Format x.y.z (major.minor.patch) * @param {boolean=} allowExpired
const appVersion = ctx.get('x-app-version'); * @return {import('koa').Middleware}
const appBuildNumber = toNumber(ctx.get('x-app-buildnumber'), 0); */
const appPlatform = ctx.get('x-app-platform'); controller.authorization = allowExpired => {
return async (ctx, next) => {
ctx.token = {};
/** @type {string} */
const token = ctx.get('authorization');
Object.assign(ctx.state, { appVersion, appBuildNumber, appPlatform }); 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(); return next();
};
}; };

View File

@ -5,24 +5,3 @@ controller.healthCheck = async ctx => {
ctx.body = 'ok'; ctx.body = 'ok';
ctx.status = 200; ctx.status = 200;
}; };
controller.appleAppSiteAssociation = async ctx => {
ctx.status = 200;
ctx.body = {
applinks: {
details: [
{
appID: 'CL3K9D5FDN.com.lawsnote.college.staging',
paths: ['*'],
},
{
appID: 'CL3K9D5FDN.com.lawsnote.college',
paths: ['*'],
},
],
},
webcredentials: {
apps: ['CL3K9D5FDN.com.lawsnote.college.staging', 'CL3K9D5FDN.com.lawsnote.college'],
},
};
};

View File

@ -36,7 +36,7 @@ controller.verifyCode = () => async ctx => {
// generate jwt token // generate jwt token
const jwtToken = jwt.sign( const jwtToken = jwt.sign(
{ {
user_id: `${token}-id`, user_id: token.user_id,
sso: true, sso: true,
}, },
config.server.jwt_secret, config.server.jwt_secret,

View File

@ -1,12 +1,58 @@
const Router = require('@koa/router'); const Router = require('@koa/router');
const joi = require('joi');
const commonCtrl = require('src/controllers/common/index.js'); const commonCtrl = require('src/controllers/common/index.js');
const v1Router = require('./v1/index.js'); const accCtrl = require('src/controllers/account/index.js');
const r = new Router({ prefix: '/api' }); const r = new Router({ prefix: '/api' });
module.exports = r; module.exports = r;
// set api handler middleware // set api handler middleware
r.use(commonCtrl.apiHandler(), commonCtrl.getAppVersion()); r.use(commonCtrl.apiHandler());
r.use(v1Router.routes()); /**
* get account info
* @swagger
* @route GET /api/login
* @group account - account apis
* @param {string} back_url.query.required - back to url
* @returns {RespDefault.model} default -
*/
r.get(
'/login',
commonCtrl.validate({
query: {
back_url: joi.string().required(),
},
}),
accCtrl.loginSSO()
);
/**
* account refresh token
* @swagger
* @route POST /api/refresh
* @group account - account apis
* @security JWT
* @returns {RespDefault.model} default -
*/
r.post('/refresh', commonCtrl.authorization(true), accCtrl.logout());
/**
* account logout
* @swagger
* @route POST /api/logout
* @group account - account apis
* @security JWT
* @returns {RespDefault.model} default -
*/
r.post('/logout', commonCtrl.authorization(false), accCtrl.logout());
/**
* account get info
* @swagger
* @route GET /api/userinfo
* @group account - account apis
* @security JWT
* @returns {RespDefault.model} default -
*/
r.get('/userinfo', commonCtrl.authorization(false), accCtrl.getInfo());

View File

@ -1,25 +0,0 @@
const Router = require('@koa/router');
const joi = require('joi');
const commonCtrl = require('src/controllers/common/index.js');
const accCtrl = require('src/controllers/account/v1/index.js');
const r = new Router({ prefix: '/account' });
module.exports = r;
/**
* get account info
* @swagger
* @route GET /api/v1/account/login/sso
* @group account - account apis
* @param {string} back_url.query.required - back to url
* @returns {RespDefault.model} default -
*/
r.get(
'/login/sso',
commonCtrl.validate({
query: {
back_url: joi.string().required(),
},
}),
accCtrl.loginSSO()
);

View File

@ -1,8 +0,0 @@
const Router = require('@koa/router');
const accountRouter = require('./account/index.js');
const r = new Router({ prefix: '/v1' });
module.exports = r;
r.use(accountRouter.routes());

View File

@ -8,7 +8,6 @@ const r = new Router();
module.exports = r; module.exports = r;
r.get('/', controller.healthCheck); r.get('/', controller.healthCheck);
r.get(['/apple-app-site-association', '/.well-known/apple-app-site-association'], controller.appleAppSiteAssociation);
r.use(apiRouter.routes()); r.use(apiRouter.routes());
r.use(oauthRouter.routes()); r.use(oauthRouter.routes());

View File

@ -1,11 +1,11 @@
const IORedis = require('ioredis'); const IORedis = require("ioredis");
const config = require('src/config/index.js'); const config = require("src/config/index.js");
class Redis extends IORedis { class Redis extends IORedis {
constructor() { constructor() {
let { prefix } = config.redis; let { prefix } = config.redis;
const { host, port, password, db } = config.redis; const { host, port, password, db } = config.redis;
if (prefix && !/:$/.test(prefix)) prefix += ':'; if (prefix && !/:$/.test(prefix)) prefix += ":";
super({ super({
host, host,
port, port,
@ -23,7 +23,13 @@ class Redis extends IORedis {
* @param {string} s state * @param {string} s state
* @return {string} * @return {string}
*/ */
ssoLoginCache: s => self.getKeyWithPrefix(`sso-login:${s}`), ssoLoginCache: (s) => self.getKeyWithPrefix(`sso-login:${s}`),
/**
* 儲存 Token
* @param {string} s state
* @return {string}
*/
userToken: (s) => self.getKeyWithPrefix(`token:${s}`),
}; };
} }
@ -33,7 +39,7 @@ class Redis extends IORedis {
* @return {string} * @return {string}
*/ */
getKeyWithPrefix(s) { getKeyWithPrefix(s) {
if (typeof s !== 'string') throw new Error('input key not a string'); if (typeof s !== "string") throw new Error("input key not a string");
return `${this.prefix}${s}`; return `${this.prefix}${s}`;
} }

View File

@ -42,6 +42,9 @@ mod.getAuthURL = state => {
return `${value.authorized_endpoint}?${querystring.stringify(qs)}`; return `${value.authorized_endpoint}?${querystring.stringify(qs)}`;
}; };
/**
* @return {string}
*/
mod.getLogoutURL = () => { mod.getLogoutURL = () => {
const input = joi const input = joi
.object({ .object({
@ -59,6 +62,9 @@ mod.getLogoutURL = () => {
/** /**
* @typedef SSOAccount * @typedef SSOAccount
* @property {string} access_token
* @property {string} refresh_token
* @property {string} user_id
* @property {string} username * @property {string} username
* @property {string} display_name * @property {string} display_name
* @property {string} email * @property {string} email
@ -106,13 +112,13 @@ mod.getToken = async (code, state) => {
const { body } = resp; const { body } = resp;
if (!body) throw new Error('resopnse body empty'); if (!body) throw new Error('resopnse body empty');
const { id_token: idToken, access_token: accessToken } = body; const { id_token: idToken, access_token: accessToken, refresh_token: refreshToken } = body;
if (!idToken) throw new Error('get id token fail'); if (!idToken) throw new Error('get id token fail');
const decoded = jwt.decode(idToken); const decoded = jwt.decode(idToken);
if (!decoded || typeof decoded !== 'object') throw new Error('jwt decode fail'); if (!decoded || typeof decoded !== 'object') throw new Error('jwt decode fail');
console.log(decoded) console.log('decoded ::: ', decoded)
console.log(body) console.log('body ::: ', body)
// @ts-ignore // @ts-ignore
const { preferred_username: preferredUsername } = decoded; const { preferred_username: preferredUsername } = decoded;
if (!preferredUsername) throw new Error('id token field missing'); if (!preferredUsername) throw new Error('id token field missing');
@ -121,6 +127,9 @@ mod.getToken = async (code, state) => {
/** @type {SSOAccount} */ /** @type {SSOAccount} */
const ssoAccount = { const ssoAccount = {
access_token: accessToken,
refresh_token: refreshToken,
user_id: decoded.sub,
username: preferredUsername.toLowerCase(), username: preferredUsername.toLowerCase(),
display_name: displayName ?? preferredUsername, display_name: displayName ?? preferredUsername,
email: decoded.email ?? '', email: decoded.email ?? '',