[feat] Init code
This commit is contained in:
@@ -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
@@ -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);
|
||||
@@ -0,0 +1,9 @@
|
||||
const path = require('path');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const pkg = {};
|
||||
|
||||
pkg.path = path;
|
||||
pkg.jwt = jwt;
|
||||
|
||||
module.exports = pkg;
|
||||
@@ -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();
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user