[feat] Update route format
This commit is contained in:
parent
b1e9c5e62a
commit
9174b540fd
@ -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;
|
||||||
|
@ -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, {});
|
||||||
|
};
|
@ -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);
|
||||||
|
|
||||||
return next();
|
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();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -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'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -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,
|
||||||
|
@ -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());
|
||||||
|
@ -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()
|
|
||||||
);
|
|
@ -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());
|
|
@ -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());
|
||||||
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
@ -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 ?? '',
|
||||||
|
Loading…
Reference in New Issue
Block a user