[feat] Init code

This commit is contained in:
JasonWu
2021-08-31 18:24:42 +08:00
commit b1e9c5e62a
57 changed files with 17957 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
const knex = require('knex');
const config = require('src/config/index.js');
const pool = knex({
client: 'pg',
connection: {
user: config.database.user,
password: config.database.password,
host: config.database.host,
port: config.database.port,
database: config.database.dbname,
},
pool: {
max: config.database.pool_max,
min: config.database.pool_min,
},
});
module.exports = pool;
+166
View File
@@ -0,0 +1,166 @@
/* eslint-disable no-restricted-globals,no-param-reassign,guard-for-in,no-restricted-syntax,no-continue */
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 === '');
/**
* value to number
* @param {object} v source value
* @param {number=} defVal default value
* @param {number=} min range min
* @param {number=} max range max
* @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);
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;
return inVal;
};
/**
* @exports
* @typedef pageObject
* @prop {number} total page number
* @prop {number} page number
* @prop {number} count all count number
* @prop {number} limit query limit item
* @prop {number} offset query offset number
*
* calc page object
* @param {number} argCount all count
* @param {number} argPage page number
* @param {number} argMaxItem per page item show
* @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);
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 };
for (const key in pageObject) {
pageObject[key] = mod.toNumber(pageObject[key]);
}
return pageObject;
};
/**
* deep copy object
* @param {any} src
* @return {any}
*/
mod.copyObject = src => {
if (typeof src !== 'object') return src;
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]);
}
return copy;
};
/**
* @typedef {(string|number)} SelectObjectAlias
* @description pass string and length > 0 rename key to alias, other keep key name
*/
/**
* @exports
* @typedef {{[key: string]: SelectObjectAlias }} SelectObjectParam
*/
/**
* select object keys
* @param {Object<string, any>} obj
* @param {SelectObjectParam} param
*/
mod.selectObject = (obj, param) => {
if (typeof obj !== 'object' || typeof param !== 'object') throw new Error('input arg wrong');
let newObj = {};
for (const key in param) {
const strs = key.split('.');
const alias = param[key];
if (strs.length > 1) {
if (!(strs[0] in obj)) continue;
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;
newObj[toAlias ? alias : key] = obj[key];
}
return newObj;
};
/**
* pad string to target length
* @param {any} v source input
* @param {number} len target length
* @param {number} direct pad direct (-1 left, 1 right)
* @param {string} padChar default '0'
* @return {string}
*/
mod.pad = (v, len = 0, direct = -1, padChar = '0') => {
if (v === null || v === undefined) return '';
if (typeof v !== 'string' && !v.toString) return '';
if (direct !== 1 && direct !== -1) return '';
if (typeof v !== 'string') v = v.toString();
if (typeof padChar !== 'string') padChar = '0';
len = mod.toNumber(len, 0, 0);
if (v.length < len) {
if (direct < 0) v = `${padChar}${v}`;
else v = `${v}${padChar}`;
return mod.pad(v, len, direct, padChar);
}
return v;
};
/**
* pad left
* @param {any} v
* @param {number} len
* @param {string} padChar
* @return {string}
*/
mod.padLeft = (v, len = 0, padChar = '0') => mod.pad(v, len, -1, padChar);
/**
* pad right
* @param {any} v
* @param {number} len
* @param {string} padChar
* @return {string}
*/
mod.padRight = (v, len = 0, padChar = '0') => mod.pad(v, len, 1, padChar);
+9
View File
@@ -0,0 +1,9 @@
const path = require('path');
const jwt = require('jsonwebtoken');
const pkg = {};
pkg.path = path;
pkg.jwt = jwt;
module.exports = pkg;
+42
View File
@@ -0,0 +1,42 @@
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}`),
};
}
/**
* 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();
+72
View File
@@ -0,0 +1,72 @@
/* eslint-disable no-underscore-dangle */
/**
* @exports
* @typedef {Object} codeMessage
* @prop {number} code
* @prop {string} message
*/
/**
* @exports
* @typedef {Object} respObject
* @prop {number} status
* @prop {codeMessage} object
*/
/**
* create respobject
* @param {number} status
* @param {codeMessage} codeMsg
*/
const mod = {};
module.exports = mod;
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 || {};
}
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;
};
mod.codeMessage = {
CodeSuccess: { code: 1000, message: 'success' },
CodeCreated: { code: 1001, message: 'created' },
CodeAccepted: { code: 1002, message: 'accepted' },
CodeDataFormat: { code: 1003, message: 'data format error' },
CodeUnauthorized: { code: 1004, message: 'unauthorized' },
CodeForbidden: { code: 1005, message: 'forbidden' },
CodeNotFound: { code: 1006, message: 'not found' },
CodeInternalError: { code: 1007, message: 'internal error' },
};
mod.resp = {
Success: mod.respDefault(200, mod.codeMessage.CodeSuccess),
Created: mod.respDefault(201, mod.codeMessage.CodeCreated),
Accepted: mod.respDefault(202, mod.codeMessage.CodeAccepted),
DataFormat: mod.respDefault(400, mod.codeMessage.CodeDataFormat),
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),
};
+130
View File
@@ -0,0 +1,130 @@
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;
/**
* @return {string}
*/
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(''),
})
.unknown()
.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 redirectUri = new url.URL('/oauth/redirect', config.server.url);
const qs = {
client_id: value.client_id,
scope: 'openid',
response_type: 'code',
redirect_uri: redirectUri.toString(),
};
if (value.state) qs.state = state;
return `${value.authorized_endpoint}?${querystring.stringify(qs)}`;
};
mod.getLogoutURL = () => {
const input = joi
.object({
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);
const qs = { state: 'logout', redirect_uri: redirectUri.toString() };
return `${input.value.logout_endpoint}?${querystring.stringify(qs)}`;
};
/**
* @typedef SSOAccount
* @property {string} username
* @property {string} display_name
* @property {string} email
*/
mod.getToken = async (code, 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().required(),
code: joi.string().required(),
})
.unknown()
.validate({ ...config.sso, state, code });
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 redirectUri = new url.URL('/oauth/redirect', config.server.url);
const qs = {
client_id: value.client_id,
client_secret: value.client_secret,
redirect_uri: redirectUri.toString(),
code: value.code,
client_session_state: value.state,
grant_type: 'authorization_code',
};
const resp = await got.default.post(value.token_endpoint, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: querystring.stringify(qs),
responseType: 'json',
});
const { body } = resp;
if (!body) throw new Error('resopnse body empty');
const { id_token: idToken, access_token: accessToken } = 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)
console.log(body)
// @ts-ignore
const { preferred_username: preferredUsername } = decoded;
if (!preferredUsername) throw new Error('id token field missing');
const displayName = `${decoded.family_name ?? ''}${decoded.given_name ?? ''}`;
/** @type {SSOAccount} */
const ssoAccount = {
username: preferredUsername.toLowerCase(),
display_name: displayName ?? preferredUsername,
email: decoded.email ?? '',
};
return ssoAccount;
};