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