Compare commits
No commits in common. "b91ce62aa4302d3d5de21cb51d64d552ab1fbe05" and "c96cdf0ebd17f805235c6fa9eecf2ea79ecca19b" have entirely different histories.
b91ce62aa4
...
c96cdf0ebd
21
README.md
21
README.md
@ -29,4 +29,25 @@ REDIS_PREFIX
|
||||
# Redis 連線的資料庫號碼
|
||||
REDIS_DB
|
||||
|
||||
# PostgreSQL 資料庫位址
|
||||
DB_HOST
|
||||
|
||||
# PostgreSQL 資料庫連接埠
|
||||
DB_PORT
|
||||
|
||||
# PostgreSQL 資料庫使用者
|
||||
DB_USER
|
||||
|
||||
# PostgreSQL 資料庫密碼
|
||||
DB_PASSWORD
|
||||
|
||||
# PostgreSQL 資料庫名稱
|
||||
DB_NAME
|
||||
|
||||
# PostgreSQL 資料庫連接池最大連線數
|
||||
DB_POOL_MAX
|
||||
|
||||
# PostgreSQL 資料庫連接池閒置連線數
|
||||
DB_POOL_MIN
|
||||
|
||||
```
|
||||
|
@ -2,23 +2,10 @@ 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_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',
|
||||
@ -33,6 +20,5 @@ module.exports = {
|
||||
logout_endpoint: env.SSO_LOGOUT_ENDPOINT || '',
|
||||
client_id: env.SSO_CLIENT_ID || '',
|
||||
client_secret: env.SSO_CLIENT_SECRET || '',
|
||||
>>>>>>> c96cdf0ebd17f805235c6fa9eecf2ea79ecca19b
|
||||
},
|
||||
};
|
||||
|
@ -2,13 +2,9 @@
|
||||
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,14 +1,14 @@
|
||||
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 redis = require('src/utils/redis.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 controller = {};
|
||||
module.exports = controller;
|
||||
|
||||
controller.loginSSO = () => async (ctx) => {
|
||||
controller.loginSSO = () => async ctx => {
|
||||
const { back_url: backURL } = ctx.query;
|
||||
|
||||
const state = uuid.v4();
|
||||
@ -16,18 +16,17 @@ controller.loginSSO = () => async (ctx) => {
|
||||
const authURL = sso.getAuthURL(state);
|
||||
|
||||
// store back url to cache
|
||||
const cacheKey = `login-${state}`;
|
||||
const cache = getCacheInstance();
|
||||
const cacheKey = redis.Key.ssoLoginCache(state);
|
||||
|
||||
cache.set(cacheKey, JSON.stringify({ back_url: backURL }), true);
|
||||
await redis.set(cacheKey, JSON.stringify({ back_url: backURL }), 'EX', OPENID_EXPIRE);
|
||||
|
||||
const u = new url.URL(authURL);
|
||||
|
||||
ctx.resp(resp.Success, { url: u.toString() });
|
||||
};
|
||||
|
||||
controller.logout = () => async (ctx) => {
|
||||
let link = "";
|
||||
controller.logout = () => async ctx => {
|
||||
let link = '';
|
||||
|
||||
if (ctx.token.sso) {
|
||||
link = sso.getLogoutURL();
|
||||
@ -36,6 +35,6 @@ controller.logout = () => async (ctx) => {
|
||||
ctx.resp(resp.Success, { url: link });
|
||||
};
|
||||
|
||||
controller.getInfo = () => async (ctx) => {
|
||||
controller.getInfo = () => async ctx => {
|
||||
ctx.resp(resp.Success, {});
|
||||
};
|
@ -1,33 +1,32 @@
|
||||
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 redis = require('src/utils/redis.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;
|
||||
|
||||
controller.verifyCode = () => async (ctx) => {
|
||||
controller.verifyCode = () => async ctx => {
|
||||
const { code, session_state: sessionState, state } = ctx.query;
|
||||
|
||||
// logout flow redirect tot frontend
|
||||
if (state === "logout") {
|
||||
if (state === 'logout') {
|
||||
ctx.redirect(config.server.frontend_url);
|
||||
return;
|
||||
}
|
||||
|
||||
// get back url from redis
|
||||
const cacheKey = `login-${state}`;
|
||||
const cache = getCacheInstance();
|
||||
const cacheKey = redis.Key.ssoLoginCache(state);
|
||||
|
||||
const data = cache.get(cacheKey);
|
||||
if (!data) ctx.throw("get login cache fail");
|
||||
const data = await redis.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");
|
||||
if (!backURL) ctx.throw('cache data missing');
|
||||
|
||||
const u = new url.URL(backURL);
|
||||
|
||||
@ -43,17 +42,14 @@ controller.verifyCode = () => async (ctx) => {
|
||||
config.server.jwt_secret,
|
||||
{
|
||||
expiresIn: config.server.jwt_expire,
|
||||
issuer: "lawsnote",
|
||||
issuer: 'lawsnote',
|
||||
}
|
||||
);
|
||||
|
||||
u.searchParams.append(
|
||||
"success",
|
||||
Buffer.from(JSON.stringify({ token: jwtToken })).toString("base64")
|
||||
);
|
||||
u.searchParams.append('success', Buffer.from(JSON.stringify({ token: jwtToken })).toString('base64'));
|
||||
|
||||
try {
|
||||
cache.del(cacheKey);
|
||||
await redis.del(cacheKey);
|
||||
} catch (err) {
|
||||
debug(`delete cache fail: ${util.inspect(err, false, null)}`);
|
||||
}
|
||||
@ -70,10 +66,7 @@ controller.verifyCode = () => async (ctx) => {
|
||||
|
||||
errObj.errorStack = err.stack;
|
||||
errObj.errorMessage = err.message;
|
||||
u.searchParams.append(
|
||||
"error",
|
||||
Buffer.from(JSON.stringify(errObj)).toString("base64")
|
||||
);
|
||||
u.searchParams.append('error', Buffer.from(JSON.stringify(errObj)).toString('base64'));
|
||||
}
|
||||
|
||||
ctx.redirect(u.toString());
|
||||
|
8
index.js
8
index.js
@ -1,11 +1,9 @@
|
||||
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 app = require('./server.js');
|
||||
|
||||
async function runServer() {
|
||||
newCacheInstance();
|
||||
const server = app.listen(config.server.port, () => {
|
||||
// @ts-ignore
|
||||
console.info(`server start on port ${server.address().port}`);
|
||||
|
10
package.json
10
package.json
@ -5,30 +5,32 @@
|
||||
"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": [],
|
||||
"author": "Jay <admin@trj.tw>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "5.4.0",
|
||||
"@koa/cors": "^3.0.0",
|
||||
"@koa/router": "^8.0.5",
|
||||
"@mtfos/swagger-generator": "git+https://github.com/otakukaze/swagger-generator.git#1.4.1",
|
||||
"@mtfos/swagger-generator": "git+https://github.com/otakukaze/swagger-generator.git#1.2.2",
|
||||
"axios": "0.21.0",
|
||||
"debug": "4.2.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"got": "^11.8.2",
|
||||
"ioredis": "4.19.0",
|
||||
"joi": "17.3.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"knex": "0.21.15",
|
||||
"koa": "^2.11.0",
|
||||
"koa-body": "^4.1.1",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-mount": "4.0.0",
|
||||
"koa-range": "0.3.0",
|
||||
"koa-static": "5.0.0",
|
||||
"pg": "8.4.1",
|
||||
"uuid": "8.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,48 +0,0 @@
|
||||
class Cache {
|
||||
constructor() {
|
||||
this.kv = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} value
|
||||
* @param {boolean?} noOverride
|
||||
*/
|
||||
set(key, value, noOverride) {
|
||||
if (noOverride && key in this.kv) {
|
||||
throw new Error("key exists");
|
||||
}
|
||||
|
||||
this.kv[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @return {string?}
|
||||
*/
|
||||
get(key) {
|
||||
return this.kv[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} keys
|
||||
*/
|
||||
del(...keys) {
|
||||
for (const key of keys) {
|
||||
delete this.kv[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cache = null;
|
||||
|
||||
exports.new = function () {
|
||||
if (cache) throw new Error("cache already initiate");
|
||||
cache = new Cache();
|
||||
return cache;
|
||||
};
|
||||
|
||||
exports.get = function () {
|
||||
if (!cache) throw new Error("cache not initiate");
|
||||
return cache;
|
||||
};
|
48
utils/redis.js
Normal file
48
utils/redis.js
Normal file
@ -0,0 +1,48 @@
|
||||
const IORedis = require("ioredis");
|
||||
const config = require("src/config/index.js");
|
||||
|
||||
class Redis extends IORedis {
|
||||
constructor() {
|
||||
let { prefix } = config.redis;
|
||||
const { host, port, password, db } = config.redis;
|
||||
if (prefix && !/:$/.test(prefix)) prefix += ":";
|
||||
super({
|
||||
host,
|
||||
port,
|
||||
password,
|
||||
db,
|
||||
});
|
||||
|
||||
this.prefix = prefix;
|
||||
|
||||
const self = this;
|
||||
// key pattern functions
|
||||
this.Key = {
|
||||
/**
|
||||
* SSO 登入暫存
|
||||
* @param {string} s state
|
||||
* @return {string}
|
||||
*/
|
||||
ssoLoginCache: (s) => self.getKeyWithPrefix(`sso-login:${s}`),
|
||||
/**
|
||||
* 儲存 Token
|
||||
* @param {string} s state
|
||||
* @return {string}
|
||||
*/
|
||||
userToken: (s) => self.getKeyWithPrefix(`token:${s}`),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* combine key and prefix
|
||||
* @param {string} s
|
||||
* @return {string}
|
||||
*/
|
||||
getKeyWithPrefix(s) {
|
||||
if (typeof s !== "string") throw new Error("input key not a string");
|
||||
|
||||
return `${this.prefix}${s}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Redis();
|
@ -1,9 +1,9 @@
|
||||
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 { jwt } = require('src/utils/pkgs.js');
|
||||
|
||||
const mod = {};
|
||||
module.exports = mod;
|
||||
@ -11,14 +11,14 @@ module.exports = mod;
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
mod.getAuthURL = (state) => {
|
||||
mod.getAuthURL = state => {
|
||||
const input = joi
|
||||
.object({
|
||||
authorized_endpoint: joi.string().required(),
|
||||
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 });
|
||||
@ -29,12 +29,12 @@ mod.getAuthURL = (state) => {
|
||||
*/
|
||||
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",
|
||||
scope: 'openid',
|
||||
response_type: 'code',
|
||||
redirect_uri: redirectUri.toString(),
|
||||
};
|
||||
if (value.state) qs.state = state;
|
||||
@ -53,20 +53,13 @@ mod.getLogoutURL = () => {
|
||||
.unknown()
|
||||
.validate({ ...config.sso });
|
||||
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)}`;
|
||||
};
|
||||
|
||||
mod.getUserInfo = async (token) => {
|
||||
const input = joi
|
||||
.object()
|
||||
.unknown()
|
||||
.validateAsync({ ...config.sso, token });
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef SSOAccount
|
||||
* @property {string} access_token
|
||||
@ -97,7 +90,7 @@ mod.getToken = async (code, state) => {
|
||||
*/
|
||||
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,47 +98,30 @@ 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");
|
||||
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");
|
||||
const { id_token: idToken, access_token: accessToken, refresh_token: refreshToken } = body;
|
||||
if (!idToken) throw new Error('get id token fail');
|
||||
|
||||
// const decoded = jwt.decode(idToken);
|
||||
// if (!decoded || typeof decoded !== "object")
|
||||
// throw new Error("jwt decode fail");
|
||||
// console.log("decoded ::: ", decoded);
|
||||
|
||||
const decoded = jwt.decode(accessToken);
|
||||
// decode access token
|
||||
console.log("token ::: ", jwt.decode(accessToken));
|
||||
|
||||
<<<<<<< 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");
|
||||
if (!preferredUsername) throw new Error('id token field missing');
|
||||
|
||||
const displayName = `${decoded.family_name ?? ""}${decoded.given_name ?? ""}`;
|
||||
const displayName = `${decoded.family_name ?? ''}${decoded.given_name ?? ''}`;
|
||||
|
||||
/** @type {SSOAccount} */
|
||||
const ssoAccount = {
|
||||
@ -154,7 +130,7 @@ mod.getToken = async (code, state) => {
|
||||
user_id: decoded.sub,
|
||||
username: preferredUsername.toLowerCase(),
|
||||
display_name: displayName ?? preferredUsername,
|
||||
email: decoded.email ?? "",
|
||||
email: decoded.email ?? '',
|
||||
};
|
||||
|
||||
return ssoAccount;
|
||||
|
Loading…
Reference in New Issue
Block a user