This commit is contained in:
Jay 2021-09-01 20:46:41 +08:00
parent b91ce62aa4
commit 2e05f90851
19 changed files with 3061 additions and 11174 deletions

View File

@ -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 || ''
}
}

View File

@ -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;

View File

@ -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, {})
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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": {

View File

@ -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())

View File

@ -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())

View File

@ -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

View File

@ -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以外的環境會有)
*/

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}