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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user