update
This commit is contained in:
parent
b91ce62aa4
commit
2e05f90851
@ -1,38 +1,18 @@
|
|||||||
const { env } = process;
|
const { env } = process
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
server: {
|
server: {
|
||||||
<<<<<<< HEAD
|
|
||||||
url: env.SERVER_URL || "http://localhost:10230",
|
|
||||||
=======
|
|
||||||
url: env.SERVER_URL || 'http://localhost:10230',
|
url: env.SERVER_URL || 'http://localhost:10230',
|
||||||
>>>>>>> c96cdf0ebd17f805235c6fa9eecf2ea79ecca19b
|
|
||||||
port: parseInt(env.SERVER_PORT, 10) || 10230,
|
port: parseInt(env.SERVER_PORT, 10) || 10230,
|
||||||
jwt_secret: env.SERVER_JWT_SECRET || "testsecret",
|
jwt_secret: env.SERVER_JWT_SECRET || 'testsecret',
|
||||||
jwt_expire: parseInt(env.SERVER_JWT_EXPIRE, 10) || 60 * 60 * 24 * 30, // 30 day
|
jwt_expire: parseInt(env.SERVER_JWT_EXPIRE, 10) || 60 * 60 * 24 * 30 // 30 day
|
||||||
<<<<<<< HEAD
|
|
||||||
},
|
|
||||||
sso: {
|
|
||||||
authorized_endpoint: env.SSO_AUTHORIZED_ENDPOINT || "",
|
|
||||||
token_endpoint: env.SSO_TOKEN_ENDPOINT || "",
|
|
||||||
logout_endpoint: env.SSO_LOGOUT_ENDPOINT || "",
|
|
||||||
client_id: env.SSO_CLIENT_ID || "",
|
|
||||||
client_secret: env.SSO_CLIENT_SECRET || "",
|
|
||||||
=======
|
|
||||||
},
|
|
||||||
redis: {
|
|
||||||
host: env.REDIS_HOST || 'localhost',
|
|
||||||
port: parseInt(env.REDIS_PORT, 10) || 6379,
|
|
||||||
password: env.REDIS_PASSWORD || '',
|
|
||||||
prefix: env.REDIS_PREFIX || '',
|
|
||||||
db: parseInt(env.REDIS_DB, 10) || 0,
|
|
||||||
},
|
},
|
||||||
sso: {
|
sso: {
|
||||||
authorized_endpoint: env.SSO_AUTHORIZED_ENDPOINT || '',
|
authorized_endpoint: env.SSO_AUTHORIZED_ENDPOINT || '',
|
||||||
token_endpoint: env.SSO_TOKEN_ENDPOINT || '',
|
token_endpoint: env.SSO_TOKEN_ENDPOINT || '',
|
||||||
|
userinfo_endpoint: env.SSO_USERINFO_ENDPOINT || '',
|
||||||
logout_endpoint: env.SSO_LOGOUT_ENDPOINT || '',
|
logout_endpoint: env.SSO_LOGOUT_ENDPOINT || '',
|
||||||
client_id: env.SSO_CLIENT_ID || '',
|
client_id: env.SSO_CLIENT_ID || '',
|
||||||
client_secret: env.SSO_CLIENT_SECRET || '',
|
client_secret: env.SSO_CLIENT_SECRET || ''
|
||||||
>>>>>>> c96cdf0ebd17f805235c6fa9eecf2ea79ecca19b
|
}
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
@ -2,13 +2,7 @@
|
|||||||
const constants = {
|
const constants = {
|
||||||
PAGE_SIZE: 20,
|
PAGE_SIZE: 20,
|
||||||
OPENID_EXPIRE: 300, // 5min
|
OPENID_EXPIRE: 300, // 5min
|
||||||
<<<<<<< HEAD
|
|
||||||
ALLOW_GROUP_ROLE: ["Ironman3"],
|
ALLOW_GROUP_ROLE: ["Ironman3"],
|
||||||
=======
|
|
||||||
INTERNAL_REGULATION_CACHE_TTL: 1800, // 30min
|
|
||||||
REPORT_CACHE_TTL: 600, // 10 min
|
|
||||||
ALLOW_GROUP_ROLE: ['Ironman3'] // 允許的 Group 身份
|
|
||||||
>>>>>>> c96cdf0ebd17f805235c6fa9eecf2ea79ecca19b
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = constants;
|
module.exports = constants;
|
||||||
|
@ -1,41 +1,40 @@
|
|||||||
const { resp } = require("src/utils/response/index.js");
|
const { resp } = require('src/utils/response/index.js')
|
||||||
const { get: getCacheInstance } = require("src/utils/cache.js");
|
const { get: getCacheInstance } = require('src/utils/cache.js')
|
||||||
const sso = require("src/utils/sso/index.js");
|
const sso = require('src/utils/sso/index.js')
|
||||||
const { OPENID_EXPIRE } = require("src/constants/index.js");
|
const uuid = require('uuid')
|
||||||
const uuid = require("uuid");
|
const url = require('url')
|
||||||
const url = require("url");
|
|
||||||
|
|
||||||
const controller = {};
|
const controller = {}
|
||||||
module.exports = controller;
|
module.exports = controller
|
||||||
|
|
||||||
controller.loginSSO = () => async (ctx) => {
|
controller.loginSSO = () => async (ctx) => {
|
||||||
const { back_url: backURL } = ctx.query;
|
const { back_url: backURL } = ctx.query
|
||||||
|
|
||||||
const state = uuid.v4();
|
const state = uuid.v4()
|
||||||
|
|
||||||
const authURL = sso.getAuthURL(state);
|
const authURL = sso.getAuthURL(state)
|
||||||
|
|
||||||
// store back url to cache
|
// store back url to cache
|
||||||
const cacheKey = `login-${state}`;
|
const cacheKey = `login-${state}`
|
||||||
const cache = getCacheInstance();
|
const cache = getCacheInstance()
|
||||||
|
|
||||||
cache.set(cacheKey, JSON.stringify({ back_url: backURL }), true);
|
cache.set(cacheKey, JSON.stringify({ back_url: backURL }), true)
|
||||||
|
|
||||||
const u = new url.URL(authURL);
|
const u = new url.URL(authURL)
|
||||||
|
|
||||||
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.logout = () => async (ctx) => {
|
||||||
};
|
let link = ''
|
||||||
|
|
||||||
|
if (ctx.token.sso) {
|
||||||
|
link = sso.getLogoutURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.resp(resp.Success, { url: link })
|
||||||
|
}
|
||||||
|
|
||||||
controller.getInfo = () => async (ctx) => {
|
controller.getInfo = () => async (ctx) => {
|
||||||
ctx.resp(resp.Success, {});
|
ctx.resp(resp.Success, {})
|
||||||
};
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
const debug = require('debug')('ctrl:common');
|
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 config = require('src/config/index.js')
|
||||||
const { jwt } = require('src/utils/pkgs.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, Forbidden, Unauthorized } = response.resp;
|
const { Success, InternalError, DataFormat, Forbidden, Unauthorized } = response.resp
|
||||||
|
|
||||||
const controller = {};
|
const controller = {}
|
||||||
module.exports = controller;
|
module.exports = controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* api reponse function
|
* api reponse function
|
||||||
@ -19,15 +19,15 @@ module.exports = controller;
|
|||||||
*/
|
*/
|
||||||
function responseFunc (resp, body) {
|
function responseFunc (resp, body) {
|
||||||
/** @type {import('src/utils/response/index.js').respObject} */
|
/** @type {import('src/utils/response/index.js').respObject} */
|
||||||
const copy = copyObject(response.checkStruct(resp) ? resp : Success);
|
const copy = copyObject(response.checkStruct(resp) ? resp : Success)
|
||||||
|
|
||||||
if (typeof body === 'string') copy.object.message = body;
|
if (typeof body === 'string') copy.object.message = body
|
||||||
else if (typeof body === 'object') copy.object = body;
|
else if (typeof body === 'object') copy.object = body
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (!('obj' in this)) this.obj = {};
|
if (!('obj' in this)) this.obj = {}
|
||||||
this.obj.status = copy.status;
|
this.obj.status = copy.status
|
||||||
this.obj.object = copy.object;
|
this.obj.object = copy.object
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,71 +37,71 @@ function responseFunc(resp, body) {
|
|||||||
*/
|
*/
|
||||||
function responseError (resp, code) {
|
function responseError (resp, code) {
|
||||||
/** @type {import('src/utils/response/index.js').respObject} */
|
/** @type {import('src/utils/response/index.js').respObject} */
|
||||||
const copy = copyObject(response.checkStruct(resp) ? resp : InternalError);
|
const copy = copyObject(response.checkStruct(resp) ? resp : InternalError)
|
||||||
|
|
||||||
if (code && typeof code === 'object' && 'message' in code && 'code' in code) {
|
if (code && typeof code === 'object' && 'message' in code && 'code' in code) {
|
||||||
copy.object = code;
|
copy.object = code
|
||||||
}
|
}
|
||||||
|
|
||||||
const err = new response.APIError(copy.object.message, copy);
|
const err = new response.APIError(copy.object.message, copy)
|
||||||
throw err;
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.apiHandler = () => async (ctx, next) => {
|
controller.apiHandler = () => async (ctx, next) => {
|
||||||
ctx.obj = {};
|
ctx.obj = {}
|
||||||
ctx.token = {};
|
ctx.token = {}
|
||||||
|
|
||||||
ctx.resp = responseFunc.bind(ctx);
|
ctx.resp = responseFunc.bind(ctx)
|
||||||
ctx.err = responseError;
|
ctx.err = responseError
|
||||||
|
|
||||||
ctx.getBody = key => (ctx.request.body || {})[key];
|
ctx.getBody = key => (ctx.request.body || {})[key]
|
||||||
ctx.getFile = key => (ctx.request.files || {})[key];
|
ctx.getFile = key => (ctx.request.files || {})[key]
|
||||||
|
|
||||||
// run next
|
// run next
|
||||||
try {
|
try {
|
||||||
await next();
|
await next()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug(`Get API Throw Error: ${util.inspect(err, false, null)}`);
|
debug(`Get API Throw Error: ${util.inspect(err, false, null)}`)
|
||||||
// debug(err.stack);
|
// debug(err.stack);
|
||||||
if (!(err instanceof response.APIError)) {
|
if (!(err instanceof response.APIError)) {
|
||||||
ctx.resp(InternalError);
|
ctx.resp(InternalError)
|
||||||
} else {
|
} else {
|
||||||
ctx.obj = err.object;
|
ctx.obj = err.object
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
ctx.obj.object.errorStack = err.stack;
|
ctx.obj.object.errorStack = err.stack
|
||||||
ctx.obj.object.errorMessage = err.message;
|
ctx.obj.object.errorMessage = err.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(ctx.obj).length > 0) {
|
if (Object.keys(ctx.obj).length > 0) {
|
||||||
ctx.status = ctx.obj.status;
|
ctx.status = ctx.obj.status
|
||||||
ctx.body = ctx.obj.object;
|
ctx.body = ctx.obj.object
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* data validate middleware
|
* data validate middleware
|
||||||
* @param {{query?: any, header?: any, body?: any}} schema body,query and header is joi.Schema
|
* @param {{query?: any, header?: any, body?: any}} schema body,query and header is joi.Schema
|
||||||
*/
|
*/
|
||||||
controller.validate = schema => {
|
controller.validate = schema => {
|
||||||
if (typeof schema !== 'object') responseError(InternalError);
|
if (typeof schema !== 'object') responseError(InternalError)
|
||||||
const v = {};
|
const v = {}
|
||||||
if ('body' in schema) v.body = joi.isSchema(schema.body) ? schema.body : joi.object(schema.body).unknown();
|
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 ('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();
|
if ('query' in schema) v.query = joi.isSchema(schema.query) ? schema.query : joi.object(schema.query).unknown()
|
||||||
|
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
try {
|
try {
|
||||||
await joi.object(v).unknown().validateAsync({ query: ctx.query, header: ctx.headers, body: ctx.request.body });
|
await joi.object(v).unknown().validateAsync({ query: ctx.query, header: ctx.headers, body: ctx.request.body })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug(`data validate error: ${util.inspect(err, false, null)}`);
|
debug(`data validate error: ${util.inspect(err, false, null)}`)
|
||||||
responseError(DataFormat);
|
responseError(DataFormat)
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return next();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} allowExpired
|
* @param {boolean=} allowExpired
|
||||||
@ -109,60 +109,60 @@ controller.validate = schema => {
|
|||||||
*/
|
*/
|
||||||
controller.authorization = allowExpired => {
|
controller.authorization = allowExpired => {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
ctx.token = {};
|
ctx.token = {}
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
const token = ctx.get('authorization');
|
const token = ctx.get('authorization')
|
||||||
|
|
||||||
if (!token) ctx.err(Unauthorized);
|
if (!token) ctx.err(Unauthorized)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const strs = token.split(/\s/);
|
const strs = token.split(/\s/)
|
||||||
debug(`Get Header: ${token}`);
|
debug(`Get Header: ${token}`)
|
||||||
if (strs.length !== 2 || !/^bearer$/i.test(strs[0])) ctx.err(Unauthorized, response.codeMessage.CodeTokenInvalid);
|
if (strs.length !== 2 || !/^bearer$/i.test(strs[0])) ctx.err(Unauthorized, response.codeMessage.CodeTokenInvalid);
|
||||||
|
|
||||||
[, ctx.token.origin] = strs;
|
[, ctx.token.origin] = strs
|
||||||
|
|
||||||
let decoded = {};
|
let decoded = {}
|
||||||
let expired = false;
|
let expired = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
decoded = jwt.verify(strs[1], config.server.jwt_secret);
|
decoded = jwt.verify(strs[1], config.server.jwt_secret)
|
||||||
|
|
||||||
await joi
|
await joi
|
||||||
.object({
|
.object({
|
||||||
user_id: joi.string().required(),
|
user_id: joi.string().required()
|
||||||
})
|
})
|
||||||
.unknown()
|
.unknown()
|
||||||
.validateAsync(decoded);
|
.validateAsync(decoded)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug(`jwt token verify fail: ${util.inspect(err, false, null)}`);
|
debug(`jwt token verify fail: ${util.inspect(err, false, null)}`)
|
||||||
if (err instanceof jwt.TokenExpiredError) {
|
if (err instanceof jwt.TokenExpiredError) {
|
||||||
decoded = jwt.decode(ctx.token.origin);
|
decoded = jwt.decode(ctx.token.origin)
|
||||||
expired = true;
|
expired = true
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.token.user_id = decoded.user_id;
|
ctx.token.user_id = decoded.user_id
|
||||||
ctx.token.sso = !!decoded.sso;
|
ctx.token.sso = !!decoded.sso
|
||||||
|
|
||||||
if (expired) ctx.err(Forbidden, response.codeMessage.CodeTokenExpired);
|
if (expired) ctx.err(Forbidden, response.codeMessage.CodeTokenExpired)
|
||||||
|
|
||||||
ctx.verified = true;
|
ctx.verified = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug(`Token valid fail: ${util.inspect(err, false, null)}`);
|
debug(`Token valid fail: ${util.inspect(err, false, null)}`)
|
||||||
if (err instanceof response.APIError) {
|
if (err instanceof response.APIError) {
|
||||||
// 如果是過期的錯誤,判斷是否允許過期存取
|
// 如果是過期的錯誤,判斷是否允許過期存取
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
if (err._object?.object?.code === response.codeMessage.CodeTokenExpired.code) {
|
if (err._object?.object?.code === response.codeMessage.CodeTokenExpired.code) {
|
||||||
if (!!allowExpired) return next();
|
if (allowExpired) return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw err;
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next()
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const controller = {};
|
const controller = {}
|
||||||
module.exports = controller;
|
module.exports = controller
|
||||||
|
|
||||||
controller.healthCheck = async ctx => {
|
controller.healthCheck = async ctx => {
|
||||||
ctx.body = 'ok';
|
ctx.body = 'ok'
|
||||||
ctx.status = 200;
|
ctx.status = 200
|
||||||
};
|
}
|
||||||
|
@ -1,80 +1,70 @@
|
|||||||
const debug = require("debug")("ctrl:common");
|
const debug = require('debug')('ctrl:common')
|
||||||
const util = require("util");
|
const util = require('util')
|
||||||
const url = require("url");
|
const url = require('url')
|
||||||
const sso = require("src/utils/sso/index.js");
|
const sso = require('src/utils/sso/index.js')
|
||||||
const { get: getCacheInstance } = require("src/utils/cache.js");
|
const { get: getCacheInstance } = require('src/utils/cache.js')
|
||||||
const { codeMessage, APIError } = require("src/utils/response/index.js");
|
const { codeMessage, APIError } = require('src/utils/response/index.js')
|
||||||
const config = require("src/config/index.js");
|
const config = require('src/config/index.js')
|
||||||
const { jwt } = require("src/utils/pkgs.js");
|
const { jwt } = require('src/utils/pkgs.js')
|
||||||
|
|
||||||
const controller = {};
|
const controller = {}
|
||||||
module.exports = controller;
|
module.exports = controller
|
||||||
|
|
||||||
controller.verifyCode = () => async (ctx) => {
|
controller.verifyCode = () => async (ctx) => {
|
||||||
const { code, session_state: sessionState, state } = ctx.query;
|
const { code, session_state: sessionState, state } = ctx.query
|
||||||
|
|
||||||
// logout flow redirect tot frontend
|
// logout flow redirect tot frontend
|
||||||
if (state === "logout") {
|
if (state === 'logout') {
|
||||||
ctx.redirect(config.server.frontend_url);
|
ctx.redirect(config.server.frontend_url)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// get back url from redis
|
// get back url from redis
|
||||||
const cacheKey = `login-${state}`;
|
const cacheKey = `login-${state}`
|
||||||
const cache = getCacheInstance();
|
const cache = getCacheInstance()
|
||||||
|
|
||||||
const data = cache.get(cacheKey);
|
const data = cache.get(cacheKey)
|
||||||
if (!data) ctx.throw("get login cache fail");
|
if (!data) ctx.throw('get login cache fail')
|
||||||
const stateObj = JSON.parse(data);
|
const stateObj = JSON.parse(data)
|
||||||
const { back_url: backURL } = stateObj;
|
const { back_url: backURL } = stateObj
|
||||||
if (!backURL) ctx.throw("cache data missing");
|
if (!backURL) ctx.throw('cache data missing')
|
||||||
|
|
||||||
const u = new url.URL(backURL);
|
const u = new url.URL(backURL)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = await sso.getToken(code, sessionState);
|
const token = await sso.getToken(code, sessionState)
|
||||||
|
|
||||||
// generate jwt token
|
// set accessToken/refreshToken cache
|
||||||
const jwtToken = jwt.sign(
|
cache.set(token.access_token, token.refresh_token, false)
|
||||||
{
|
|
||||||
user_id: token.user_id,
|
|
||||||
sso: true,
|
|
||||||
},
|
|
||||||
config.server.jwt_secret,
|
|
||||||
{
|
|
||||||
expiresIn: config.server.jwt_expire,
|
|
||||||
issuer: "lawsnote",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
u.searchParams.append(
|
u.searchParams.append(
|
||||||
"success",
|
'success',
|
||||||
Buffer.from(JSON.stringify({ token: jwtToken })).toString("base64")
|
Buffer.from(JSON.stringify({ token: token.access_token })).toString('base64')
|
||||||
);
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cache.del(cacheKey);
|
cache.del(cacheKey)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug(`delete cache fail: ${util.inspect(err, false, null)}`);
|
debug(`delete cache fail: ${util.inspect(err, false, null)}`)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug(`openid verify fail: ${util.inspect(err, false, null)}`);
|
debug(`openid verify fail: ${util.inspect(err, false, null)}`)
|
||||||
|
|
||||||
/** @type {object} */
|
/** @type {object} */
|
||||||
const errObj = { ...codeMessage.CodeInternalError };
|
const errObj = { ...codeMessage.CodeInternalError }
|
||||||
|
|
||||||
if (err instanceof APIError) {
|
if (err instanceof APIError) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
Object.assign(errObj, err.object.object);
|
Object.assign(errObj, err.object.object)
|
||||||
}
|
}
|
||||||
|
|
||||||
errObj.errorStack = err.stack;
|
errObj.errorStack = err.stack
|
||||||
errObj.errorMessage = err.message;
|
errObj.errorMessage = err.message
|
||||||
u.searchParams.append(
|
u.searchParams.append(
|
||||||
"error",
|
'error',
|
||||||
Buffer.from(JSON.stringify(errObj)).toString("base64")
|
Buffer.from(JSON.stringify(errObj)).toString('base64')
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.redirect(u.toString());
|
ctx.redirect(u.toString())
|
||||||
};
|
}
|
||||||
|
16
index.js
16
index.js
@ -1,15 +1,15 @@
|
|||||||
require("dotenv").config();
|
require('dotenv').config()
|
||||||
|
|
||||||
const config = require("src/config/index.js");
|
const config = require('src/config/index.js')
|
||||||
const { new: newCacheInstance } = require("src/utils/cache.js");
|
const { new: newCacheInstance } = require('src/utils/cache.js')
|
||||||
const app = require("./server.js");
|
const app = require('./server.js')
|
||||||
|
|
||||||
async function runServer () {
|
async function runServer () {
|
||||||
newCacheInstance();
|
newCacheInstance()
|
||||||
const server = app.listen(config.server.port, () => {
|
const server = app.listen(config.server.port, () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
console.info(`server start on port ${server.address().port}`);
|
console.info(`server start on port ${server.address().port}`)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
runServer();
|
runServer()
|
||||||
|
13346
package-lock.json
generated
13346
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -5,10 +5,6 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"test": "mocha --timeout 5000 --exit test/ && jest --passWithNoTests --runInBand --coverage .",
|
|
||||||
>>>>>>> c96cdf0ebd17f805235c6fa9eecf2ea79ecca19b
|
|
||||||
"postinstall": "node -e \"var s='../',d='node_modules/src',fs=require('fs');fs.exists(d,function(e){e||fs.symlinkSync(s,d,'dir')});\""
|
"postinstall": "node -e \"var s='../',d='node_modules/src',fs=require('fs');fs.exists(d,function(e){e||fs.symlinkSync(s,d,'dir')});\""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@ -34,13 +30,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "4.2.0",
|
"chai": "4.2.0",
|
||||||
"eslint": "^7.2.0",
|
"eslint": "^7.2.0",
|
||||||
"eslint-config-airbnb-base": "^14.2.0",
|
|
||||||
"eslint-config-prettier": "^6.11.0",
|
|
||||||
"eslint-plugin-import": "^2.21.2",
|
|
||||||
"eslint-plugin-prettier": "^3.1.3",
|
|
||||||
"jest": "^26.6.0",
|
"jest": "^26.6.0",
|
||||||
"mocha": "^8.2.0",
|
"mocha": "^8.2.0",
|
||||||
"prettier": "^2.0.5",
|
"standard": "^16.0.3",
|
||||||
"supertest": "^5.0.0"
|
"supertest": "^5.0.0"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
const Router = require('@koa/router');
|
const Router = require('@koa/router')
|
||||||
const joi = require('joi');
|
const joi = require('joi')
|
||||||
const commonCtrl = require('src/controllers/common/index.js');
|
const commonCtrl = require('src/controllers/common/index.js')
|
||||||
const accCtrl = require('src/controllers/account/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());
|
r.use(commonCtrl.apiHandler())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get account info
|
* get account info
|
||||||
@ -21,11 +21,11 @@ r.use(commonCtrl.apiHandler());
|
|||||||
'/login',
|
'/login',
|
||||||
commonCtrl.validate({
|
commonCtrl.validate({
|
||||||
query: {
|
query: {
|
||||||
back_url: joi.string().required(),
|
back_url: joi.string().required()
|
||||||
},
|
}
|
||||||
}),
|
}),
|
||||||
accCtrl.loginSSO()
|
accCtrl.loginSSO()
|
||||||
);
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* account refresh token
|
* account refresh token
|
||||||
@ -35,7 +35,7 @@ r.use(commonCtrl.apiHandler());
|
|||||||
* @security JWT
|
* @security JWT
|
||||||
* @returns {RespDefault.model} default -
|
* @returns {RespDefault.model} default -
|
||||||
*/
|
*/
|
||||||
r.post('/refresh', commonCtrl.authorization(true), accCtrl.logout());
|
r.post('/refresh', commonCtrl.authorization(true), accCtrl.logout())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* account logout
|
* account logout
|
||||||
@ -45,7 +45,7 @@ r.post('/refresh', commonCtrl.authorization(true), accCtrl.logout());
|
|||||||
* @security JWT
|
* @security JWT
|
||||||
* @returns {RespDefault.model} default -
|
* @returns {RespDefault.model} default -
|
||||||
*/
|
*/
|
||||||
r.post('/logout', commonCtrl.authorization(false), accCtrl.logout());
|
r.post('/logout', commonCtrl.authorization(false), accCtrl.logout())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* account get info
|
* account get info
|
||||||
@ -55,4 +55,4 @@ r.post('/logout', commonCtrl.authorization(false), accCtrl.logout());
|
|||||||
* @security JWT
|
* @security JWT
|
||||||
* @returns {RespDefault.model} default -
|
* @returns {RespDefault.model} default -
|
||||||
*/
|
*/
|
||||||
r.get('/userinfo', commonCtrl.authorization(false), accCtrl.getInfo());
|
r.get('/userinfo', commonCtrl.authorization(false), accCtrl.getInfo())
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
const Router = require('@koa/router');
|
const Router = require('@koa/router')
|
||||||
|
|
||||||
const controller = require('src/controllers/index.js');
|
const controller = require('src/controllers/index.js')
|
||||||
const apiRouter = require('./api/index.js');
|
const apiRouter = require('./api/index.js')
|
||||||
const oauthRouter = require('./oauth/index.js');
|
const oauthRouter = require('./oauth/index.js')
|
||||||
|
|
||||||
const r = new Router();
|
const r = new Router()
|
||||||
module.exports = r;
|
module.exports = r
|
||||||
|
|
||||||
r.get('/', controller.healthCheck);
|
r.get('/', controller.healthCheck)
|
||||||
|
|
||||||
r.use(apiRouter.routes());
|
r.use(apiRouter.routes())
|
||||||
r.use(oauthRouter.routes());
|
r.use(oauthRouter.routes())
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
const Router = require('@koa/router');
|
const Router = require('@koa/router')
|
||||||
const oauthCtrl = require('src/controllers/oauth/index.js');
|
const oauthCtrl = require('src/controllers/oauth/index.js')
|
||||||
|
|
||||||
const r = new Router({ prefix: '/oauth' });
|
const r = new Router({ prefix: '/oauth' })
|
||||||
|
|
||||||
r.get('/redirect', oauthCtrl.verifyCode());
|
r.get('/redirect', oauthCtrl.verifyCode())
|
||||||
|
|
||||||
module.exports = r;
|
module.exports = r
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
module.exports = {};
|
module.exports = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef RespDefault
|
* @typedef RespDefault
|
||||||
@ -9,4 +9,3 @@ module.exports = {};
|
|||||||
* @property {string} errorStack api error stack (除了prod以外的環境會有)
|
* @property {string} errorStack api error stack (除了prod以外的環境會有)
|
||||||
* @property {string} errorMessage api error message (除了prod以外的環境會有)
|
* @property {string} errorMessage api error message (除了prod以外的環境會有)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
86
server.js
86
server.js
@ -1,28 +1,28 @@
|
|||||||
const Koa = require('koa');
|
const Koa = require('koa')
|
||||||
const path = require('path');
|
const path = require('path')
|
||||||
const url = require('url');
|
const url = require('url')
|
||||||
const swaggerGenerator = require('@mtfos/swagger-generator');
|
const swaggerGenerator = require('@mtfos/swagger-generator')
|
||||||
const { copyObject } = require('src/utils/index.js');
|
const { copyObject } = require('src/utils/index.js')
|
||||||
|
|
||||||
const app = new Koa();
|
const app = new Koa()
|
||||||
const config = require('src/config/index.js');
|
const config = require('src/config/index.js')
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
app.proxy = true;
|
app.proxy = true
|
||||||
|
|
||||||
// const server = app.listen(config.server.port, () => {
|
// const server = app.listen(config.server.port, () => {
|
||||||
// console.log(`server start on port ${server.address().port}`); // eslint-disable-line
|
// console.log(`server start on port ${server.address().port}`); // eslint-disable-line
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// load middleware module
|
// load middleware module
|
||||||
const koaLogger = require('koa-logger');
|
const koaLogger = require('koa-logger')
|
||||||
const koaCors = require('@koa/cors');
|
const koaCors = require('@koa/cors')
|
||||||
const koaBody = require('koa-body');
|
const koaBody = require('koa-body')
|
||||||
const koaMount = require('koa-mount');
|
const koaMount = require('koa-mount')
|
||||||
const koaStatic = require('koa-static');
|
const koaStatic = require('koa-static')
|
||||||
const rootRouter = require('src/routes/index.js');
|
const rootRouter = require('src/routes/index.js')
|
||||||
|
|
||||||
const packageJSON = require('./package.json');
|
const packageJSON = require('./package.json')
|
||||||
|
|
||||||
let swaggerDoc = null;
|
let swaggerDoc = null;
|
||||||
// generate swagger document
|
// generate swagger document
|
||||||
@ -32,7 +32,7 @@ let swaggerDoc = null;
|
|||||||
info: {
|
info: {
|
||||||
description: 'KeyCloak OAuth Demo Server',
|
description: 'KeyCloak OAuth Demo Server',
|
||||||
title: 'KeyCloak',
|
title: 'KeyCloak',
|
||||||
version: packageJSON.version,
|
version: packageJSON.version
|
||||||
},
|
},
|
||||||
host: new url.URL(config.server.url).host,
|
host: new url.URL(config.server.url).host,
|
||||||
basePath: '',
|
basePath: '',
|
||||||
@ -43,57 +43,57 @@ let swaggerDoc = null;
|
|||||||
type: 'apiKey',
|
type: 'apiKey',
|
||||||
in: 'header',
|
in: 'header',
|
||||||
name: 'Authorization',
|
name: 'Authorization',
|
||||||
description: 'Bearer token',
|
description: 'Bearer token'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
basedir: __dirname,
|
basedir: __dirname,
|
||||||
files: ['./routes/**/*.js'],
|
files: ['./routes/**/*.js']
|
||||||
};
|
}
|
||||||
swaggerDoc = await swaggerGenerator.generateSpec(swaggerSpec);
|
swaggerDoc = await swaggerGenerator.generateSpec(swaggerSpec)
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
console.error('[Error] Generate swagger doc failed, ', err);
|
console.error('[Error] Generate swagger doc failed, ', err)
|
||||||
process.exit(1);
|
process.exit(1)
|
||||||
});
|
})
|
||||||
|
|
||||||
// production 不掛上swagger ui
|
// production 不掛上swagger ui
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
app.use(koaMount('/api-docs/', koaStatic(path.resolve(__dirname, 'public', 'swagger-ui'), { index: 'index.html' })));
|
app.use(koaMount('/api-docs/', koaStatic(path.resolve(__dirname, 'public', 'swagger-ui'), { index: 'index.html' })))
|
||||||
rootRouter.get('/api-docs', async c => {
|
rootRouter.get('/api-docs', async c => {
|
||||||
if (!/\/$/.test(c.url)) c.redirect(`${c.url}/`);
|
if (!/\/$/.test(c.url)) c.redirect(`${c.url}/`)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// set swagger doc route
|
// set swagger doc route
|
||||||
rootRouter.get('/api-docs.json', async c => {
|
rootRouter.get('/api-docs.json', async c => {
|
||||||
c.type = 'application/json';
|
c.type = 'application/json'
|
||||||
const spec = copyObject(swaggerDoc || {});
|
const spec = copyObject(swaggerDoc || {})
|
||||||
|
|
||||||
if (c.protocol === 'https') {
|
if (c.protocol === 'https') {
|
||||||
spec.schemes = ['https'];
|
spec.schemes = ['https']
|
||||||
}
|
}
|
||||||
|
|
||||||
c.body = spec;
|
c.body = spec
|
||||||
});
|
})
|
||||||
|
|
||||||
app.use(koaLogger());
|
app.use(koaLogger())
|
||||||
app.use(
|
app.use(
|
||||||
koaCors({
|
koaCors({
|
||||||
credentials: true,
|
credentials: true,
|
||||||
// allow all origin
|
// allow all origin
|
||||||
origin: ctx => ctx.get('origin'),
|
origin: ctx => ctx.get('origin')
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
app.use(
|
app.use(
|
||||||
koaBody({
|
koaBody({
|
||||||
multipart: true,
|
multipart: true,
|
||||||
formidable: {
|
formidable: {
|
||||||
maxFileSize: 100 * 1024 * 1024, // 100 mb
|
maxFileSize: 100 * 1024 * 1024 // 100 mb
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
app.use(koaMount('/', koaStatic(path.resolve(__dirname, 'public', 'html'))));
|
app.use(koaMount('/', koaStatic(path.resolve(__dirname, 'public', 'html'))))
|
||||||
app.use(rootRouter.allowedMethods());
|
app.use(rootRouter.allowedMethods())
|
||||||
app.use(rootRouter.routes());
|
app.use(rootRouter.routes())
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
class Cache {
|
class Cache {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.kv = {};
|
this.kv = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -10,10 +10,10 @@ class Cache {
|
|||||||
*/
|
*/
|
||||||
set (key, value, noOverride) {
|
set (key, value, noOverride) {
|
||||||
if (noOverride && key in this.kv) {
|
if (noOverride && key in this.kv) {
|
||||||
throw new Error("key exists");
|
throw new Error('key exists')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.kv[key] = value;
|
this.kv[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,7 +21,7 @@ class Cache {
|
|||||||
* @return {string?}
|
* @return {string?}
|
||||||
*/
|
*/
|
||||||
get (key) {
|
get (key) {
|
||||||
return this.kv[key] || null;
|
return this.kv[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,20 +29,23 @@ class Cache {
|
|||||||
*/
|
*/
|
||||||
del (...keys) {
|
del (...keys) {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
delete this.kv[key];
|
delete this.kv[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache = null;
|
let cache = null
|
||||||
|
|
||||||
exports.new = function () {
|
exports.new = function () {
|
||||||
if (cache) throw new Error("cache already initiate");
|
if (cache) throw new Error('cache already initiate')
|
||||||
cache = new Cache();
|
cache = new Cache()
|
||||||
return cache;
|
return cache
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Cache}
|
||||||
|
*/
|
||||||
exports.get = function () {
|
exports.get = function () {
|
||||||
if (!cache) throw new Error("cache not initiate");
|
if (!cache) throw new Error('cache not initiate')
|
||||||
return cache;
|
return cache
|
||||||
};
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
/* eslint-disable no-restricted-globals,no-param-reassign,guard-for-in,no-restricted-syntax,no-continue */
|
/* eslint-disable no-restricted-globals,no-param-reassign,guard-for-in,no-restricted-syntax,no-continue */
|
||||||
const mod = {};
|
const mod = {}
|
||||||
module.exports = mod;
|
module.exports = mod
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check value is number
|
* check value is number
|
||||||
* @param {any} v input value
|
* @param {any} v input value
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
mod.isNumber = v => !(!isFinite(v) || v === true || v === false || v === null || v === '');
|
mod.isNumber = v => !(!isFinite(v) || v === true || v === false || v === null || v === '')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* value to number
|
* value to number
|
||||||
@ -18,20 +18,20 @@ mod.isNumber = v => !(!isFinite(v) || v === true || v === false || v === null ||
|
|||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
mod.toNumber = (v, defVal, min, max) => {
|
mod.toNumber = (v, defVal, min, max) => {
|
||||||
let defaultVal = defVal;
|
let defaultVal = defVal
|
||||||
let inVal = v;
|
let inVal = v
|
||||||
if (!mod.isNumber(defVal)) defVal = 0;
|
if (!mod.isNumber(defVal)) defVal = 0
|
||||||
if (typeof defVal === 'string') defaultVal = parseFloat(defVal);
|
if (typeof defVal === 'string') defaultVal = parseFloat(defVal)
|
||||||
const minVal = !mod.isNumber(min) ? null : typeof min === 'string' ? parseFloat(min) : min; // eslint-disable-line
|
const minVal = !mod.isNumber(min) ? null : typeof min === 'string' ? parseFloat(min) : min; // eslint-disable-line
|
||||||
const maxVal = !mod.isNumber(max) ? null : typeof max === 'string' ? parseFloat(max) : max; // eslint-disable-line
|
const maxVal = !mod.isNumber(max) ? null : typeof max === 'string' ? parseFloat(max) : max; // eslint-disable-line
|
||||||
|
|
||||||
if (!mod.isNumber(v)) return defaultVal;
|
if (!mod.isNumber(v)) return defaultVal
|
||||||
if (typeof v === 'string') inVal = parseFloat(v);
|
if (typeof v === 'string') inVal = parseFloat(v)
|
||||||
if (minVal !== null && inVal < minVal) inVal = min;
|
if (minVal !== null && inVal < minVal) inVal = min
|
||||||
if (maxVal !== null && inVal > maxVal) inVal = max;
|
if (maxVal !== null && inVal > maxVal) inVal = max
|
||||||
|
|
||||||
return inVal;
|
return inVal
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports
|
* @exports
|
||||||
@ -49,24 +49,24 @@ mod.toNumber = (v, defVal, min, max) => {
|
|||||||
* @return {pageObject}
|
* @return {pageObject}
|
||||||
*/
|
*/
|
||||||
mod.calcPage = (argCount, argPage, argMaxItem = 10) => {
|
mod.calcPage = (argCount, argPage, argMaxItem = 10) => {
|
||||||
const count = mod.toNumber(argCount, 0, 0);
|
const count = mod.toNumber(argCount, 0, 0)
|
||||||
let page = mod.toNumber(argPage, 1, 1);
|
let page = mod.toNumber(argPage, 1, 1)
|
||||||
const maxItem = mod.toNumber(argMaxItem, 10, 1);
|
const maxItem = mod.toNumber(argMaxItem, 10, 1)
|
||||||
|
|
||||||
let total = Math.ceil(count / maxItem);
|
let total = Math.ceil(count / maxItem)
|
||||||
if (total < 1) total = 1;
|
if (total < 1) total = 1
|
||||||
if (page > total) page = total;
|
if (page > total) page = total
|
||||||
let offset = (page - 1) * maxItem;
|
let offset = (page - 1) * maxItem
|
||||||
if (offset > count) offset = count;
|
if (offset > count) offset = count
|
||||||
const limit = maxItem;
|
const limit = maxItem
|
||||||
|
|
||||||
const pageObject = { total, page, count, offset, limit };
|
const pageObject = { total, page, count, offset, limit }
|
||||||
for (const key in pageObject) {
|
for (const key in pageObject) {
|
||||||
pageObject[key] = mod.toNumber(pageObject[key]);
|
pageObject[key] = mod.toNumber(pageObject[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
return pageObject;
|
return pageObject
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* deep copy object
|
* deep copy object
|
||||||
@ -74,18 +74,18 @@ mod.calcPage = (argCount, argPage, argMaxItem = 10) => {
|
|||||||
* @return {any}
|
* @return {any}
|
||||||
*/
|
*/
|
||||||
mod.copyObject = src => {
|
mod.copyObject = src => {
|
||||||
if (typeof src !== 'object') return src;
|
if (typeof src !== 'object') return src
|
||||||
|
|
||||||
const isArray = Array.isArray(src);
|
const isArray = Array.isArray(src)
|
||||||
const copy = isArray ? [] : {};
|
const copy = isArray ? [] : {}
|
||||||
for (let it in src) {
|
for (let it in src) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (isArray) it = parseInt(it, 10);
|
if (isArray) it = parseInt(it, 10)
|
||||||
if (typeof src[it] !== 'object') copy[it] = src[it];
|
if (typeof src[it] !== 'object') copy[it] = src[it]
|
||||||
else copy[it] = mod.copyObject(src[it]);
|
else copy[it] = mod.copyObject(src[it])
|
||||||
|
}
|
||||||
|
return copy
|
||||||
}
|
}
|
||||||
return copy;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {(string|number)} SelectObjectAlias
|
* @typedef {(string|number)} SelectObjectAlias
|
||||||
@ -102,23 +102,23 @@ mod.copyObject = src => {
|
|||||||
* @param {SelectObjectParam} param
|
* @param {SelectObjectParam} param
|
||||||
*/
|
*/
|
||||||
mod.selectObject = (obj, param) => {
|
mod.selectObject = (obj, param) => {
|
||||||
if (typeof obj !== 'object' || typeof param !== 'object') throw new Error('input arg wrong');
|
if (typeof obj !== 'object' || typeof param !== 'object') throw new Error('input arg wrong')
|
||||||
|
|
||||||
let newObj = {};
|
let newObj = {}
|
||||||
|
|
||||||
for (const key in param) {
|
for (const key in param) {
|
||||||
const strs = key.split('.');
|
const strs = key.split('.')
|
||||||
const alias = param[key];
|
const alias = param[key]
|
||||||
if (strs.length > 1) {
|
if (strs.length > 1) {
|
||||||
if (!(strs[0] in obj)) continue;
|
if (!(strs[0] in obj)) continue
|
||||||
|
|
||||||
newObj = { ...newObj, ...mod.selectObject(obj[strs[0]], { [strs.slice(1).join('.')]: alias }) };
|
newObj = { ...newObj, ...mod.selectObject(obj[strs[0]], { [strs.slice(1).join('.')]: alias }) }
|
||||||
}
|
}
|
||||||
const toAlias = param[key] && typeof param[key] === 'string';
|
const toAlias = param[key] && typeof param[key] === 'string'
|
||||||
if (!(key in obj)) continue;
|
if (!(key in obj)) continue
|
||||||
|
|
||||||
newObj[toAlias ? alias : key] = obj[key];
|
newObj[toAlias ? alias : key] = obj[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
return newObj;
|
return newObj
|
||||||
};
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const path = require('path');
|
const path = require('path')
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken')
|
||||||
|
|
||||||
const pkg = {};
|
const pkg = {}
|
||||||
|
|
||||||
pkg.path = path;
|
pkg.path = path
|
||||||
pkg.jwt = jwt;
|
pkg.jwt = jwt
|
||||||
|
|
||||||
module.exports = pkg;
|
module.exports = pkg
|
||||||
|
@ -18,10 +18,10 @@
|
|||||||
* @param {codeMessage} codeMsg
|
* @param {codeMessage} codeMsg
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const mod = {};
|
const mod = {}
|
||||||
module.exports = mod;
|
module.exports = mod
|
||||||
|
|
||||||
mod.respDefault = (status = 200, codeMsg) => ({ status, object: codeMsg });
|
mod.respDefault = (status = 200, codeMsg) => ({ status, object: codeMsg })
|
||||||
|
|
||||||
mod.APIError = class extends Error {
|
mod.APIError = class extends Error {
|
||||||
/**
|
/**
|
||||||
@ -29,25 +29,25 @@ mod.APIError = class extends Error {
|
|||||||
* @param {respObject} resp
|
* @param {respObject} resp
|
||||||
*/
|
*/
|
||||||
constructor (message = '', resp) {
|
constructor (message = '', resp) {
|
||||||
super(message);
|
super(message)
|
||||||
this._object = resp || {};
|
this._object = resp || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
get object () {
|
get object () {
|
||||||
return this._object;
|
return this._object
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check response object struct
|
* check response object struct
|
||||||
* @param {respObject} v
|
* @param {respObject} v
|
||||||
*/
|
*/
|
||||||
mod.checkStruct = v => {
|
mod.checkStruct = v => {
|
||||||
if (typeof v !== 'object' || v === null || v === undefined) return false;
|
if (typeof v !== 'object' || v === null || v === undefined) return false
|
||||||
if (!('status' in v) || !('object' in v)) return false;
|
if (!('status' in v) || !('object' in v)) return false
|
||||||
if (typeof v.object !== 'object' || !('code' in v.object) || !('message' in v.object)) return false;
|
if (typeof v.object !== 'object' || !('code' in v.object) || !('message' in v.object)) return false
|
||||||
return true;
|
return true
|
||||||
};
|
}
|
||||||
|
|
||||||
mod.codeMessage = {
|
mod.codeMessage = {
|
||||||
CodeSuccess: { code: 1000, message: 'success' },
|
CodeSuccess: { code: 1000, message: 'success' },
|
||||||
@ -57,8 +57,8 @@ mod.codeMessage = {
|
|||||||
CodeUnauthorized: { code: 1004, message: 'unauthorized' },
|
CodeUnauthorized: { code: 1004, message: 'unauthorized' },
|
||||||
CodeForbidden: { code: 1005, message: 'forbidden' },
|
CodeForbidden: { code: 1005, message: 'forbidden' },
|
||||||
CodeNotFound: { code: 1006, message: 'not found' },
|
CodeNotFound: { code: 1006, message: 'not found' },
|
||||||
CodeInternalError: { code: 1007, message: 'internal error' },
|
CodeInternalError: { code: 1007, message: 'internal error' }
|
||||||
};
|
}
|
||||||
|
|
||||||
mod.resp = {
|
mod.resp = {
|
||||||
Success: mod.respDefault(200, mod.codeMessage.CodeSuccess),
|
Success: mod.respDefault(200, mod.codeMessage.CodeSuccess),
|
||||||
@ -68,5 +68,5 @@ mod.resp = {
|
|||||||
Unauthorized: mod.respDefault(401, mod.codeMessage.CodeUnauthorized),
|
Unauthorized: mod.respDefault(401, mod.codeMessage.CodeUnauthorized),
|
||||||
Forbidden: mod.respDefault(403, mod.codeMessage.CodeForbidden),
|
Forbidden: mod.respDefault(403, mod.codeMessage.CodeForbidden),
|
||||||
NotFound: mod.respDefault(404, mod.codeMessage.CodeNotFound),
|
NotFound: mod.respDefault(404, mod.codeMessage.CodeNotFound),
|
||||||
InternalError: mod.respDefault(500, mod.codeMessage.CodeInternalError),
|
InternalError: mod.respDefault(500, mod.codeMessage.CodeInternalError)
|
||||||
};
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
const joi = require("joi");
|
const joi = require('joi')
|
||||||
const url = require("url");
|
const url = require('url')
|
||||||
const querystring = require("querystring");
|
const querystring = require('querystring')
|
||||||
const got = require("got");
|
const got = require('got')
|
||||||
const config = require("src/config/index.js");
|
const config = require('src/config/index.js')
|
||||||
const { jwt } = require("src/utils/pkgs.js");
|
|
||||||
|
|
||||||
const mod = {};
|
const mod = {}
|
||||||
module.exports = mod;
|
module.exports = mod
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* @return {string}
|
||||||
@ -18,29 +17,29 @@ mod.getAuthURL = (state) => {
|
|||||||
token_endpoint: joi.string().required(),
|
token_endpoint: joi.string().required(),
|
||||||
client_id: joi.string().required(),
|
client_id: joi.string().required(),
|
||||||
client_secret: joi.string().required(),
|
client_secret: joi.string().required(),
|
||||||
state: joi.string().allow("", null).default(""),
|
state: joi.string().allow('', null).default('')
|
||||||
})
|
})
|
||||||
.unknown()
|
.unknown()
|
||||||
.validate({ ...config.sso, state });
|
.validate({ ...config.sso, state })
|
||||||
if (input.error) throw new Error(input.error.message);
|
if (input.error) throw new Error(input.error.message)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string }}}
|
* @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string }}}
|
||||||
*/
|
*/
|
||||||
const { value } = input;
|
const { value } = input
|
||||||
|
|
||||||
const redirectUri = new url.URL("/oauth/redirect", config.server.url);
|
const redirectUri = new url.URL('/oauth/redirect', config.server.url)
|
||||||
|
|
||||||
const qs = {
|
const qs = {
|
||||||
client_id: value.client_id,
|
client_id: value.client_id,
|
||||||
scope: "offline_access",
|
scope: 'offline_access',
|
||||||
response_type: "code",
|
response_type: 'code',
|
||||||
redirect_uri: redirectUri.toString(),
|
redirect_uri: redirectUri.toString()
|
||||||
};
|
}
|
||||||
if (value.state) qs.state = state;
|
if (value.state) qs.state = state
|
||||||
|
|
||||||
return `${value.authorized_endpoint}?${querystring.stringify(qs)}`;
|
return `${value.authorized_endpoint}?${querystring.stringify(qs)}`
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* @return {string}
|
||||||
@ -48,33 +47,116 @@ mod.getAuthURL = (state) => {
|
|||||||
mod.getLogoutURL = () => {
|
mod.getLogoutURL = () => {
|
||||||
const input = joi
|
const input = joi
|
||||||
.object({
|
.object({
|
||||||
logout_endpoint: joi.string().required(),
|
logout_endpoint: joi.string().required()
|
||||||
})
|
})
|
||||||
.unknown()
|
.unknown()
|
||||||
.validate({ ...config.sso });
|
.validate({ ...config.sso })
|
||||||
if (input.error) throw new Error(input.error.message);
|
if (input.error) throw new Error(input.error.message)
|
||||||
const redirectUri = new url.URL("/oauth/redirect", config.server.url);
|
const redirectUri = new url.URL('/oauth/redirect', config.server.url)
|
||||||
|
|
||||||
const qs = { state: "logout", redirect_uri: redirectUri.toString() };
|
const qs = { state: 'logout', redirect_uri: redirectUri.toString() }
|
||||||
|
|
||||||
return `${input.value.logout_endpoint}?${querystring.stringify(qs)}`;
|
return `${input.value.logout_endpoint}?${querystring.stringify(qs)}`
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @return {Promise<{username: string, display_name: string, email: string, groups: string[]}>}
|
||||||
|
*/
|
||||||
mod.getUserInfo = async (token) => {
|
mod.getUserInfo = async (token) => {
|
||||||
const input = joi
|
/**
|
||||||
.object()
|
* @type {{
|
||||||
|
* client_id: string,
|
||||||
|
* userinfo_endpoint: string,
|
||||||
|
* token: string,
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
const input = await joi
|
||||||
|
.object({
|
||||||
|
client_id: joi.string().required(),
|
||||||
|
client_secret: joi.string().required(),
|
||||||
|
userinfo_endpoint: joi.string().required(),
|
||||||
|
token: joi.string().required()
|
||||||
|
})
|
||||||
.unknown()
|
.unknown()
|
||||||
.validateAsync({ ...config.sso, token });
|
.validateAsync({ ...config.sso, token })
|
||||||
};
|
|
||||||
|
try {
|
||||||
|
const resp = await got.default.get(input.userinfo_endpoint, {
|
||||||
|
responseType: 'json',
|
||||||
|
headers: { Authorization: `Bearer ${input.token}` }
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = await joi.object({
|
||||||
|
name: joi.string(),
|
||||||
|
email: joi.string().email().required(),
|
||||||
|
groups: joi.array().items(joi.string()).default([]),
|
||||||
|
family_name: joi.string(),
|
||||||
|
given_name: joi.string(),
|
||||||
|
preferred_username: joi.string()
|
||||||
|
}).unknown().validateAsync(resp.body)
|
||||||
|
|
||||||
|
const displayName = body.name || body.preferred_username || body.given_name || ''
|
||||||
|
return { display_name: displayName, email: body.email, groups: body.groups, username: body.preferred_username || '' }
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
if (err instanceof got.HTTPError) {
|
||||||
|
if (err.code === 401) {
|
||||||
|
// try refresh token
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.refreshToken = async (token) => {
|
||||||
|
/**
|
||||||
|
* @type {{
|
||||||
|
* token_endpoint: string,
|
||||||
|
* client_id: string,
|
||||||
|
* client_secret: string,
|
||||||
|
* token: string,
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
const input = await joi.object({
|
||||||
|
token_endpoint: joi.string().required(),
|
||||||
|
client_id: joi.string().required(),
|
||||||
|
client_secret: joi.string().required(),
|
||||||
|
token: joi.string().required()
|
||||||
|
}).unknown().validateAsync({ ...config.sso, token })
|
||||||
|
|
||||||
|
const qs = {
|
||||||
|
client_id: input.client_id,
|
||||||
|
client_secret: input.client_secret,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: input.token
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await got.default.post(input.token_endpoint, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: querystring.stringify(qs),
|
||||||
|
responseType: 'json'
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = await joi.object({
|
||||||
|
access_token: joi.string().required(),
|
||||||
|
refresh_token: joi.string().required()
|
||||||
|
}).unknown().validateAsync(resp.body)
|
||||||
|
|
||||||
|
return { access_token: body.access_token, refresh_token: body.refresh_token }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef SSOAccount
|
* @typedef SSOAccount
|
||||||
* @property {string} access_token
|
* @property {string} access_token
|
||||||
* @property {string} refresh_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
|
||||||
|
* @property {string[]} groups
|
||||||
*/
|
*/
|
||||||
|
|
||||||
mod.getToken = async (code, state) => {
|
mod.getToken = async (code, state) => {
|
||||||
@ -85,19 +167,19 @@ mod.getToken = async (code, state) => {
|
|||||||
client_id: joi.string().required(),
|
client_id: joi.string().required(),
|
||||||
client_secret: joi.string().required(),
|
client_secret: joi.string().required(),
|
||||||
state: joi.string().required(),
|
state: joi.string().required(),
|
||||||
code: joi.string().required(),
|
code: joi.string().required()
|
||||||
})
|
})
|
||||||
.unknown()
|
.unknown()
|
||||||
.validate({ ...config.sso, state, code });
|
.validate({ ...config.sso, state, code })
|
||||||
|
|
||||||
if (input.error) throw new Error(input.error.message);
|
if (input.error) throw new Error(input.error.message)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string, code: string }}}
|
* @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string, code: string }}}
|
||||||
*/
|
*/
|
||||||
const { value } = input;
|
const { value } = input
|
||||||
|
|
||||||
const redirectUri = new url.URL("/oauth/redirect", config.server.url);
|
const redirectUri = new url.URL('/oauth/redirect', config.server.url)
|
||||||
|
|
||||||
const qs = {
|
const qs = {
|
||||||
client_id: value.client_id,
|
client_id: value.client_id,
|
||||||
@ -105,57 +187,39 @@ mod.getToken = async (code, state) => {
|
|||||||
redirect_uri: redirectUri.toString(),
|
redirect_uri: redirectUri.toString(),
|
||||||
code: value.code,
|
code: value.code,
|
||||||
client_session_state: value.state,
|
client_session_state: value.state,
|
||||||
grant_type: "authorization_code",
|
grant_type: 'authorization_code'
|
||||||
};
|
}
|
||||||
|
|
||||||
const resp = await got.default.post(value.token_endpoint, {
|
const resp = await got.default.post(value.token_endpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
},
|
},
|
||||||
body: querystring.stringify(qs),
|
body: querystring.stringify(qs),
|
||||||
responseType: "json",
|
responseType: 'json'
|
||||||
});
|
})
|
||||||
|
|
||||||
const { body } = resp;
|
const { body } = resp
|
||||||
if (!body) throw new Error("resopnse body empty");
|
if (!body) throw new Error('resopnse body empty')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id_token: idToken,
|
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken
|
||||||
} = body;
|
} = body
|
||||||
// if (!idToken) throw new Error("get id token fail");
|
|
||||||
|
|
||||||
// const decoded = jwt.decode(idToken);
|
const userInfo = await mod.getUserInfo(accessToken)
|
||||||
// if (!decoded || typeof decoded !== "object")
|
if (!userInfo) throw new Error('user info get fail')
|
||||||
// throw new Error("jwt decode fail");
|
|
||||||
// console.log("decoded ::: ", decoded);
|
|
||||||
|
|
||||||
const decoded = jwt.decode(accessToken);
|
|
||||||
// decode access token
|
// decode access token
|
||||||
console.log("token ::: ", jwt.decode(accessToken));
|
console.log('user info ::: ', userInfo)
|
||||||
|
|
||||||
<<<<<<< HEAD
|
console.log('body ::: ', body)
|
||||||
console.log("body ::: ", body);
|
|
||||||
=======
|
|
||||||
const decoded = jwt.decode(idToken);
|
|
||||||
if (!decoded || typeof decoded !== 'object') throw new Error('jwt decode fail');
|
|
||||||
>>>>>>> c96cdf0ebd17f805235c6fa9eecf2ea79ecca19b
|
|
||||||
// @ts-ignore
|
|
||||||
const { preferred_username: preferredUsername } = decoded;
|
|
||||||
if (!preferredUsername) throw new Error("id token field missing");
|
|
||||||
|
|
||||||
const displayName = `${decoded.family_name ?? ""}${decoded.given_name ?? ""}`;
|
|
||||||
|
|
||||||
/** @type {SSOAccount} */
|
/** @type {SSOAccount} */
|
||||||
const ssoAccount = {
|
const ssoAccount = {
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
user_id: decoded.sub,
|
...userInfo
|
||||||
username: preferredUsername.toLowerCase(),
|
}
|
||||||
display_name: displayName ?? preferredUsername,
|
|
||||||
email: decoded.email ?? "",
|
|
||||||
};
|
|
||||||
|
|
||||||
return ssoAccount;
|
return ssoAccount
|
||||||
};
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user