update
This commit is contained in:
+20
-17
@@ -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
|
||||
}
|
||||
|
||||
+45
-45
@@ -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
|
||||
}
|
||||
|
||||
+6
-6
@@ -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
-18
@@ -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)
|
||||
}
|
||||
|
||||
+136
-72
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user