const interfaceModel = require('../models/interface.js');
const interfaceCatModel = require('../models/interfaceCat.js');
const interfaceCaseModel = require('../models/interfaceCase.js');
const followModel = require('../models/follow.js');
const groupModel = require('../models/group.js');
const _ = require('underscore');
const url = require('url');
const baseController = require('./base.js');
const yapi = require('../yapi.js');
const userModel = require('../models/user.js');
const projectModel = require('../models/project.js');
const jsondiffpatch = require('jsondiffpatch');
const formattersHtml = jsondiffpatch.formatters.html;
const showDiffMsg = require('../../common/diff-view.js');
const mergeJsonSchema = require('../../common/mergeJsonSchema');
const fs = require('fs-extra');
const path = require('path');
// const annotatedCss = require("jsondiffpatch/public/formatters-styles/annotated.css");
// const htmlCss = require("jsondiffpatch/public/formatters-styles/html.css");
function handleHeaders(values){
let isfile = false,
isHaveContentType = false;
if (values.req_body_type === 'form') {
values.req_body_form.forEach(item => {
if (item.type === 'file') {
isfile = true;
}
});
values.req_headers.map(item => {
if (item.name === 'Content-Type') {
item.value = isfile ? 'multipart/form-data' : 'application/x-www-form-urlencoded';
isHaveContentType = true;
}
});
if (isHaveContentType === false) {
values.req_headers.unshift({
name: 'Content-Type',
value: isfile ? 'multipart/form-data' : 'application/x-www-form-urlencoded'
});
}
} else if (values.req_body_type === 'json') {
values.req_headers
? values.req_headers.map(item => {
if (item.name === 'Content-Type') {
item.value = 'application/json';
isHaveContentType = true;
}
})
: [];
if (isHaveContentType === false) {
values.req_headers = values.req_headers || [];
values.req_headers.unshift({
name: 'Content-Type',
value: 'application/json'
});
}
}
}
class interfaceController extends baseController {
constructor(ctx) {
super(ctx);
this.Model = yapi.getInst(interfaceModel);
this.catModel = yapi.getInst(interfaceCatModel);
this.projectModel = yapi.getInst(projectModel);
this.caseModel = yapi.getInst(interfaceCaseModel);
this.followModel = yapi.getInst(followModel);
this.userModel = yapi.getInst(userModel);
this.groupModel = yapi.getInst(groupModel);
const minLengthStringField = {
type: 'string',
minLength: 1
};
const addAndUpCommonField = {
desc: 'string',
status: 'string',
req_query: [
{
name: 'string',
value: 'string',
example: 'string',
desc: 'string',
required: 'string'
}
],
req_headers: [
{
name: 'string',
value: 'string',
example: 'string',
desc: 'string',
required: 'string'
}
],
req_body_type: 'string',
req_params: [
{
name: 'string',
example: 'string',
desc: 'string'
}
],
req_body_form: [
{
name: 'string',
type: {
type: 'string'
},
example: 'string',
desc: 'string',
required: 'string'
}
],
req_body_other: 'string',
res_body_type: 'string',
res_body: 'string',
custom_field_value: 'string',
api_opened: 'boolean',
req_body_is_json_schema: 'string',
res_body_is_json_schema: 'string',
markdown: 'string',
tag: 'array'
};
this.schemaMap = {
add: Object.assign(
{
'*project_id': 'number',
'*path': minLengthStringField,
'*title': minLengthStringField,
'*method': minLengthStringField,
'*catid': 'number'
},
addAndUpCommonField
),
up: Object.assign(
{
'*id': 'number',
project_id: 'number',
path: minLengthStringField,
title: minLengthStringField,
method: minLengthStringField,
catid: 'number',
switch_notice: 'boolean',
message: minLengthStringField
},
addAndUpCommonField
),
save: Object.assign(
{
project_id: 'number',
catid: 'number',
title: minLengthStringField,
path: minLengthStringField,
method: minLengthStringField,
message: minLengthStringField,
switch_notice: 'boolean',
dataSync: 'string'
},
addAndUpCommonField
)
};
}
/**
* 添加项目分组
* @interface /interface/add
* @method POST
* @category interface
* @foldnumber 10
* @param {Number} project_id 项目id,不能为空
* @param {String} title 接口标题,不能为空
* @param {String} path 接口请求路径,不能为空
* @param {String} method 请求方式
* @param {Array} [req_headers] 请求的header信息
* @param {String} [req_headers[].name] 请求的header信息名
* @param {String} [req_headers[].value] 请求的header信息值
* @param {Boolean} [req_headers[].required] 是否是必须,默认为否
* @param {String} [req_headers[].desc] header描述
* @param {String} [req_body_type] 请求参数方式,有["form", "json", "text", "xml"]四种
* @param {Array} [req_params] name, desc两个参数
* @param {Mixed} [req_body_form] 请求参数,如果请求方式是form,参数是Array数组,其他格式请求参数是字符串
* @param {String} [req_body_form[].name] 请求参数名
* @param {String} [req_body_form[].value] 请求参数值,可填写生成规则(mock)。如@email,随机生成一条email
* @param {String} [req_body_form[].type] 请求参数类型,有["text", "file"]两种
* @param {String} [req_body_other] 非form类型的请求参数可保存到此字段
* @param {String} [res_body_type] 相应信息的数据格式,有["json", "text", "xml"]三种
* @param {String} [res_body] 响应信息,可填写任意字符串,如果res_body_type是json,则会调用mock功能
* @param {String} [desc] 接口描述
* @returns {Object}
* @example ./api/interface/add.json
*/
async add(ctx) {
let params = ctx.params;
if (!this.$tokenAuth) {
let auth = await this.checkAuth(params.project_id, 'project', 'edit');
if (!auth) {
return (ctx.body = yapi.commons.resReturn(null, 40033, '没有权限'));
}
}
params.method = params.method || 'GET';
params.res_body_is_json_schema = _.isUndefined(params.res_body_is_json_schema)
? false
: params.res_body_is_json_schema;
params.req_body_is_json_schema = _.isUndefined(params.req_body_is_json_schema)
? false
: params.req_body_is_json_schema;
params.method = params.method.toUpperCase();
params.req_params = params.req_params || [];
params.res_body_type = params.res_body_type ? params.res_body_type.toLowerCase() : 'json';
let http_path = url.parse(params.path, true);
if (!yapi.commons.verifyPath(http_path.pathname)) {
return (ctx.body = yapi.commons.resReturn(
null,
400,
'path第一位必需为 /, 只允许由 字母数字-/_:.! 组成'
));
}
handleHeaders(params)
params.query_path = {};
params.query_path.path = http_path.pathname;
params.query_path.params = [];
Object.keys(http_path.query).forEach(item => {
params.query_path.params.push({
name: item,
value: http_path.query[item]
});
});
let checkRepeat = await this.Model.checkRepeat(params.project_id, params.path, params.method);
if (checkRepeat > 0) {
return (ctx.body = yapi.commons.resReturn(
null,
40022,
'已存在的接口:' + params.path + '[' + params.method + ']'
));
}
let data = Object.assign(params, {
uid: this.getUid(),
add_time: yapi.commons.time(),
up_time: yapi.commons.time()
});
yapi.commons.handleVarPath(params.path, params.req_params);
if (params.req_params.length > 0) {
data.type = 'var';
data.req_params = params.req_params;
} else {
data.type = 'static';
}
// 新建接口的人成为项目dev 如果不存在的话
// 命令行导入时无法获知导入接口人的信息,其uid 为 999999
let uid = this.getUid();
if (this.getRole() !== 'admin' && uid !== 999999) {
let userdata = await yapi.commons.getUserdata(uid, 'dev');
// 检查一下是否有这个人
let check = await this.projectModel.checkMemberRepeat(params.project_id, uid);
if (check === 0 && userdata) {
await this.projectModel.addMember(params.project_id, [userdata]);
}
}
let result = await this.Model.save(data);
yapi.emitHook('interface_add', result).then();
this.catModel.get(params.catid).then(cate => {
let username = this.getUsername();
let title = `${username} 为分类 ${cate.name} 添加了接口 ${data.title} `;
yapi.commons.saveLog({
content: title,
type: 'project',
uid: this.getUid(),
username: username,
typeid: params.project_id
});
this.projectModel.up(params.project_id, { up_time: new Date().getTime() }).then();
});
await this.autoAddTag(params);
ctx.body = yapi.commons.resReturn(result);
}
/**
* 保存接口数据,如果接口存在则更新数据,如果接口不存在则添加数据
* @interface /interface/save
* @method post
* @category interface
* @foldnumber 10
* @param {Number} project_id 项目id,不能为空
* @param {String} title 接口标题,不能为空
* @param {String} path 接口请求路径,不能为空
* @param {String} method 请求方式
* @param {Array} [req_headers] 请求的header信息
* @param {String} [req_headers[].name] 请求的header信息名
* @param {String} [req_headers[].value] 请求的header信息值
* @param {Boolean} [req_headers[].required] 是否是必须,默认为否
* @param {String} [req_headers[].desc] header描述
* @param {String} [req_body_type] 请求参数方式,有["form", "json", "text", "xml"]四种
* @param {Array} [req_params] name, desc两个参数
* @param {Mixed} [req_body_form] 请求参数,如果请求方式是form,参数是Array数组,其他格式请求参数是字符串
* @param {String} [req_body_form[].name] 请求参数名
* @param {String} [req_body_form[].value] 请求参数值,可填写生成规则(mock)。如@email,随机生成一条email
* @param {String} [req_body_form[].type] 请求参数类型,有["text", "file"]两种
* @param {String} [req_body_other] 非form类型的请求参数可保存到此字段
* @param {String} [res_body_type] 相应信息的数据格式,有["json", "text", "xml"]三种
* @param {String} [res_body] 响应信息,可填写任意字符串,如果res_body_type是json,则会调用mock功能
* @param {String} [desc] 接口描述
* @returns {Object}
*/
async save(ctx) {
let params = ctx.params;
if (!this.$tokenAuth) {
let auth = await this.checkAuth(params.project_id, 'project', 'edit');
if (!auth) {
return (ctx.body = yapi.commons.resReturn(null, 40033, '没有权限'));
}
}
params.method = params.method || 'GET';
params.method = params.method.toUpperCase();
let http_path = url.parse(params.path, true);
if (!yapi.commons.verifyPath(http_path.pathname)) {
return (ctx.body = yapi.commons.resReturn(
null,
400,
'path第一位必需为 /, 只允许由 字母数字-/_:.! 组成'
));
}
let result = await this.Model.getByPath(params.project_id, params.path, params.method, '_id res_body');
if (result.length > 0) {
result.forEach(async item => {
params.id = item._id;
// console.log(this.schemaMap['up'])
let validParams = Object.assign({}, params)
let validResult = yapi.commons.validateParams(this.schemaMap['up'], validParams);
if (validResult.valid) {
let data = Object.assign({}, ctx);
data.params = validParams;
if(params.res_body_is_json_schema && params.dataSync === 'good'){
try{
let new_res_body = yapi.commons.json_parse(params.res_body)
let old_res_body = yapi.commons.json_parse(item.res_body)
data.params.res_body = JSON.stringify(mergeJsonSchema(old_res_body, new_res_body),null,2);
}catch(err){}
}
await this.up(data);
} else {
return (ctx.body = yapi.commons.resReturn(null, 400, validResult.message));
}
});
} else {
let validResult = yapi.commons.validateParams(this.schemaMap['add'], params);
if (validResult.valid) {
let data = {};
data.params = params;
await this.add(data);
} else {
return (ctx.body = yapi.commons.resReturn(null, 400, validResult.message));
}
}
ctx.body = yapi.commons.resReturn(result);
// return ctx.body = yapi.commons.resReturn(null, 400, 'path第一位必需为 /, 只允许由 字母数字-/_:.! 组成');
}
async autoAddTag(params) {
//检查是否提交了目前不存在的tag
let tags = params.tag;
if (tags && Array.isArray(tags) && tags.length > 0) {
let projectData = await this.projectModel.get(params.project_id);
let tagsInProject = projectData.tag;
let needUpdate = false;
if (tagsInProject && Array.isArray(tagsInProject) && tagsInProject.length > 0) {
tags.forEach(tag => {
if (!_.find(tagsInProject, item => {
return item.name === tag;
})) {//tag不存在
needUpdate = true;
tagsInProject.push({
name: tag,
desc: tag
});
}
});
} else {
needUpdate = true
tagsInProject = []
tags.forEach(tag => {
tagsInProject.push({
name: tag,
desc: tag
});
});
}
if (needUpdate) {//需要更新tag
let data = {
tag: tagsInProject,
up_time: yapi.commons.time()
};
await this.projectModel.up(params.project_id, data);
}
}
}
/**
* 获取项目分组
* @interface /interface/get
* @method GET
* @category interface
* @foldnumber 10
* @param {Number} id 接口id,不能为空
* @returns {Object}
* @example ./api/interface/get.json
*/
async get(ctx) {
let params = ctx.params;
if (!params.id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '接口id不能为空'));
}
try {
let result = await this.Model.get(params.id);
if(this.$tokenAuth){
if(params.project_id !== result.project_id){
ctx.body = yapi.commons.resReturn(null, 400, 'token有误')
return;
}
}
// console.log('result', result);
if (!result) {
return (ctx.body = yapi.commons.resReturn(null, 490, '不存在的'));
}
let userinfo = await this.userModel.findById(result.uid);
let project = await this.projectModel.getBaseInfo(result.project_id);
if (project.project_type === 'private') {
if ((await this.checkAuth(project._id, 'project', 'view')) !== true) {
return (ctx.body = yapi.commons.resReturn(null, 406, '没有权限'));
}
}
yapi.emitHook('interface_get', result).then();
result = result.toObject();
if (userinfo) {
result.username = userinfo.username;
}
ctx.body = yapi.commons.resReturn(result);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 接口列表
* @interface /interface/list
* @method GET
* @category interface
* @foldnumber 10
* @param {Number} project_id 项目id,不能为空
* @param {Number} page 当前页
* @param {Number} limit 每一页限制条数
* @returns {Object}
* @example ./api/interface/list.json
*/
async list(ctx) {
let project_id = ctx.params.project_id;
let page = ctx.request.query.page || 1,
limit = ctx.request.query.limit || 10;
let status = ctx.request.query.status,
tag = ctx.request.query.tag;
let project = await this.projectModel.getBaseInfo(project_id);
if (!project) {
return (ctx.body = yapi.commons.resReturn(null, 407, '不存在的项目'));
}
if (project.project_type === 'private') {
if ((await this.checkAuth(project._id, 'project', 'view')) !== true) {
return (ctx.body = yapi.commons.resReturn(null, 406, '没有权限'));
}
}
if (!project_id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '项目id不能为空'));
}
try {
let result, count;
if (limit === 'all') {
result = await this.Model.list(project_id);
count = await this.Model.listCount({project_id});
} else {
let option = {project_id};
if (status) {
if (Array.isArray(status)) {
option.status = {"$in": status};
} else {
option.status = status;
}
}
if (tag) {
if (Array.isArray(tag)) {
option.tag = {"$in": tag};
} else {
option.tag = tag;
}
}
result = await this.Model.listByOptionWithPage(option, page, limit);
count = await this.Model.listCount(option);
}
ctx.body = yapi.commons.resReturn({
count: count,
total: Math.ceil(count / limit),
list: result
});
yapi.emitHook('interface_list', result).then();
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 402, err.message);
}
}
async downloadCrx(ctx) {
let filename = 'crossRequest.zip';
let dataBuffer = yapi.fs.readFileSync(
yapi.path.join(yapi.WEBROOT, 'static/attachment/cross-request.zip')
);
ctx.set('Content-disposition', 'attachment; filename=' + filename);
ctx.set('Content-Type', 'application/zip');
ctx.body = dataBuffer;
}
async listByCat(ctx) {
let catid = ctx.request.query.catid;
let page = ctx.request.query.page || 1,
limit = ctx.request.query.limit || 10;
let status = ctx.request.query.status,
tag = ctx.request.query.tag;
if (!catid) {
return (ctx.body = yapi.commons.resReturn(null, 400, 'catid不能为空'));
}
try {
let catdata = await this.catModel.get(catid);
let project = await this.projectModel.getBaseInfo(catdata.project_id);
if (project.project_type === 'private') {
if ((await this.checkAuth(project._id, 'project', 'view')) !== true) {
return (ctx.body = yapi.commons.resReturn(null, 406, '没有权限'));
}
}
let option = {catid}
if (status) {
if (Array.isArray(status)) {
option.status = {"$in": status};
} else {
option.status = status;
}
}
if (tag) {
if (Array.isArray(tag)) {
option.tag = {"$in": tag};
} else {
option.tag = tag;
}
}
let result = await this.Model.listByOptionWithPage(option, page, limit);
let count = await this.Model.listCount(option);
ctx.body = yapi.commons.resReturn({
count: count,
total: Math.ceil(count / limit),
list: result
});
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 402, err.message + '1');
}
}
async listByMenu(ctx) {
let project_id = ctx.params.project_id;
if (!project_id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '项目id不能为空'));
}
let project = await this.projectModel.getBaseInfo(project_id);
if (!project) {
return (ctx.body = yapi.commons.resReturn(null, 406, '不存在的项目'));
}
if (project.project_type === 'private') {
if ((await this.checkAuth(project._id, 'project', 'view')) !== true) {
return (ctx.body = yapi.commons.resReturn(null, 406, '没有权限'));
}
}
try {
let result = await this.catModel.list(project_id),
newResult = [];
for (let i = 0, item, list; i < result.length; i++) {
item = result[i].toObject();
list = await this.Model.listByCatid(item._id);
for (let j = 0; j < list.length; j++) {
list[j] = list[j].toObject();
}
item.list = list;
newResult[i] = item;
}
ctx.body = yapi.commons.resReturn(newResult);
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 402, err.message);
}
}
/**
* 编辑接口
* @interface /interface/up
* @method POST
* @category interface
* @foldnumber 10
* @param {Number} id 接口id,不能为空
* @param {String} [path] 接口请求路径
* @param {String} [method] 请求方式
* @param {Array} [req_headers] 请求的header信息
* @param {String} [req_headers[].name] 请求的header信息名
* @param {String} [req_headers[].value] 请求的header信息值
* @param {Boolean} [req_headers[].required] 是否是必须,默认为否
* @param {String} [req_headers[].desc] header描述
* @param {String} [req_body_type] 请求参数方式,有["form", "json", "text", "xml"]四种
* @param {Mixed} [req_body_form] 请求参数,如果请求方式是form,参数是Array数组,其他格式请求参数是字符串
* @param {String} [req_body_form[].name] 请求参数名
* @param {String} [req_body_form[].value] 请求参数值,可填写生成规则(mock)。如@email,随机生成一条email
* @param {String} [req_body_form[].type] 请求参数类型,有["text", "file"]两种
* @param {String} [req_body_other] 非form类型的请求参数可保存到此字段
* @param {String} [res_body_type] 相应信息的数据格式,有["json", "text", "xml"]三种
* @param {String} [res_body] 响应信息,可填写任意字符串,如果res_body_type是json,则会调用mock功能
* @param {String} [desc] 接口描述
* @returns {Object}
* @example ./api/interface/up.json
*/
async up(ctx) {
let params = ctx.params;
if (!_.isUndefined(params.method)) {
params.method = params.method || 'GET';
params.method = params.method.toUpperCase();
}
let id = params.id;
params.message = params.message || '';
params.message = params.message.replace(/\n/g, '
');
// params.res_body_is_json_schema = _.isUndefined (params.res_body_is_json_schema) ? true : params.res_body_is_json_schema;
// params.req_body_is_json_schema = _.isUndefined(params.req_body_is_json_schema) ? true : params.req_body_is_json_schema;
handleHeaders(params)
let interfaceData = await this.Model.get(id);
if (!interfaceData) {
return (ctx.body = yapi.commons.resReturn(null, 400, '不存在的接口'));
}
if (!this.$tokenAuth) {
let auth = await this.checkAuth(interfaceData.project_id, 'project', 'edit');
if (!auth) {
return (ctx.body = yapi.commons.resReturn(null, 400, '没有权限'));
}
}
let data = Object.assign(
{
up_time: yapi.commons.time()
},
params
);
if (params.path) {
let http_path;
http_path = url.parse(params.path, true);
if (!yapi.commons.verifyPath(http_path.pathname)) {
return (ctx.body = yapi.commons.resReturn(
null,
400,
'path第一位必需为 /, 只允许由 字母数字-/_:.! 组成'
));
}
params.query_path = {};
params.query_path.path = http_path.pathname;
params.query_path.params = [];
Object.keys(http_path.query).forEach(item => {
params.query_path.params.push({
name: item,
value: http_path.query[item]
});
});
data.query_path = params.query_path;
}
if (
params.path &&
(params.path !== interfaceData.path || params.method !== interfaceData.method)
) {
let checkRepeat = await this.Model.checkRepeat(
interfaceData.project_id,
params.path,
params.method
);
if (checkRepeat > 0) {
return (ctx.body = yapi.commons.resReturn(
null,
401,
'已存在的接口:' + params.path + '[' + params.method + ']'
));
}
}
if (!_.isUndefined(data.req_params)) {
if (Array.isArray(data.req_params) && data.req_params.length > 0) {
data.type = 'var';
} else {
data.type = 'static';
data.req_params = [];
}
}
let result = await this.Model.up(id, data);
let username = this.getUsername();
let CurrentInterfaceData = await this.Model.get(id);
let logData = {
interface_id: id,
cat_id: data.catid,
current: CurrentInterfaceData.toObject(),
old: interfaceData.toObject()
};
this.catModel.get(interfaceData.catid).then(cate => {
let diffView2 = showDiffMsg(jsondiffpatch, formattersHtml, logData);
if (diffView2.length <= 0) {
return; // 没有变化时,不写日志
}
yapi.commons.saveLog({
content: `${username}
更新了分类 ${cate.name}
下的接口 ${
interfaceData.title
}
${params.message}
`, type: 'project', uid: this.getUid(), username: username, typeid: cate.project_id, data: logData }); }); this.projectModel.up(interfaceData.project_id, { up_time: new Date().getTime() }).then(); if (params.switch_notice === true) { let diffView = showDiffMsg(jsondiffpatch, formattersHtml, logData); let annotatedCss = fs.readFileSync( path.resolve( yapi.WEBROOT, 'node_modules/jsondiffpatch/dist/formatters-styles/annotated.css' ), 'utf8' ); let htmlCss = fs.readFileSync( path.resolve(yapi.WEBROOT, 'node_modules/jsondiffpatch/dist/formatters-styles/html.css'), 'utf8' ); let project = await this.projectModel.getBaseInfo(interfaceData.project_id); let interfaceUrl = `${ctx.request.origin}/project/${ interfaceData.project_id }/interface/api/${id}`; yapi.commons.sendNotice(interfaceData.project_id, { title: `${username} 更新了接口`, content: `项目名:${project.name}
修改用户: ${username}
接口名: ${data.title}
接口路径: [${data.method}]${data.path}
详细改动日志: ${this.diffHTML(diffView)}