fork from bc4552c5a8
This commit is contained in:
683
server/utils/commons.js
Normal file
683
server/utils/commons.js
Normal file
@@ -0,0 +1,683 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const yapi = require('../yapi.js');
|
||||
const sha1 = require('sha1');
|
||||
const logModel = require('../models/log.js');
|
||||
const projectModel = require('../models/project.js');
|
||||
const interfaceColModel = require('../models/interfaceCol.js');
|
||||
const interfaceCaseModel = require('../models/interfaceCase.js');
|
||||
const interfaceModel = require('../models/interface.js');
|
||||
const userModel = require('../models/user.js');
|
||||
const json5 = require('json5');
|
||||
const _ = require('underscore');
|
||||
const Ajv = require('ajv');
|
||||
const Mock = require('mockjs');
|
||||
const sandboxFn = require('./sandbox')
|
||||
|
||||
|
||||
|
||||
const ejs = require('easy-json-schema');
|
||||
|
||||
const jsf = require('json-schema-faker');
|
||||
const { schemaValidator } = require('../../common/utils');
|
||||
const http = require('http');
|
||||
|
||||
jsf.extend('mock', function () {
|
||||
return {
|
||||
mock: function (xx) {
|
||||
return Mock.mock(xx);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const defaultOptions = {
|
||||
failOnInvalidTypes: false,
|
||||
failOnInvalidFormat: false
|
||||
};
|
||||
|
||||
// formats.forEach(item => {
|
||||
// item = item.name;
|
||||
// jsf.format(item, () => {
|
||||
// if (item === 'mobile') {
|
||||
// return jsf.random.randexp('^[1][34578][0-9]{9}$');
|
||||
// }
|
||||
// return Mock.mock('@' + item);
|
||||
// });
|
||||
// });
|
||||
|
||||
exports.schemaToJson = function (schema, options = {}) {
|
||||
Object.assign(options, defaultOptions);
|
||||
|
||||
jsf.option(options);
|
||||
let result;
|
||||
try {
|
||||
result = jsf(schema);
|
||||
} catch (err) {
|
||||
result = err.message;
|
||||
}
|
||||
jsf.option(defaultOptions);
|
||||
return result;
|
||||
};
|
||||
|
||||
exports.resReturn = (data, num, errmsg) => {
|
||||
num = num || 0;
|
||||
|
||||
return {
|
||||
errcode: num,
|
||||
errmsg: errmsg || '成功!',
|
||||
data: data
|
||||
};
|
||||
};
|
||||
|
||||
exports.log = (msg, type) => {
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
type = type || 'log';
|
||||
|
||||
let f;
|
||||
|
||||
switch (type) {
|
||||
case 'log':
|
||||
f = console.log; // eslint-disable-line
|
||||
break;
|
||||
case 'warn':
|
||||
f = console.warn; // eslint-disable-line
|
||||
break;
|
||||
case 'error':
|
||||
f = console.error; // eslint-disable-line
|
||||
break;
|
||||
default:
|
||||
f = console.log; // eslint-disable-line
|
||||
break;
|
||||
}
|
||||
|
||||
f(type + ':', msg);
|
||||
|
||||
let date = new Date();
|
||||
let year = date.getFullYear();
|
||||
let month = date.getMonth() + 1;
|
||||
|
||||
let logfile = path.join(yapi.WEBROOT_LOG, year + '-' + month + '.log');
|
||||
|
||||
if (typeof msg === 'object') {
|
||||
if (msg instanceof Error) msg = msg.message;
|
||||
else msg = JSON.stringify(msg);
|
||||
}
|
||||
|
||||
// let data = (new Date).toLocaleString() + '\t|\t' + type + '\t|\t' + msg + '\n';
|
||||
let data = `[ ${new Date().toLocaleString()} ] [ ${type} ] ${msg}\n`;
|
||||
|
||||
fs.writeFileSync(logfile, data, {
|
||||
flag: 'a'
|
||||
});
|
||||
};
|
||||
|
||||
exports.fileExist = filePath => {
|
||||
try {
|
||||
return fs.statSync(filePath).isFile();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
exports.time = () => {
|
||||
return Date.parse(new Date()) / 1000;
|
||||
};
|
||||
|
||||
exports.fieldSelect = (data, field) => {
|
||||
if (!data || !field || !Array.isArray(field)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var arr = {};
|
||||
|
||||
field.forEach(f => {
|
||||
typeof data[f] !== 'undefined' && (arr[f] = data[f]);
|
||||
});
|
||||
|
||||
return arr;
|
||||
};
|
||||
|
||||
exports.rand = (min, max) => {
|
||||
return Math.floor(Math.random() * (max - min) + min);
|
||||
};
|
||||
|
||||
exports.json_parse = json => {
|
||||
try {
|
||||
return json5.parse(json);
|
||||
} catch (e) {
|
||||
return json;
|
||||
}
|
||||
};
|
||||
|
||||
exports.randStr = () => {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.substr(2);
|
||||
};
|
||||
exports.getIp = ctx => {
|
||||
let ip;
|
||||
try {
|
||||
ip = ctx.ip.match(/\d+.\d+.\d+.\d+/) ? ctx.ip.match(/\d+.\d+.\d+.\d+/)[0] : 'localhost';
|
||||
} catch (e) {
|
||||
ip = null;
|
||||
}
|
||||
return ip;
|
||||
};
|
||||
|
||||
exports.generatePassword = (password, passsalt) => {
|
||||
return sha1(password + sha1(passsalt));
|
||||
};
|
||||
|
||||
exports.expireDate = day => {
|
||||
let date = new Date();
|
||||
date.setTime(date.getTime() + day * 86400000);
|
||||
return date;
|
||||
};
|
||||
|
||||
exports.sendMail = (options, cb) => {
|
||||
if (!yapi.mail) return false;
|
||||
options.subject = options.subject ? options.subject + '-YApi 平台' : 'YApi 平台';
|
||||
|
||||
cb =
|
||||
cb ||
|
||||
function (err) {
|
||||
if (err) {
|
||||
yapi.commons.log('send mail ' + options.to + ' error,' + err.message, 'error');
|
||||
} else {
|
||||
yapi.commons.log('send mail ' + options.to + ' success');
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
yapi.mail.sendMail(
|
||||
{
|
||||
from: yapi.WEBCONFIG.mail.from,
|
||||
to: options.to,
|
||||
subject: options.subject,
|
||||
html: options.contents
|
||||
},
|
||||
cb
|
||||
);
|
||||
} catch (e) {
|
||||
yapi.commons.log(e.message, 'error');
|
||||
console.error(e.message); // eslint-disable-line
|
||||
}
|
||||
};
|
||||
|
||||
exports.validateSearchKeyword = keyword => {
|
||||
if (/^\*|\?|\+|\$|\^|\\|\.$/.test(keyword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
exports.filterRes = (list, rules) => {
|
||||
return list.map(item => {
|
||||
let filteredRes = {};
|
||||
|
||||
rules.forEach(rule => {
|
||||
if (typeof rule == 'string') {
|
||||
filteredRes[rule] = item[rule];
|
||||
} else if (typeof rule == 'object') {
|
||||
filteredRes[rule.alias] = item[rule.key];
|
||||
}
|
||||
});
|
||||
|
||||
return filteredRes;
|
||||
});
|
||||
};
|
||||
|
||||
exports.handleVarPath = (pathname, params) => {
|
||||
function insertParams(name) {
|
||||
if (!_.find(params, { name: name })) {
|
||||
params.push({
|
||||
name: name,
|
||||
desc: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathname) return;
|
||||
if (pathname.indexOf(':') !== -1) {
|
||||
let paths = pathname.split('/'),
|
||||
name,
|
||||
i;
|
||||
for (i = 1; i < paths.length; i++) {
|
||||
if (paths[i] && paths[i][0] === ':') {
|
||||
name = paths[i].substr(1);
|
||||
insertParams(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
pathname.replace(/\{(.+?)\}/g, function (str, match) {
|
||||
insertParams(match);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证一个 path 是否合法
|
||||
* path第一位必需为 /, path 只允许由 字母数字-/_:.{}= 组成
|
||||
*/
|
||||
exports.verifyPath = path => {
|
||||
// if (/^\/[a-zA-Z0-9\-\/_:!\.\{\}\=]*$/.test(path)) {
|
||||
// return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
return /^\/[a-zA-Z0-9\-\/_:!\.\{\}\=]*$/.test(path);
|
||||
};
|
||||
|
||||
/**
|
||||
* 沙盒执行 js 代码
|
||||
* @sandbox Object context
|
||||
* @script String script
|
||||
* @return sandbox
|
||||
*
|
||||
* @example let a = sandbox({a: 1}, 'a=2')
|
||||
* a = {a: 2}
|
||||
*/
|
||||
exports.sandbox = (sandbox, script) => {
|
||||
let filter = '^(process|exec|require)$';
|
||||
let reg = new RegExp(filter, "g");
|
||||
if (reg.test(script)) {
|
||||
throw new Error("执行失败,脚本中含敏感操作....");
|
||||
}
|
||||
try {
|
||||
const vm = require('vm');
|
||||
sandbox = sandbox || {};
|
||||
script = new vm.Script(script);
|
||||
const context = new vm.createContext(sandbox);
|
||||
script.runInContext(context, {
|
||||
timeout: 3000
|
||||
});
|
||||
return sandbox
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
};
|
||||
|
||||
function trim(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
str = str + '';
|
||||
|
||||
return str.replace(/(^\s*)|(\s*$)/g, '');
|
||||
}
|
||||
|
||||
function ltrim(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
str = str + '';
|
||||
|
||||
return str.replace(/(^\s*)/g, '');
|
||||
}
|
||||
|
||||
function rtrim(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
str = str + '';
|
||||
|
||||
return str.replace(/(\s*$)/g, '');
|
||||
}
|
||||
|
||||
exports.trim = trim;
|
||||
exports.ltrim = ltrim;
|
||||
exports.rtrim = rtrim;
|
||||
|
||||
/**
|
||||
* 处理请求参数类型,String 字符串去除两边空格,Number 使用parseInt 转换为数字
|
||||
* @params Object {a: ' ab ', b: ' 123 '}
|
||||
* @keys Object {a: 'string', b: 'number'}
|
||||
* @return Object {a: 'ab', b: 123}
|
||||
*/
|
||||
exports.handleParams = (params, keys) => {
|
||||
if (!params || typeof params !== 'object' || !keys || typeof keys !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var key in keys) {
|
||||
var filter = keys[key];
|
||||
if (params[key]) {
|
||||
switch (filter) {
|
||||
case 'string':
|
||||
params[key] = trim(params[key] + '');
|
||||
break;
|
||||
case 'number':
|
||||
params[key] = !isNaN(params[key]) ? parseInt(params[key], 10) : 0;
|
||||
break;
|
||||
default:
|
||||
params[key] = trim(params + '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
exports.validateParams = (schema2, params) => {
|
||||
const flag = schema2.closeRemoveAdditional;
|
||||
const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
coerceTypes: true,
|
||||
useDefaults: true,
|
||||
removeAdditional: flag ? false : true
|
||||
});
|
||||
|
||||
var localize = require('ajv-i18n');
|
||||
delete schema2.closeRemoveAdditional;
|
||||
|
||||
const schema = ejs(schema2);
|
||||
|
||||
schema.additionalProperties = flag ? true : false;
|
||||
const validate = ajv.compile(schema);
|
||||
let valid = validate(params);
|
||||
|
||||
let message = '请求参数 ';
|
||||
if (!valid) {
|
||||
localize.zh(validate.errors);
|
||||
message += ajv.errorsText(validate.errors, { separator: '\n' });
|
||||
}
|
||||
|
||||
return {
|
||||
valid: valid,
|
||||
message: message
|
||||
};
|
||||
};
|
||||
|
||||
exports.saveLog = logData => {
|
||||
try {
|
||||
let logInst = yapi.getInst(logModel);
|
||||
let data = {
|
||||
content: logData.content,
|
||||
type: logData.type,
|
||||
uid: logData.uid,
|
||||
username: logData.username,
|
||||
typeid: logData.typeid,
|
||||
data: logData.data
|
||||
};
|
||||
|
||||
logInst.save(data).then();
|
||||
} catch (e) {
|
||||
yapi.commons.log(e, 'error'); // eslint-disable-line
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} router router
|
||||
* @param {*} baseurl base_url_path
|
||||
* @param {*} routerController controller
|
||||
* @param {*} path routerPath
|
||||
* @param {*} method request_method , post get put delete ...
|
||||
* @param {*} action controller action_name
|
||||
* @param {*} ws enable ws
|
||||
*/
|
||||
exports.createAction = (router, baseurl, routerController, action, path, method, ws) => {
|
||||
router[method](baseurl + path, async ctx => {
|
||||
let inst = new routerController(ctx);
|
||||
try {
|
||||
await inst.init(ctx);
|
||||
ctx.params = Object.assign({}, ctx.request.query, ctx.request.body, ctx.params);
|
||||
if (inst.schemaMap && typeof inst.schemaMap === 'object' && inst.schemaMap[action]) {
|
||||
|
||||
let validResult = yapi.commons.validateParams(inst.schemaMap[action], ctx.params);
|
||||
|
||||
if (!validResult.valid) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, validResult.message));
|
||||
}
|
||||
}
|
||||
if (inst.$auth === true) {
|
||||
await inst[action].call(inst, ctx);
|
||||
} else {
|
||||
if (ws === true) {
|
||||
ctx.ws.send('请登录...');
|
||||
} else {
|
||||
ctx.body = yapi.commons.resReturn(null, 40011, '请登录...');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.body = yapi.commons.resReturn(null, 40011, '服务器出错...');
|
||||
yapi.commons.log(err, 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} params 接口定义的参数
|
||||
* @param {*} val 接口case 定义的参数值
|
||||
*/
|
||||
function handleParamsValue(params, val) {
|
||||
let value = {};
|
||||
try {
|
||||
params = params.toObject();
|
||||
} catch (e) { }
|
||||
if (params.length === 0 || val.length === 0) {
|
||||
return params;
|
||||
}
|
||||
val.forEach(item => {
|
||||
value[item.name] = item;
|
||||
});
|
||||
params.forEach((item, index) => {
|
||||
if (!value[item.name] || typeof value[item.name] !== 'object') return null;
|
||||
params[index].value = value[item.name].value;
|
||||
if (!_.isUndefined(value[item.name].enable)) {
|
||||
params[index].enable = value[item.name].enable;
|
||||
}
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
exports.handleParamsValue = handleParamsValue;
|
||||
|
||||
exports.getCaseList = async function getCaseList(id) {
|
||||
const caseInst = yapi.getInst(interfaceCaseModel);
|
||||
const colInst = yapi.getInst(interfaceColModel);
|
||||
const projectInst = yapi.getInst(projectModel);
|
||||
const interfaceInst = yapi.getInst(interfaceModel);
|
||||
|
||||
let resultList = await caseInst.list(id, 'all');
|
||||
let colData = await colInst.get(id);
|
||||
for (let index = 0; index < resultList.length; index++) {
|
||||
let result = resultList[index].toObject();
|
||||
let data = await interfaceInst.get(result.interface_id);
|
||||
if (!data) {
|
||||
await caseInst.del(result._id);
|
||||
continue;
|
||||
}
|
||||
let projectData = await projectInst.getBaseInfo(data.project_id);
|
||||
result.path = projectData.basepath + data.path;
|
||||
result.method = data.method;
|
||||
result.title = data.title;
|
||||
result.req_body_type = data.req_body_type;
|
||||
result.req_headers = handleParamsValue(data.req_headers, result.req_headers);
|
||||
result.res_body_type = data.res_body_type;
|
||||
result.req_body_form = handleParamsValue(data.req_body_form, result.req_body_form);
|
||||
result.req_query = handleParamsValue(data.req_query, result.req_query);
|
||||
result.req_params = handleParamsValue(data.req_params, result.req_params);
|
||||
resultList[index] = result;
|
||||
}
|
||||
resultList = resultList.sort((a, b) => {
|
||||
return a.index - b.index;
|
||||
});
|
||||
let ctxBody = yapi.commons.resReturn(resultList);
|
||||
ctxBody.colData = colData;
|
||||
return ctxBody;
|
||||
};
|
||||
|
||||
function convertString(variable) {
|
||||
if (variable instanceof Error) {
|
||||
return variable.name + ': ' + variable.message;
|
||||
}
|
||||
try {
|
||||
if (variable && typeof variable === 'string') {
|
||||
return variable;
|
||||
}
|
||||
return JSON.stringify(variable, null, ' ');
|
||||
} catch (err) {
|
||||
return variable || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
exports.runCaseScript = async function runCaseScript(params, colId, interfaceId) {
|
||||
const colInst = yapi.getInst(interfaceColModel);
|
||||
let colData = await colInst.get(colId);
|
||||
const logs = [];
|
||||
const context = {
|
||||
assert: require('assert'),
|
||||
status: params.response.status,
|
||||
body: params.response.body,
|
||||
header: params.response.header,
|
||||
records: params.records,
|
||||
params: params.params,
|
||||
log: msg => {
|
||||
logs.push('log: ' + convertString(msg));
|
||||
}
|
||||
};
|
||||
|
||||
let result = {};
|
||||
try {
|
||||
|
||||
if (colData.checkHttpCodeIs200) {
|
||||
let status = +params.response.status;
|
||||
if (status !== 200) {
|
||||
throw ('Http status code 不是 200,请检查(该规则来源于于 [测试集->通用规则配置] )')
|
||||
}
|
||||
}
|
||||
|
||||
if (colData.checkResponseField.enable) {
|
||||
if (params.response.body[colData.checkResponseField.name] != colData.checkResponseField.value) {
|
||||
throw (`返回json ${colData.checkResponseField.name} 值不是${colData.checkResponseField.value},请检查(该规则来源于于 [测试集->通用规则配置] )`)
|
||||
}
|
||||
}
|
||||
|
||||
if (colData.checkResponseSchema) {
|
||||
const interfaceInst = yapi.getInst(interfaceModel);
|
||||
let interfaceData = await interfaceInst.get(interfaceId);
|
||||
if (interfaceData.res_body_is_json_schema && interfaceData.res_body) {
|
||||
let schema = JSON.parse(interfaceData.res_body);
|
||||
let result = schemaValidator(schema, context.body)
|
||||
if (!result.valid) {
|
||||
throw (`返回Json 不符合 response 定义的数据结构,原因: ${result.message}
|
||||
数据结构如下:
|
||||
${JSON.stringify(schema, null, 2)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (colData.checkScript.enable) {
|
||||
let globalScript = colData.checkScript.content;
|
||||
// script 是断言
|
||||
if (globalScript) {
|
||||
logs.push('执行脚本:' + globalScript)
|
||||
result = await sandboxFn(context, globalScript, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let script = params.script;
|
||||
// script 是断言
|
||||
if (script) {
|
||||
logs.push('执行脚本:' + script)
|
||||
result = await sandboxFn(context, script, true);
|
||||
}
|
||||
result.logs = logs;
|
||||
return yapi.commons.resReturn(result);
|
||||
} catch (err) {
|
||||
logs.push(convertString(err));
|
||||
result.logs = logs;
|
||||
return yapi.commons.resReturn(result, 400, err.name + ': ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUserdata = async function getUserdata(uid, role) {
|
||||
role = role || 'dev';
|
||||
let userInst = yapi.getInst(userModel);
|
||||
let userData = await userInst.findById(uid);
|
||||
if (!userData) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
role: role,
|
||||
uid: userData._id,
|
||||
username: userData.username,
|
||||
email: userData.email
|
||||
};
|
||||
};
|
||||
|
||||
// 处理mockJs脚本
|
||||
exports.handleMockScript = async function (script, context) {
|
||||
let sandbox = {
|
||||
header: context.ctx.header,
|
||||
query: context.ctx.query,
|
||||
body: context.ctx.request.body,
|
||||
mockJson: context.mockJson,
|
||||
params: Object.assign({}, context.ctx.query, context.ctx.request.body),
|
||||
resHeader: context.resHeader,
|
||||
httpCode: context.httpCode,
|
||||
delay: context.httpCode,
|
||||
Random: Mock.Random
|
||||
};
|
||||
sandbox.cookie = {};
|
||||
|
||||
context.ctx.header.cookie &&
|
||||
context.ctx.header.cookie.split(';').forEach(function (Cookie) {
|
||||
var parts = Cookie.split('=');
|
||||
sandbox.cookie[parts[0].trim()] = (parts[1] || '').trim();
|
||||
});
|
||||
sandbox = await sandboxFn(sandbox, script, false);
|
||||
sandbox.delay = isNaN(sandbox.delay) ? 0 : +sandbox.delay;
|
||||
|
||||
context.mockJson = sandbox.mockJson;
|
||||
context.resHeader = sandbox.resHeader;
|
||||
context.httpCode = sandbox.httpCode;
|
||||
context.delay = sandbox.delay;
|
||||
};
|
||||
|
||||
|
||||
|
||||
exports.createWebAPIRequest = function (ops) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let req = '';
|
||||
let http_client = http.request(
|
||||
{
|
||||
host: ops.hostname,
|
||||
method: 'GET',
|
||||
port: ops.port,
|
||||
path: ops.path
|
||||
},
|
||||
function (res) {
|
||||
res.on('error', function (err) {
|
||||
reject(err);
|
||||
});
|
||||
res.setEncoding('utf8');
|
||||
if (res.statusCode != 200) {
|
||||
reject({ message: 'statusCode != 200' });
|
||||
} else {
|
||||
res.on('data', function (chunk) {
|
||||
req += chunk;
|
||||
});
|
||||
res.on('end', function () {
|
||||
resolve(req);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
http_client.on('error', (e) => {
|
||||
reject({ message: `request error: ${e.message}` });
|
||||
});
|
||||
http_client.end();
|
||||
});
|
||||
}
|
||||
|
||||
83
server/utils/db.js
Normal file
83
server/utils/db.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const mongoose = require('mongoose');
|
||||
const yapi = require('../yapi.js');
|
||||
const autoIncrement = require('./mongoose-auto-increment');
|
||||
|
||||
function model(model, schema) {
|
||||
if (schema instanceof mongoose.Schema === false) {
|
||||
schema = new mongoose.Schema(schema);
|
||||
}
|
||||
|
||||
schema.set('autoIndex', false);
|
||||
|
||||
return mongoose.model(model, schema, model);
|
||||
}
|
||||
|
||||
function connect(callback) {
|
||||
mongoose.Promise = global.Promise;
|
||||
mongoose.set('useNewUrlParser', true);
|
||||
mongoose.set('useFindAndModify', false);
|
||||
mongoose.set('useCreateIndex', true);
|
||||
|
||||
let config = yapi.WEBCONFIG;
|
||||
let options = {useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true};
|
||||
|
||||
if (config.db.user) {
|
||||
options.user = config.db.user;
|
||||
options.pass = config.db.pass;
|
||||
}
|
||||
|
||||
if (config.db.reconnectTries) {
|
||||
options.reconnectTries = config.db.reconnectTries;
|
||||
}
|
||||
|
||||
if (config.db.reconnectInterval) {
|
||||
options.reconnectInterval = config.db.reconnectInterval;
|
||||
}
|
||||
|
||||
|
||||
options = Object.assign({}, options, config.db.options)
|
||||
|
||||
var connectString = '';
|
||||
|
||||
if(config.db.connectString){
|
||||
connectString = config.db.connectString;
|
||||
}else{
|
||||
connectString = `mongodb://${config.db.servername}:${config.db.port}/${config.db.DATABASE}`;
|
||||
if (config.db.authSource) {
|
||||
connectString = connectString + `?authSource=${config.db.authSource}`;
|
||||
}
|
||||
}
|
||||
|
||||
let db = mongoose.connect(
|
||||
connectString,
|
||||
options,
|
||||
function(err) {
|
||||
if (err) {
|
||||
yapi.commons.log(err + ', mongodb Authentication failed', 'error');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
db.then(
|
||||
function() {
|
||||
yapi.commons.log('mongodb load success...');
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback.call(db);
|
||||
}
|
||||
},
|
||||
function(err) {
|
||||
yapi.commons.log(err + 'mongodb connect error', 'error');
|
||||
}
|
||||
);
|
||||
|
||||
autoIncrement.initialize(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
yapi.db = model;
|
||||
|
||||
module.exports = {
|
||||
model: model,
|
||||
connect: connect
|
||||
};
|
||||
13
server/utils/initConfig.js
Normal file
13
server/utils/initConfig.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const config = require('../config.js');
|
||||
|
||||
let runtimePath = config.runtime_path;
|
||||
fs.ensureDirSync(runtimePath);
|
||||
fs.ensureDirSync(path.join(runtimePath, 'log'));
|
||||
let configPath = path.join(runtimePath, 'config.json');
|
||||
|
||||
fs.writeFileSync(configPath,
|
||||
JSON.stringify(config, null, '\t'),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
134
server/utils/ldap.js
Normal file
134
server/utils/ldap.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const ldap = require('ldapjs');
|
||||
const yapi = require('../yapi.js');
|
||||
const util = require('util');
|
||||
|
||||
exports.ldapQuery = (username, password) => {
|
||||
// const deferred = Q.defer();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ldapLogin } = yapi.WEBCONFIG;
|
||||
|
||||
// 使用ldapjs库创建一个LDAP客户端
|
||||
const client = ldap.createClient({
|
||||
url: ldapLogin.server
|
||||
});
|
||||
|
||||
client.once('error', err => {
|
||||
if (err) {
|
||||
let msg = {
|
||||
type: false,
|
||||
message: `once: ${err}`
|
||||
};
|
||||
reject(msg);
|
||||
}
|
||||
});
|
||||
// 注册事件处理函数
|
||||
const ldapSearch = (err, search) => {
|
||||
const users = [];
|
||||
if (err) {
|
||||
let msg = {
|
||||
type: false,
|
||||
message: `ldapSearch: ${err}`
|
||||
};
|
||||
reject(msg);
|
||||
}
|
||||
// 查询结果事件响应
|
||||
search.on('searchEntry', entry => {
|
||||
if (entry) {
|
||||
// 获取查询对象
|
||||
users.push(entry.object);
|
||||
}
|
||||
});
|
||||
// 查询错误事件
|
||||
search.on('error', e => {
|
||||
if (e) {
|
||||
let msg = {
|
||||
type: false,
|
||||
message: `searchErr: ${e}`
|
||||
};
|
||||
reject(msg);
|
||||
}
|
||||
});
|
||||
|
||||
search.on('searchReference', referral => {
|
||||
// if (referral) {
|
||||
// let msg = {
|
||||
// type: false,
|
||||
// message: `searchReference: ${referral}`
|
||||
// };
|
||||
// reject(msg);
|
||||
// }
|
||||
console.log('referral: ' + referral.uris.join());
|
||||
});
|
||||
// 查询结束
|
||||
search.on('end', () => {
|
||||
if (users.length > 0) {
|
||||
client.bind(users[0].dn, password, e => {
|
||||
if (e) {
|
||||
let msg = {
|
||||
type: false,
|
||||
message: `用户名或密码不正确: ${e}`
|
||||
};
|
||||
reject(msg);
|
||||
} else {
|
||||
let msg = {
|
||||
type: true,
|
||||
message: `验证成功`,
|
||||
info: users[0]
|
||||
};
|
||||
resolve(msg);
|
||||
}
|
||||
client.unbind();
|
||||
});
|
||||
} else {
|
||||
let msg = {
|
||||
type: false,
|
||||
message: `用户名不存在`
|
||||
};
|
||||
reject(msg);
|
||||
client.unbind();
|
||||
}
|
||||
});
|
||||
};
|
||||
// 将client绑定LDAP Server
|
||||
// 第一个参数: 是用户,必须是从根结点到用户节点的全路径
|
||||
// 第二个参数: 用户密码
|
||||
return new Promise((resolve, reject) => {
|
||||
if (ldapLogin.bindPassword) {
|
||||
client.bind(ldapLogin.baseDn, ldapLogin.bindPassword, err => {
|
||||
if (err) {
|
||||
let msg = {
|
||||
type: false,
|
||||
message: `LDAP server绑定失败: ${err}`
|
||||
};
|
||||
reject(msg);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}).then(() => {
|
||||
const searchDn = ldapLogin.searchDn;
|
||||
const searchStandard = ldapLogin.searchStandard;
|
||||
// 处理可以自定义filter
|
||||
let customFilter;
|
||||
if (/^(&|\|)/gi.test(searchStandard)) {
|
||||
customFilter = searchStandard.replace(/%s/g,username);
|
||||
} else {
|
||||
customFilter = `${searchStandard}=${username}`;
|
||||
}
|
||||
const opts = {
|
||||
// filter: `(${searchStandard}=${username})`,
|
||||
filter: `(${customFilter})`,
|
||||
scope: 'sub'
|
||||
};
|
||||
|
||||
// 开始查询
|
||||
// 第一个参数: 查询基础路径,代表在查询用户信息将在这个路径下进行,该路径由根结点开始
|
||||
// 第二个参数: 查询选项
|
||||
client.search(searchDn, opts, ldapSearch);
|
||||
});
|
||||
});
|
||||
};
|
||||
178
server/utils/mongoose-auto-increment.js
Normal file
178
server/utils/mongoose-auto-increment.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// Module Scope
|
||||
var mongoose = require('mongoose'),
|
||||
extend = require('extend'),
|
||||
counterSchema,
|
||||
IdentityCounter;
|
||||
|
||||
// Initialize plugin by creating counter collection in database.
|
||||
exports.initialize = function (connection) {
|
||||
try {
|
||||
IdentityCounter = mongoose.model('IdentityCounter');
|
||||
} catch (ex) {
|
||||
if (ex.name === 'MissingSchemaError') {
|
||||
// Create new counter schema.
|
||||
counterSchema = new mongoose.Schema({
|
||||
model: { type: String, require: true },
|
||||
field: { type: String, require: true },
|
||||
count: { type: Number, default: 0 }
|
||||
});
|
||||
|
||||
// Create a unique index using the "field" and "model" fields.
|
||||
counterSchema.index({ field: 1, model: 1 }, { unique: true, required: true, index: -1 });
|
||||
|
||||
// Create model using new schema.
|
||||
IdentityCounter = mongoose.model('IdentityCounter', counterSchema);
|
||||
}
|
||||
else
|
||||
throw ex;
|
||||
}
|
||||
};
|
||||
|
||||
// The function to use when invoking the plugin on a custom schema.
|
||||
exports.plugin = function (schema, options) {
|
||||
|
||||
// If we don't have reference to the counterSchema or the IdentityCounter model then the plugin was most likely not
|
||||
// initialized properly so throw an error.
|
||||
if (!counterSchema || !IdentityCounter) throw new Error("mongoose-auto-increment has not been initialized");
|
||||
|
||||
// Default settings and plugin scope variables.
|
||||
var settings = {
|
||||
model: null, // The model to configure the plugin for.
|
||||
field: '_id', // The field the plugin should track.
|
||||
startAt: 0, // The number the count should start at.
|
||||
incrementBy: 1, // The number by which to increment the count each time.
|
||||
unique: true // Should we create a unique index for the field
|
||||
},
|
||||
fields = {}, // A hash of fields to add properties to in Mongoose.
|
||||
ready = false; // True if the counter collection has been updated and the document is ready to be saved.
|
||||
|
||||
switch (typeof(options)) {
|
||||
// If string, the user chose to pass in just the model name.
|
||||
case 'string':
|
||||
settings.model = options;
|
||||
break;
|
||||
// If object, the user passed in a hash of options.
|
||||
case 'object':
|
||||
extend(settings, options);
|
||||
break;
|
||||
}
|
||||
|
||||
if (settings.model == null)
|
||||
throw new Error("model must be set");
|
||||
|
||||
// Add properties for field in schema.
|
||||
fields[settings.field] = {
|
||||
type: Number,
|
||||
require: true
|
||||
};
|
||||
if (settings.field !== '_id')
|
||||
fields[settings.field].unique = settings.unique
|
||||
schema.add(fields);
|
||||
|
||||
// Find the counter for this model and the relevant field.
|
||||
IdentityCounter.findOne(
|
||||
{ model: settings.model, field: settings.field },
|
||||
function (err, counter) {
|
||||
if (!counter) {
|
||||
// If no counter exists then create one and save it.
|
||||
counter = new IdentityCounter({ model: settings.model, field: settings.field, count: settings.startAt - settings.incrementBy });
|
||||
counter.save(function () {
|
||||
ready = true;
|
||||
});
|
||||
}
|
||||
else {
|
||||
ready = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Declare a function to get the next counter for the model/schema.
|
||||
var nextCount = function (callback) {
|
||||
IdentityCounter.findOne({
|
||||
model: settings.model,
|
||||
field: settings.field
|
||||
}, function (err, counter) {
|
||||
if (err) return callback(err);
|
||||
callback(null, counter === null ? settings.startAt : counter.count + settings.incrementBy);
|
||||
});
|
||||
};
|
||||
// Add nextCount as both a method on documents and a static on the schema for convenience.
|
||||
schema.method('nextCount', nextCount);
|
||||
schema.static('nextCount', nextCount);
|
||||
|
||||
// Declare a function to reset counter at the start value - increment value.
|
||||
var resetCount = function (callback) {
|
||||
IdentityCounter.findOneAndUpdate(
|
||||
{ model: settings.model, field: settings.field },
|
||||
{ count: settings.startAt - settings.incrementBy },
|
||||
{ new: true }, // new: true specifies that the callback should get the updated counter.
|
||||
function (err) {
|
||||
if (err) return callback(err);
|
||||
callback(null, settings.startAt);
|
||||
}
|
||||
);
|
||||
};
|
||||
// Add resetCount as both a method on documents and a static on the schema for convenience.
|
||||
schema.method('resetCount', resetCount);
|
||||
schema.static('resetCount', resetCount);
|
||||
|
||||
// Every time documents in this schema are saved, run this logic.
|
||||
schema.pre('save', function (next) {
|
||||
// Get reference to the document being saved.
|
||||
var doc = this;
|
||||
|
||||
// Only do this if it is a new document (see http://mongoosejs.com/docs/api.html#document_Document-isNew)
|
||||
if (doc.isNew) {
|
||||
// Declare self-invoking save function.
|
||||
(function save() {
|
||||
// If ready, run increment logic.
|
||||
// Note: ready is true when an existing counter collection is found or after it is created for the
|
||||
// first time.
|
||||
if (ready) {
|
||||
// check that a number has already been provided, and update the counter to that number if it is
|
||||
// greater than the current count
|
||||
if (typeof doc[settings.field] === 'number') {
|
||||
IdentityCounter.findOneAndUpdate(
|
||||
// IdentityCounter documents are identified by the model and field that the plugin was invoked for.
|
||||
// Check also that count is less than field value.
|
||||
{ model: settings.model, field: settings.field, count: { $lt: doc[settings.field] } },
|
||||
// Change the count of the value found to the new field value.
|
||||
{ count: doc[settings.field] },
|
||||
function (err) {
|
||||
if (err) return next(err);
|
||||
// Continue with default document save functionality.
|
||||
next();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Find the counter collection entry for this model and field and update it.
|
||||
IdentityCounter.findOneAndUpdate(
|
||||
// IdentityCounter documents are identified by the model and field that the plugin was invoked for.
|
||||
{ model: settings.model, field: settings.field },
|
||||
// Increment the count by `incrementBy`.
|
||||
{ $inc: { count: settings.incrementBy } },
|
||||
// new:true specifies that the callback should get the counter AFTER it is updated (incremented).
|
||||
{ new: true },
|
||||
// Receive the updated counter.
|
||||
function (err, updatedIdentityCounter) {
|
||||
if (err) return next(err);
|
||||
// If there are no errors then go ahead and set the document's field to the current count.
|
||||
doc[settings.field] = updatedIdentityCounter.count;
|
||||
// Continue with default document save functionality.
|
||||
next();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// If not ready then set a 5 millisecond timer and try to save again. It will keep doing this until
|
||||
// the counter collection is ready.
|
||||
else
|
||||
setTimeout(save, 5);
|
||||
})();
|
||||
}
|
||||
// If the document does not have the field we're interested in or that field isn't a number AND the user did
|
||||
// not specify that we should increment on updates, then just continue the save without any increment logic.
|
||||
else
|
||||
next();
|
||||
});
|
||||
};
|
||||
63
server/utils/notice.js
Normal file
63
server/utils/notice.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const yapi = require('../yapi.js');
|
||||
|
||||
function arrUnique(arr1, arr2) {
|
||||
let arr = arr1.concat(arr2);
|
||||
let res = arr.filter(function(item, index, arr) {
|
||||
return arr.indexOf(item) === index;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
const noticeObj = {
|
||||
mail: {
|
||||
title: '邮件',
|
||||
hander: (emails, title, content)=>{
|
||||
yapi.commons.sendMail({
|
||||
to: emails,
|
||||
contents: content,
|
||||
subject: title
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yapi.emitHook('addNotice', noticeObj)
|
||||
|
||||
yapi.commons.sendNotice = async function(projectId, data) {
|
||||
const projectModel = require('../models/project.js');
|
||||
const userModel = require('../models/user.js');
|
||||
const followModel = require('../models/follow.js');
|
||||
|
||||
const followInst = yapi.getInst(followModel);
|
||||
const userInst = yapi.getInst(userModel);
|
||||
const projectInst = yapi.getInst(projectModel);
|
||||
const list = await followInst.listByProjectId(projectId);
|
||||
const starUsers = list.map(item => item.uid);
|
||||
|
||||
const projectList = await projectInst.get(projectId);
|
||||
const projectMenbers = projectList.members
|
||||
.filter(item => item.email_notice)
|
||||
.map(item => item.uid);
|
||||
|
||||
const users = arrUnique(projectMenbers, starUsers);
|
||||
const usersInfo = await userInst.findByUids(users);
|
||||
const emails = usersInfo.map(item => item.email).join(',');
|
||||
|
||||
try {
|
||||
Object.keys(noticeObj).forEach(key=>{
|
||||
let noticeItem = noticeObj[key];
|
||||
try{
|
||||
noticeItem.hander(emails, data.title, data.content)
|
||||
}catch(err){
|
||||
yapi.commons.log('发送' + (noticeItem.title || key) + '失败' + err.message, 'error')
|
||||
}
|
||||
})
|
||||
// yapi.commons.sendMail({
|
||||
// to: emails,
|
||||
// contents: data.content,
|
||||
// subject: data.title
|
||||
// });
|
||||
} catch (e) {
|
||||
yapi.commons.log('发送失败:' + e, 'error');
|
||||
}
|
||||
};
|
||||
442
server/utils/reportHtml/defaultTheme.css
Normal file
442
server/utils/reportHtml/defaultTheme.css
Normal file
@@ -0,0 +1,442 @@
|
||||
@charset "UTF-8";
|
||||
html,
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* 设置滚动条的样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
/* 外层轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset006pxrgba(255, 0, 0, 0.3);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
-webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:window-inactive {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.yapi-run-auto-test {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 25px;
|
||||
color: #393838;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test table {
|
||||
margin: 10px 0 15px 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test td,
|
||||
th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test th {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test a, a:link, a:visited {
|
||||
color: #34495e;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test a:hover, a:focus {
|
||||
color: #59d69d;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test p {
|
||||
padding-left: 10px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #404040;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test h1 {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
font-size: 32px;
|
||||
padding-bottom: 16px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test h2 {
|
||||
font-size: 28px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test h3 {
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 3px solid #59d69d;
|
||||
padding-left: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test h6 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test hr {
|
||||
margin: 0 0 19px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test blockquote {
|
||||
padding: 13px 13px 21px 15px;
|
||||
margin-bottom: 18px;
|
||||
font-family: georgia, serif;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test blockquote:before {
|
||||
font-size: 40px;
|
||||
margin-left: -10px;
|
||||
font-family: georgia, serif;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test blockquote p {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 18px;
|
||||
margin-bottom: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test code,
|
||||
pre {
|
||||
font-family: Monaco, Andale Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test code {
|
||||
background-color: #fee9cc;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
padding: 1px 3px;
|
||||
font-size: 12px;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test pre {
|
||||
display: block;
|
||||
padding: 14px;
|
||||
margin: 0 0 18px;
|
||||
line-height: 16px;
|
||||
font-size: 11px;
|
||||
border: 1px solid #d9d9d9;
|
||||
white-space: pre-wrap;
|
||||
background: #f6f6f6;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test pre code {
|
||||
background-color: #f6f6f6;
|
||||
color: #737373;
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test sup {
|
||||
font-size: 0.83em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body,
|
||||
code,
|
||||
pre code,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: black;
|
||||
}
|
||||
|
||||
table,
|
||||
pre {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .menu-left {
|
||||
position: fixed;
|
||||
top: 61px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .menu-left > .list-content {
|
||||
overflow: auto;
|
||||
margin: 0px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 8px 0 20px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .list {
|
||||
padding: 2px 0px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
|
||||
.yapi-run-auto-test .content-right {
|
||||
max-width: 700px;
|
||||
margin-left: 290px;
|
||||
padding-left: 70px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.yapi-run-auto-test .content-right h2:target {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test > p {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test > table {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test > pre {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .curProject {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
font-size: 25px;
|
||||
color: black;
|
||||
margin-left: -240px;
|
||||
width: 240px;
|
||||
padding: 5px;
|
||||
line-height: 25px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .g-doc {
|
||||
margin-top: 56px;
|
||||
padding-top: 24px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .curproject-name {
|
||||
font-size: 42px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .m-header {
|
||||
background: #32363a;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
padding-left: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
z-index: 9;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.yapi-run-auto-test .m-header .title {
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
margin: 0;
|
||||
margin-left: 16px;
|
||||
padding: 0;
|
||||
line-height: 56px;
|
||||
border: none;
|
||||
}
|
||||
.yapi-run-auto-test .m-header .nav {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
top: 0;
|
||||
}
|
||||
.yapi-run-auto-test .m-header .nav a {
|
||||
color: #fff;
|
||||
margin-left: 16px;
|
||||
padding: 8px;
|
||||
transition: color .2s;
|
||||
}
|
||||
.yapi-run-auto-test .m-header .nav a:hover {
|
||||
color: #59d69d;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .m-footer {
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.yapi-run-auto-test .row{
|
||||
position: relative;
|
||||
height: auto;
|
||||
zoom: 1;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .case-report {
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .case-report .case-report-title {
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .col-3 {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .col-21 {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 87.5%;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .icon {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
vertical-align: baseline;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .icon-check-circle:before {
|
||||
content: "\2713";
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .icon-close-circle:before {
|
||||
content: "\2715";
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .icon-warning-circle:before {
|
||||
content: "!";
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .icon:before {
|
||||
display: block;
|
||||
font-family: "anticon" !important;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .summary {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .success{
|
||||
color: #208054;
|
||||
font-weight: 700;
|
||||
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .status {
|
||||
flex-shrink: 0;
|
||||
margin: 0 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
border-radius: 2px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .status-ok {
|
||||
background-color: #17c5a6;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.yapi-run-auto-test .status-ko {
|
||||
background-color: #fd3c3c;
|
||||
}
|
||||
|
||||
.yapi-run-auto-test .status-warning {
|
||||
background-color: #ffb74c;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=defaultTheme.css.map */
|
||||
4
server/utils/reportHtml/defaultTheme.js
Normal file
4
server/utils/reportHtml/defaultTheme.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const sysPath = require('path');
|
||||
const css = fs.readFileSync(sysPath.join(__dirname, './defaultTheme.css'));
|
||||
module.exports = '<style>' + css + '</style>';
|
||||
207
server/utils/reportHtml/index.js
Normal file
207
server/utils/reportHtml/index.js
Normal file
@@ -0,0 +1,207 @@
|
||||
const defaultTheme = require('./defaultTheme.js');
|
||||
|
||||
function json_format(json) {
|
||||
if (json && typeof json === 'object') {
|
||||
return JSON.stringify(json, null, ' ');
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
module.exports = function renderToHtml(reports) {
|
||||
let tp = createHtml(reports);
|
||||
return tp;
|
||||
};
|
||||
|
||||
function createHtml(reports) {
|
||||
let mdTemplate = ``;
|
||||
let left = ``;
|
||||
reports.list.map((item, index) => {
|
||||
mdTemplate += baseHtml(index, item.name, item.path, item.status);
|
||||
mdTemplate += validHtml(item.validRes);
|
||||
mdTemplate += requestHtml(item.url, item.headers, item.data);
|
||||
mdTemplate += reponseHtml(item.res_header, item.res_body);
|
||||
left += leftHtml(index, item.name, item.code);
|
||||
// left += codeHtml(item.code);
|
||||
});
|
||||
return createHtml5(left, mdTemplate, reports.message, reports.runTime);
|
||||
}
|
||||
|
||||
function createHtml5(left, tp, msg, runTime) {
|
||||
let message = ``;
|
||||
if (msg.failedNum === 0) {
|
||||
message += `<div>一共 <span class="success">${
|
||||
msg.successNum
|
||||
}</span> 测试用例, 全部验证通过(${runTime})</div>`;
|
||||
} else {
|
||||
message += `<div>一共 ${msg.len} 测试用例,<span class="success"> ${
|
||||
msg.successNum
|
||||
}</span> 个验证通过, ${msg.failedNum} 个未通过(${runTime})</div>`;
|
||||
}
|
||||
|
||||
//html5模板
|
||||
let html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>测试报告</title>
|
||||
<meta charset="utf-8" />
|
||||
${defaultTheme}
|
||||
</head>
|
||||
<body class="yapi-run-auto-test">
|
||||
<div class="m-header">
|
||||
<a href="#" style="display: inherit;"><svg class="svg" width="32px" height="32px" viewBox="0 0 64 64" version="1.1"><title>Icon</title><desc>Created with Sketch.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#F2F2F2" offset="100%"></stop></linearGradient><circle id="path-2" cx="31.9988602" cy="31.9988602" r="2.92886048"></circle><filter x="-85.4%" y="-68.3%" width="270.7%" height="270.7%" filterUnits="objectBoundingBox" id="filter-3"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset><feGaussianBlur stdDeviation="1.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.159703351 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix></filter></defs><g id="首页" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="大屏幕"><g id="Icon"><circle id="Oval-1" fill="url(#linearGradient-1)" cx="32" cy="32" r="32"></circle><path d="M36.7078009,31.8054514 L36.7078009,51.7110548 C36.7078009,54.2844537 34.6258634,56.3695395 32.0579205,56.3695395 C29.4899777,56.3695395 27.4099998,54.0704461 27.4099998,51.7941246 L27.4099998,31.8061972 C27.4099998,29.528395 29.4909575,27.218453 32.0589004,27.230043 C34.6268432,27.241633 36.7078009,29.528395 36.7078009,31.8054514 Z" id="blue" fill="#2359F1" fill-rule="nonzero"></path><path d="M45.2586091,17.1026914 C45.2586091,17.1026914 45.5657231,34.0524383 45.2345291,37.01141 C44.9033351,39.9703817 43.1767091,41.6667796 40.6088126,41.6667796 C38.040916,41.6667796 35.9609757,39.3676862 35.9609757,37.0913646 L35.9609757,17.1034372 C35.9609757,14.825635 38.0418959,12.515693 40.6097924,12.527283 C43.177689,12.538873 45.2586091,14.825635 45.2586091,17.1026914 Z" id="green" fill="#57CF27" fill-rule="nonzero" transform="translate(40.674608, 27.097010) rotate(60.000000) translate(-40.674608, -27.097010) "></path><path d="M28.0410158,17.0465598 L28.0410158,36.9521632 C28.0410158,39.525562 25.9591158,41.6106479 23.3912193,41.6106479 C20.8233227,41.6106479 18.7433824,39.3115545 18.7433824,37.035233 L18.7433824,17.0473055 C18.7433824,14.7695034 20.8243026,12.4595614 23.3921991,12.4711513 C25.9600956,12.4827413 28.0410158,14.7695034 28.0410158,17.0465598 Z" id="red" fill="#FF561B" fill-rule="nonzero" transform="translate(23.392199, 27.040878) rotate(-60.000000) translate(-23.392199, -27.040878) "></path><g id="inner-round"><use fill="black" fill-opacity="1" filter="url(#filter-3)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path-2"></use><use fill="#F7F7F7" fill-rule="evenodd" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path-2"></use></g></g></g></g></svg></a>
|
||||
<a href="#"><h1 class="title">YAPI 测试结果文档</h1></a>
|
||||
<div class="nav">
|
||||
<a href="https://hellosean1025.github.io/yapi">YApi</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="g-doc">
|
||||
<div class="menu-left">
|
||||
${left}
|
||||
</div>
|
||||
<div id="right" class="content-right">
|
||||
<h1>YApi 测试报告</h1>
|
||||
<div class="summary">${message}</div>
|
||||
${tp}
|
||||
<footer class="m-footer">
|
||||
<p>Build by <a href="https://ymfe.org/">YMFE</a>.</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return html;
|
||||
}
|
||||
|
||||
function requestHtml(url, headers, params) {
|
||||
headers = json_format(headers, null, ' ');
|
||||
params = json_format(params);
|
||||
let html = ``;
|
||||
html += `
|
||||
<div>
|
||||
<h3>Request</h3>
|
||||
<div class="row case-report">
|
||||
<div class="col-3 case-report-title">Url</div>
|
||||
<div class="col-21">${url}</div>
|
||||
</div>`;
|
||||
html += headers
|
||||
? `<div class="row case-report">
|
||||
<div class="col-3 case-report-title">Headers</div>
|
||||
<div class="col-21">
|
||||
<pre>${headers}</pre>
|
||||
</div>
|
||||
</div>`
|
||||
: ``;
|
||||
|
||||
html += params
|
||||
? ` <div class="row case-report">
|
||||
<div class="col-3 case-report-title">Body</div>
|
||||
<div class="col-21">
|
||||
<pre>${params}</pre>
|
||||
</div>
|
||||
</div>`
|
||||
: ``;
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function reponseHtml(res_header, res_body) {
|
||||
res_header = json_format(res_header, null, ' ');
|
||||
res_body = json_format(res_body, null, ' ');
|
||||
let html = ``;
|
||||
html += `<div><h3>Reponse</h3>`;
|
||||
|
||||
html += res_header
|
||||
? `
|
||||
<div class="row case-report">
|
||||
<div class="col-3 case-report-title">Headers</div>
|
||||
<div class="col-21">
|
||||
<pre>${res_header}</pre>
|
||||
</div>
|
||||
</div>`
|
||||
: ``;
|
||||
|
||||
html += res_body
|
||||
? ` <div class="row case-report">
|
||||
<div class="col-3 case-report-title">Body</div>
|
||||
<div class="col-21">
|
||||
<pre>${res_body}</pre>
|
||||
</div>
|
||||
</div>`
|
||||
: ``;
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function validHtml(validRes) {
|
||||
if (validRes && Array.isArray(validRes)) {
|
||||
validRes = validRes.map((item, index) => {
|
||||
return `<div key=${index}>${item.message}</div>`;
|
||||
});
|
||||
}
|
||||
let html = `
|
||||
<div>
|
||||
<div class="row case-report">
|
||||
<div class="col-3 case-report-title">验证结果</div>
|
||||
<div class="col-21">
|
||||
${validRes}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function baseHtml(index, name, path, status) {
|
||||
let html = `
|
||||
<div>
|
||||
<h2 id=${index}>${name}</h2>
|
||||
<h3>基本信息</h3>
|
||||
<div class="row case-report">
|
||||
<div class="col-3 case-report-title">Path</div>
|
||||
<div class="col-21">${path}</div>
|
||||
</div>
|
||||
<div class="row case-report">
|
||||
<div class="col-3 case-report-title">Status</div>
|
||||
<div class="col-21">${status}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function leftHtml(index, name, code) {
|
||||
let html = `
|
||||
<div class="list-content">
|
||||
<a class="list" href="#${index}">${name}</a>
|
||||
${codeHtml(code)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function codeHtml(code) {
|
||||
let codeHtml = ``;
|
||||
switch (code) {
|
||||
case 0:
|
||||
codeHtml += `<div title="验证通过" class="status status-ok"><i class="icon icon-check-circle"></i></div>`;
|
||||
break;
|
||||
case 400:
|
||||
codeHtml += `<div title="请求异常" class="status status-ko"><i class="icon icon-close-circle"></i></div>`;
|
||||
break;
|
||||
case 1:
|
||||
codeHtml += `<div title="验证失败" class="status status-warning"><i class="icon icon-warning-circle"></i></div>`;
|
||||
break;
|
||||
default:
|
||||
codeHtml += `<div title="验证通过" class="status status-warning"><i class="icon icon-warning-circle"></i></div>`;
|
||||
break;
|
||||
}
|
||||
return codeHtml;
|
||||
}
|
||||
33
server/utils/sandbox.js
Normal file
33
server/utils/sandbox.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const Safeify = require('safeify').default;
|
||||
|
||||
module.exports = async function sandboxFn(context, script, autoTest) {
|
||||
// 创建 safeify 实例
|
||||
const safeVm = new Safeify({
|
||||
timeout: 3000, //超时时间
|
||||
asyncTimeout: 10000, //包含异步操作的超时时间
|
||||
unrestricted: true,
|
||||
quantity: 4, //沙箱进程数量,默认同 CPU 核数
|
||||
memoryQuota: 500, //沙箱最大能使用的内存(单位 m),默认 500m
|
||||
cpuQuota: 0.5, //沙箱的 cpu 资源配额(百分比),默认 50%
|
||||
unsafe: {
|
||||
modules: {
|
||||
assert: 'assert',
|
||||
mockjs: 'mockjs'
|
||||
}
|
||||
}
|
||||
})
|
||||
safeVm.preset('const assert = require("assert");const Mock = require("mockjs"); const Random = Mock.Random;');
|
||||
|
||||
// 执行动态代码·
|
||||
if (autoTest) {
|
||||
script += "\n return {}";
|
||||
} else {
|
||||
script += "\n return {mockJson, resHeader, httpCode, delay}";
|
||||
}
|
||||
|
||||
const result = await safeVm.run(script, context)
|
||||
|
||||
// 释放资源
|
||||
safeVm.destroy()
|
||||
return result
|
||||
}
|
||||
28
server/utils/storage.js
Normal file
28
server/utils/storage.js
Normal file
@@ -0,0 +1,28 @@
|
||||
module.exports = function storageCreator(id) {
|
||||
const storageModel = require('../models/storage.js');
|
||||
const yapi = require('../yapi.js');
|
||||
const defaultData = {}
|
||||
return {
|
||||
getItem: async (name = '') => {
|
||||
let inst = yapi.getInst(storageModel);
|
||||
let data = await inst.get(id);
|
||||
data = data || defaultData;
|
||||
if (name) return data[name];
|
||||
return data;
|
||||
},
|
||||
setItem: async (name, value) => {
|
||||
let inst = yapi.getInst(storageModel);
|
||||
let curData = await inst.get(id);
|
||||
let data = curData || defaultData;
|
||||
let result;
|
||||
data[name] = value;
|
||||
if(!curData){
|
||||
result = await inst.save(id, data, true)
|
||||
}else{
|
||||
result = await inst.save(id, data, false)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
server/utils/token.js
Normal file
67
server/utils/token.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const yapi = require('../yapi')
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
/*
|
||||
下面是使用加密算法
|
||||
*/
|
||||
|
||||
// 创建加密算法
|
||||
const aseEncode = function(data, password) {
|
||||
|
||||
// 如下方法使用指定的算法与密码来创建cipher对象
|
||||
const cipher = crypto.createCipher('aes192', password);
|
||||
|
||||
// 使用该对象的update方法来指定需要被加密的数据
|
||||
let crypted = cipher.update(data, 'utf-8', 'hex');
|
||||
|
||||
crypted += cipher.final('hex');
|
||||
|
||||
return crypted;
|
||||
};
|
||||
|
||||
// 创建解密算法
|
||||
const aseDecode = function(data, password) {
|
||||
/*
|
||||
该方法使用指定的算法与密码来创建 decipher对象, 第一个算法必须与加密数据时所使用的算法保持一致;
|
||||
第二个参数用于指定解密时所使用的密码,其参数值为一个二进制格式的字符串或一个Buffer对象,该密码同样必须与加密该数据时所使用的密码保持一致
|
||||
*/
|
||||
const decipher = crypto.createDecipher('aes192', password);
|
||||
|
||||
/*
|
||||
第一个参数为一个Buffer对象或一个字符串,用于指定需要被解密的数据
|
||||
第二个参数用于指定被解密数据所使用的编码格式,可指定的参数值为 'hex', 'binary', 'base64'等,
|
||||
第三个参数用于指定输出解密数据时使用的编码格式,可选参数值为 'utf-8', 'ascii' 或 'binary';
|
||||
*/
|
||||
let decrypted = decipher.update(data, 'hex', 'utf-8');
|
||||
|
||||
decrypted += decipher.final('utf-8');
|
||||
return decrypted;
|
||||
};
|
||||
|
||||
const defaultSalt = 'abcde';
|
||||
|
||||
exports.getToken = function getToken(token, uid){
|
||||
if(!token)throw new Error('token 不能为空')
|
||||
yapi.WEBCONFIG.passsalt = yapi.WEBCONFIG.passsalt || defaultSalt;
|
||||
return aseEncode(uid + '|' + token, yapi.WEBCONFIG.passsalt)
|
||||
}
|
||||
|
||||
exports.parseToken = function parseToken(token){
|
||||
if(!token)throw new Error('token 不能为空')
|
||||
yapi.WEBCONFIG.passsalt = yapi.WEBCONFIG.passsalt || defaultSalt;
|
||||
let tokens;
|
||||
try{
|
||||
tokens = aseDecode(token, yapi.WEBCONFIG.passsalt)
|
||||
}catch(e){}
|
||||
if(tokens && typeof tokens === 'string' && tokens.indexOf('|') > 0){
|
||||
tokens = tokens.split('|')
|
||||
return {
|
||||
uid: tokens[0],
|
||||
projectToken: tokens[1]
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user