From 2864ba9ec44c53d41a3e7012f26b7a0805cfdbba Mon Sep 17 00:00:00 2001 From: Delong Zhu Date: Tue, 31 Oct 2017 23:09:32 +0800 Subject: [PATCH 01/42] fix auth/login --- .gitignore | 4 +- readme.md | 6 - server/Dockerfile | 1 + server/package.json | 12 +- server/report/59c0beae1f4b3f28879e3433.css | 49 +++ server/report/59c0beae1f4b3f28879e3433.html | 12 + server/src/agent/AgentCtrl.ts | 29 ++ server/src/agent/router.ts | 9 + server/src/app.ts | 40 +- server/src/auth/AuthCtrl.ts | 74 ++++ server/src/auth/router.ts | 10 + server/src/interface/InterfaceCtrl.ts | 222 ++++++++++ server/src/interface/interface.test.ts | 78 ++++ server/src/interface/model.ts | 176 ++++++++ server/src/interface/router.ts | 17 + server/src/member/MemberCtrl.ts | 96 +++++ server/src/{team => member}/member.test.ts | 10 +- .../{team/member.md.ts => member/model.ts} | 19 +- server/src/member/router.ts | 15 + server/src/message/MessageCtrl.ts | 46 ++ server/src/message/model.ts | 46 ++ server/src/message/router.ts | 10 + server/src/mock.ts | 29 -- server/src/mock/Mock2json.ts | 52 +++ server/src/mock/mock.ts | 96 +++++ server/src/mock/probability.ts | 33 ++ server/src/project/ProjectCtrl.ts | 218 ++++++++++ server/src/project/export.ts | 43 +- server/src/project/interface.ctrl.ts | 142 ------- server/src/project/interface.md.ts | 215 ---------- server/src/project/interface.test.ts | 78 ---- server/src/project/interfaceArchive.md.ts | 22 - .../src/project/{project.md.ts => model.ts} | 60 ++- server/src/project/project.ctrl.ts | 130 ------ server/src/project/project.test.ts | 30 +- server/src/project/router.ts | 16 + server/src/router.ts | 46 -- server/src/shim.d.ts | 13 +- server/src/system/SystemCtrl.ts | 96 +++++ server/src/system/model.ts | 52 +++ server/src/system/router.ts | 13 + server/src/system/system.md.ts | 42 -- server/src/team/group.ctrl.ts | 67 --- server/src/team/group.md.ts | 32 -- server/src/team/group.test.ts | 76 ---- server/src/team/member.ctrl.ts | 55 --- server/src/test/model.ts | 55 +++ server/src/thirdparty/BaseExternalMessage.ts | 15 + server/src/thirdparty/Dingding.ts | 23 + server/src/thirdparty/ThirdPartyEngine.ts | 56 +++ server/src/thirdparty/Tower.ts | 21 + server/src/util/BaseCtrl.ts | 140 +++++++ server/src/util/Cache.ts | 46 ++ server/src/util/Router.ts | 76 ++++ server/src/util/config.ts | 50 ++- server/src/util/crypto.ts | 25 +- server/template/default.body.html | 26 ++ server/template/template.html | 79 ++++ web/1.log | 393 ------------------ web/src/components/navBar.vue | 25 +- web/src/components/user/login.vue | 15 +- web/src/service/http.ts | 76 ++-- web/src/service/router.ts | 1 + 63 files changed, 2274 insertions(+), 1485 deletions(-) create mode 100644 server/report/59c0beae1f4b3f28879e3433.css create mode 100644 server/report/59c0beae1f4b3f28879e3433.html create mode 100644 server/src/agent/AgentCtrl.ts create mode 100644 server/src/agent/router.ts create mode 100644 server/src/auth/AuthCtrl.ts create mode 100644 server/src/auth/router.ts create mode 100644 server/src/interface/InterfaceCtrl.ts create mode 100644 server/src/interface/interface.test.ts create mode 100644 server/src/interface/model.ts create mode 100644 server/src/interface/router.ts create mode 100644 server/src/member/MemberCtrl.ts rename server/src/{team => member}/member.test.ts (63%) rename server/src/{team/member.md.ts => member/model.ts} (83%) create mode 100644 server/src/member/router.ts create mode 100644 server/src/message/MessageCtrl.ts create mode 100644 server/src/message/model.ts create mode 100644 server/src/message/router.ts delete mode 100644 server/src/mock.ts create mode 100644 server/src/mock/Mock2json.ts create mode 100644 server/src/mock/mock.ts create mode 100644 server/src/mock/probability.ts create mode 100644 server/src/project/ProjectCtrl.ts delete mode 100644 server/src/project/interface.ctrl.ts delete mode 100644 server/src/project/interface.md.ts delete mode 100644 server/src/project/interface.test.ts delete mode 100644 server/src/project/interfaceArchive.md.ts rename server/src/project/{project.md.ts => model.ts} (58%) delete mode 100644 server/src/project/project.ctrl.ts create mode 100644 server/src/project/router.ts delete mode 100644 server/src/router.ts create mode 100644 server/src/system/SystemCtrl.ts create mode 100644 server/src/system/model.ts create mode 100644 server/src/system/router.ts delete mode 100644 server/src/system/system.md.ts delete mode 100644 server/src/team/group.ctrl.ts delete mode 100644 server/src/team/group.md.ts delete mode 100644 server/src/team/group.test.ts delete mode 100644 server/src/team/member.ctrl.ts create mode 100644 server/src/test/model.ts create mode 100644 server/src/thirdparty/BaseExternalMessage.ts create mode 100644 server/src/thirdparty/Dingding.ts create mode 100644 server/src/thirdparty/ThirdPartyEngine.ts create mode 100644 server/src/thirdparty/Tower.ts create mode 100644 server/src/util/BaseCtrl.ts create mode 100644 server/src/util/Cache.ts create mode 100644 server/src/util/Router.ts create mode 100644 server/template/template.html delete mode 100644 web/1.log diff --git a/.gitignore b/.gitignore index 51e69e9..37c90b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -package-lock.json npm-debug.log node_modules/ dist/ .nyc_output/ -coverage/ \ No newline at end of file +coverage/ +server/static/ \ No newline at end of file diff --git a/readme.md b/readme.md index e639d0e..2af86c6 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,3 @@ # 功能点 -* API模板。可以通过模板来创建新的API * 导出模板。可以通过html模板导出成想要文档 * 开放API。支持调用接口访问API信息 -* API参数增加枚举类型。 -* API版本信息可编辑 - -# -### element-ui官方未提供 d.ts模块声明文件,若在src里添加vue-shimes.d.ts,会造成karma测试不通过,暂时手动在node_module/element-ui/下,添加index.d.ts文件 diff --git a/server/Dockerfile b/server/Dockerfile index 12fd7c9..68873cb 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -7,5 +7,6 @@ VOLUME ["/app"] EXPOSE 2018 COPY ./Hongkong /etc/localtime +RUN apk update && apk add git RUN npm set registry https://registry.npm.taobao.org RUN npm i -g pm2 nodemon typescript \ No newline at end of file diff --git a/server/package.json b/server/package.json index c36bdb9..16e687b 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "main": "app.js", "scripts": { "compose": "npm run proxy && npm run init && sh", - "dev": "tsc -w & nodemon --inspect=0.0.0.0:5858 --delay 100ms -w ./dist dist/app.js", + "dev": "tsc -w & nodemon --inspect=0.0.0.0:5858 --delay 100ms -w ./dist ./dist/app.js", "init": "npm i", "proxy": "npm set registry https://registry.npm.taobao.org", "start": "pm2 start ./dist/app.js", @@ -15,15 +15,19 @@ "author": "yalishizhude", "license": "ISC", "dependencies": { + "axios": "^0.16.2", "babel-eslint": "^7.1.1", + "jsonwebtoken": "^8.0.1", "koa": "^2.3.0", + "koa-better-body": "^3.0.4", "koa-better-router": "^2.1.1", - "koa-bodyparser": "^2.5.0", "koa-router": "^7.2.1", - "lodash": "^4.17.4", + "mockjs": "^1.0.1-beta3", "mongoose": "^4.11.0", + "path-to-regexp": "^2.0.0", "request": "^2.81.0", - "rxjs": "^5.4.3" + "rxjs": "^5.4.3", + "underscore": "^1.8.3" }, "devDependencies": { "ava": "^0.22.0", diff --git a/server/report/59c0beae1f4b3f28879e3433.css b/server/report/59c0beae1f4b3f28879e3433.css new file mode 100644 index 0000000..ccf01cd --- /dev/null +++ b/server/report/59c0beae1f4b3f28879e3433.css @@ -0,0 +1,49 @@ +h2 { + margin-bottom: 10px; +} + +hr { + margin: 0 0 20px 0; +} + +pre { + background-color: #f8f8f8; + border: 1px solid #ccc; + font-size: 13px; + line-height: 19px; + overflow: auto; + padding: 6px 10px; + border-radius: 3px; + display: inline-block; + margin: 0; +} + +table { + border-collapse: collapse; + border: none; +} + +table tr { + border-top: 1px solid #ccc; + background-color: #fff; + margin: 0; + padding: 0; +} + +table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +table td { + border: 1px solid #ccc; + text-align: left; + margin: 0; + padding: 6px 13px; +} + +.body { + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); + background-color: white; + border: 1px solid #E2E2E2; + padding: 10px; +} \ No newline at end of file diff --git a/server/report/59c0beae1f4b3f28879e3433.html b/server/report/59c0beae1f4b3f28879e3433.html new file mode 100644 index 0000000..4875041 --- /dev/null +++ b/server/report/59c0beae1f4b3f28879e3433.html @@ -0,0 +1,12 @@ + + + + + + +API文档 + + + + + \ No newline at end of file diff --git a/server/src/agent/AgentCtrl.ts b/server/src/agent/AgentCtrl.ts new file mode 100644 index 0000000..f01c218 --- /dev/null +++ b/server/src/agent/AgentCtrl.ts @@ -0,0 +1,29 @@ +/** + * 无页面测试服务端API + */ +import BaseCtrl from '../util/BaseCtrl' +import * as axios from 'axios' + +export default class AgentCtrl extends BaseCtrl{ + forward(url: string, method: string='GET', data: any={}, headers: any={}) { + let request:any = { + data, + method, + headers + } + if(/^http/.test(url)) { + request.url = url + } else { + request.url = 'http://'+url + } + return this.from(axios(request)) + .map((result:any) => { + return { + data: result.data, + status: result.status, + statusText: result.statusText, + headers: result.headers + } + }) + } +} \ No newline at end of file diff --git a/server/src/agent/router.ts b/server/src/agent/router.ts new file mode 100644 index 0000000..f842998 --- /dev/null +++ b/server/src/agent/router.ts @@ -0,0 +1,9 @@ +import Router from '../util/Router' +import Ctrl from './AgentCtrl' + +let agentRouter = new Router() +let agentCtrl = new Ctrl() + +export default agentRouter.router + .post('/agent', (ctx: any) => agentRouter.handleProxy(ctx, agentCtrl.forward(ctx.request.fields.url, ctx.request.fields.method, ctx.request.fields.data, ctx.request.fields.headers))) +.routes() \ No newline at end of file diff --git a/server/src/app.ts b/server/src/app.ts index f19dd23..9cc95d9 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,18 +1,42 @@ +import * as fs from 'fs' import * as Koa from 'koa' -import * as bodyParser from 'koa-bodyparser' +import * as body from 'koa-better-body' +import { uploadPath as uploadDir } from './util/config' -import router from './router' +import projectRouter from './project/router' +import interfaceRouter from './interface/router' +import memberRouter from './member/router' +import authRouter from './auth/router' +import systemRouter from './system/router' +import messageRouter from './message/router' +import agentRouter from './agent/router' +import mock from './mock/mock' +import authCtrl from './auth/AuthCtrl' +import SystemCtrl from './system/SystemCtrl' +import engine from './thirdparty/ThirdPartyEngine' +// 初始化系统配置 +let systemCtrl = new SystemCtrl() +systemCtrl.initSystem() +systemCtrl.initMember() +// 启动第三方消息引擎 +new engine() const app = new Koa() -app.use(bodyParser()) -app.use(router) - -app.use((ctx:any) => { +app.use(body({ uploadDir })) +app.use(authCtrl.authorize) +app.use(authRouter) +app.use(projectRouter) +app.use(interfaceRouter) +app.use(memberRouter) +app.use(systemRouter) +app.use(messageRouter) +app.use(agentRouter) +app.use(mock) +app.use((ctx: any) => { ctx.body = 'api server' }) app.listen(2018) -console.log('Listening on 2018...') +console.log('API server listening on 2018...') -export default app diff --git a/server/src/auth/AuthCtrl.ts b/server/src/auth/AuthCtrl.ts new file mode 100644 index 0000000..7d954de --- /dev/null +++ b/server/src/auth/AuthCtrl.ts @@ -0,0 +1,74 @@ +import { MemberModel } from '../member/model' +import { Observable } from 'rxjs/Rx' +import { mongoose } from '../util/db' +import BaseCtrl from '../util/BaseCtrl' +import { encrypt, hash } from '../util/crypto' +import { key } from '../util/config' +import * as jwt from 'jsonwebtoken' + +export default class AuthCtrl extends BaseCtrl{ + login(account: string, password: string) { + let loginTime = new Date().getTime() + return Observable.fromPromise(MemberModel.findOneAndUpdate({ + account, + password: encrypt(password) + }, { $set: { loginTime } }).select('_id account name avatarUrl password isAdmin').exec()) + .map((user: any) => { + if (user) { + let token = jwt.sign(Object.assign(user.toObject(), { loginTime }), key) + return { + token, + user: { + id: user._id, + account: user.account, + name: user.name, + isAdmin: user.isAdmin, + avatar: user.avatarUrl + } + } + } else { + return { + errCode: 401, + errMsg: '用户名或密码错误' + } + } + }) + } + logout(id: string) { + return Observable.fromPromise(MemberModel.findOneAndUpdate({ _id: mongoose.Types.ObjectId(id) }, { $set: { loginTime: '' } }).exec()) + .map(() => ({})) + } + static authorize(ctx: any, next: any) { + const whiteRoute = ['/api/user/login', '/api/user/logout', '/api/setting/upload/img', '/api/agent'] + if (whiteRoute.includes(ctx.path)||/^\/\w{24}/.test(ctx.path)) { + ctx.user = {} + return next() + } else if (/(\.html|\.png|\.jpg|\.css|\.svg)$/.test(ctx.path)) { + return next() + } else { + if (ctx.headers.authorization) { + let user = jwt.decode(ctx.headers.authorization) || { _id: new mongoose.Types.ObjectId() } + return MemberModel.findOne({ + _id: mongoose.Types.ObjectId(user._id || ''), + account: user.account, + isAdmin: user.isAdmin, + password: user.password, + loginTime: user.loginTime + }).then((user: any) => { + if (!user) { + ctx.status = 401 + ctx.body = '请登录' + } else { + ctx.user = user + return next() + } + }, (e: any) => { + ctx.body = e + }) + } else { + ctx.status = 401 + ctx.body = '请登录' + } + } + } +} \ No newline at end of file diff --git a/server/src/auth/router.ts b/server/src/auth/router.ts new file mode 100644 index 0000000..c47bc83 --- /dev/null +++ b/server/src/auth/router.ts @@ -0,0 +1,10 @@ +import Router from '../util/Router' +import Ctrl from './AuthCtrl' + +let authRouter = new Router() +let authCtrl = new Ctrl() + +export default authRouter.router + .post('/user/login', (ctx: any) => authRouter.handle(ctx, authCtrl.login(ctx.request.fields.account, ctx.request.fields.password))) + .get('/user/logout', (ctx: any) => authRouter.handle(ctx, authCtrl.logout(ctx.user?ctx.user._id:''))) + .routes() \ No newline at end of file diff --git a/server/src/interface/InterfaceCtrl.ts b/server/src/interface/InterfaceCtrl.ts new file mode 100644 index 0000000..bab4303 --- /dev/null +++ b/server/src/interface/InterfaceCtrl.ts @@ -0,0 +1,222 @@ +import { InterfaceModel, InterfaceHistoryModel } from './model' +import { ProjectModel, role } from '../project/model' +import { MemberInterface, MemberModel } from '../member/model' +import { Observable } from 'rxjs/Rx' +import BaseCtrl from '../util/BaseCtrl' +import { mongoose } from '../util/db' +import ProjectCtrl from '../project/ProjectCtrl' +import engine from '../thirdparty/ThirdPartyEngine' + +let projectCtrl = new ProjectCtrl() + +export default class InterfaceCtrl extends BaseCtrl { + module = '接口' + /** + * 查询项目下的接口列表 + * @param pid 项目id + * @param uid 用户id + * @param isAdmin 管理员权限 + */ + get(pid: string, uid: string, isAdmin: boolean) { + return this.verifyAuth(isAdmin, pid, uid) + .switchMap((authorized: boolean) => { + return Observable.fromPromise(ProjectModel.aggregate() + .match({ _id: mongoose.Types.ObjectId(pid) }) + .lookup({ + from: InterfaceModel.collection.collectionName, + localField: '_id', + foreignField: 'pid', + as: 'list' + }) + .project({ + _id: 0, + id: '$_id', + name: 1, + total: { $size: '$list' }, + apiList: { + $map: { + input: '$list', + as: 'i', + in: { + id: '$$i._id', + name: '$$i.name', + module: '$$i.module', + version: '$$i.version', + method: '$$i.method', + url: '$$i.url', + isTest: '$$i.isTest', + testStatusId: '$$i.testStatusId', + testStatusMsg: '$$i.testStatusMsg' + } + } + } + }) + .exec()) + .map((list: any) => list.pop() || {}) + }) + } + /** + * 查询某个接口 + * @param pid 项目id + * @param iid 接口id + * @param uid 用户id + * @param isAdmin 管理员权限 + */ + getById(pid: string, iid: string, uid: string, isAdmin: boolean) { + return this.verifyAuth(isAdmin, pid, uid) + .switchMap((authorized: boolean) => { + return Observable.fromPromise(InterfaceModel.aggregate() + .match({ _id: mongoose.Types.ObjectId(iid) }) + .append({ + $addFields: { + id: '$_id', + 'request.dataList': '$request.paramList', + 'response.dataList': '$response.paramList', + 'request.paramList': '$request.urlParams' + } + }) + .project({ _id: 0 }) + .exec()) + .map((x: any) => x.pop()) + }) + } + /** + * 查询指定版本接口信息 + * @param iid 接口id + * @param version 接口版本 + * @param uid 用户id + * @param isAdmin 管理员权限 + */ + getHistoryById(pid: string, iid: string, version: string, uid: string, isAdmin: boolean) { + return this.verifyAuth(isAdmin, pid, uid) + .switchMap((authorized: boolean) => { + return Observable.fromPromise(InterfaceHistoryModel.aggregate() + .match({ iid: mongoose.Types.ObjectId(iid), version }) + .append({ + $addFields: { + id: '$_id', + 'request.dataList': '$request.paramList', + 'response.dataList': '$response.paramList', + 'request.paramList': '$request.urlParams' + } + }) + .project({ _id: 0 }) + .exec()) + .map((res: any) => res.pop()) + }) + } + /** + * 查询接口历史版本列表 + * @param iid 接口id + * @param uid 用户id + * @param isAdmin 管理员权限 + */ + getVersionById(pid: string, iid: string, uid: string, isAdmin: boolean) { + return this.verifyAuth(isAdmin, pid, uid) + .switchMap((authorized: boolean) => { + return Observable.fromPromise(InterfaceHistoryModel.aggregate() + .match({ iid: mongoose.Types.ObjectId(iid) }) + .project({ + version: 1, + updateTime: { $dateToString: { format: '%Y-%m-%d %H:%M:%S', date: '$updateTime' } }, + updateMember: '$editor' + }) + .exec()) + .map((versionList: any) => ({ versionList })) + }) + } + /** + * 获取接口模块 + * @param pid 项目id + * @param uid 用户id + * @param isAdmin 管理员权限 + */ + getModule(pid: string, uid: string, isAdmin: boolean) { + return this.verifyAuth(isAdmin, pid, uid) + .switchMap(() => { + return Observable.fromPromise(InterfaceModel.find({ pid: mongoose.Types.ObjectId(pid) }) + .distinct('module') + .exec()) + .map((moduleList) => { + return { moduleList } + }) + }) + } + /** + * 新增接口 + * @param pid 项目id + * @param ifce接口数据 + */ + post(pid: string, ifc: any, uid: string, isAdmin: boolean, uname: string) { + return this.verifyAuth(isAdmin, pid, uid, [role.developer, role.master]) + .switchMap(() => { + ifc.pid = pid + ifc.request.urlParams = ifc.request.paramList + ifc.request.paramList = ifc.request.dataList + ifc.response.paramList = ifc.response.dataList + return Observable.fromPromise(InterfaceModel.create(ifc)) + .do((ifc: any) => { + projectCtrl.getMemberList(pid) + .subscribe((memberList:any) => { + engine.notify(engine.c, ifc) + this.newCreateMessage(ifc.id, ifc.name, uname, memberList, ifc.url + ' ' + ifc.method) + }) + }) + .map((doc: any) => ({ id: doc.id })) + }) + } + /** + * 修改接口 + * @param pid 项目id + * @param iid 接口id + * @param ifc 接口数据 + * @param uid 用户id + * @param isAdmin 管理员权限 + */ + put(pid: string, iid: string, ifc: any, uid: string, isAdmin: boolean, uname: string) { + ifc.request.urlParams = ifc.request.paramList + ifc.request.paramList = ifc.request.dataList + ifc.response.paramList = ifc.response.dataList + return this.verifyAuth(isAdmin, pid, uid, [role.developer, role.master]) + .switchMap(() => { + return Observable.fromPromise(InterfaceModel.findOneAndUpdate({ _id: mongoose.Types.ObjectId(iid) }, { $set: ifc }).exec()) + .switchMap((doc: any) => { + if (doc) { + let log = doc.toObject() + log.iid = this.objectId(iid) + delete log._id + delete log.version + projectCtrl.getMemberList(pid) + .subscribe((memberList:any) => { + engine.notify(engine.u, ifc) + this.newUpdateMessage(iid, ifc.name, uname, memberList, ifc.url + ' ' + ifc.method) + }) + return Observable.fromPromise(InterfaceHistoryModel.create(log)) + } else { + return Observable.throw('更新接口失败') + } + }) + }) + } + /** + * 删除一个接口 + * @param pid 项目id + * @param iid 接口id + * @param uid 用户id + * @param isAdmin 管理员权限 + */ + delete(pid: string, iid: string, uid: string, isAdmin: boolean, uname: string) { + return this.verifyAuth(isAdmin, pid, uid, [role.developer, role.master]) + .switchMap(() => { + return Observable.fromPromise(InterfaceModel.findOneAndRemove({ _id: mongoose.Types.ObjectId(iid) }).exec()) + .do((doc: any) => { + projectCtrl.getMemberList(pid) + .subscribe((memberList:any) => { + engine.notify(engine.d, doc) + this.newDeleteMessage(iid, doc.name, uname, memberList, doc.url + ' ' + doc.method) + }) + InterfaceHistoryModel.remove({ iid: mongoose.Types.ObjectId(iid) }).exec() + }) + }) + } +} \ No newline at end of file diff --git a/server/src/interface/interface.test.ts b/server/src/interface/interface.test.ts new file mode 100644 index 0000000..cbf9a58 --- /dev/null +++ b/server/src/interface/interface.test.ts @@ -0,0 +1,78 @@ +import test from 'ava' +import { Observable } from 'rxjs/Rx' +import InterfaceCtrl from './Interface.ctrl' +import { Interface } from './interface.md' +import ProjectCtrl from './Project.ctrl' +import { Project } from './project.md' +import { memberCtrl } from '../team/member.ctrl' +import { Member } from '../team/member.md' + +let project = new Project() +let member = new Member() +let ifc = new Interface() + +test.before('Create project,member', (t: any) => { + return memberCtrl.post(member) + .switchMap((x: any) => { + member.id = x.id + ifc.editorId = x.id + ifc.creatorId = x.id + t.truthy(member.id) + project.memberList = [{ + id: x.id, + role: 'master' + }] + return ProjectCtrl.post(project).do((x: any) => { + project.id = x.id + t.truthy(project.id) + }) + }) +}) + +// test.after('Delete project.member', (t: any) => { +// ProjectCtrl.delete(project.id).subscribe() +// memberCtrl.delete(member.id).subscribe() +// }) + +// test.serial('interface.post', (t: any) => { +// return interfaceCtrl.post(project.id, ifc) +// .do((res: any) => { +// ifc.id = res.id +// t.truthy(res.id) +// }) +// }) + +// test.serial('interface.get', (t: any) => { +// return interfaceCtrl.get(project.id) +// .do((res: any) => { +// t.truthy(res.apiList.length > 0) +// }) +// }) + +// test.serial('interface.getVersionById', (t: any) => { +// return interfaceCtrl.getVersionById(ifc.id) +// .do((res: any) => { +// t.truthy(res.versionList) +// }) +// }) + +// test.serial('interface.put', (t:any) => { +// return interfaceCtrl.put(project.id, ifc.id, {name: 'nihao'}) +// .do((res: any) => { +// t.truthy(res) +// }) +// }) + +// test.serial('interface.getHistoryById', (t: any) => { +// return interfaceCtrl.getHistoryById(ifc.id, ifc.version) +// .do((res: any) => { +// t.deepEqual(res.name, ifc.name) +// }) +// }) + +// test.serial('interface.delete', (t: any) => { +// return interfaceCtrl.delete(project.id, ifc.id) +// .do((res: any) => { +// t.truthy(res) +// }) +// }) \ No newline at end of file diff --git a/server/src/interface/model.ts b/server/src/interface/model.ts new file mode 100644 index 0000000..fa7dc9c --- /dev/null +++ b/server/src/interface/model.ts @@ -0,0 +1,176 @@ +import { Schema, mongoose, Model } from '../util/db' + +let paramSchemaObj = { + _id: false, + headerList: { + type: [{ + _id: false, + key: String, + value: String + }], + default: [] + }, + paramList: { + type: [{ + _id: false, + id: String, + name: String, + required: Boolean, + ancestor: [String], + type: { + type: String, + enum: ['String', 'Number', 'Boolean', 'Object', 'Array'], + set(v: string) { + return `${v[0].toUpperCase()}${v.slice(1)}` + } + }, + mock: String, + remark: String + }], + default: [] + } +} + +let requestSchema = new Schema(Object.assign({ + urlParams: { + type: [{ + _id: false, + id: String, + name: String, + required: Boolean, + mock: String, + remark: String + }], + default: [] + } +}, paramSchemaObj)) + +let responseSchema = new Schema(Object.assign({ + errList: { + type: [{ + _id: false, + enabled: Boolean, + data: String, + remark: String, + probability: { + type: Number, + min: 0, + max: 100 + } + }], + default: [] + } +}, paramSchemaObj)) + +let InterfaceSchemaObj = { + iid: Schema.Types.ObjectId, + pid: { + type: Schema.Types.ObjectId, + required: true, + set: (v: string | any) => mongoose.Types.ObjectId(v) + }, + url: { + type: String, + match: /^\//, + required: true + }, + name: { + type: String, + maxlength: 40, + required: true + }, + version: { + type: String, + required: true, + default: () => new Date().getTime().toString(36) + }, + module: String, + remark: { + type: String, + maxlength: 200, + default: '' + }, + createdTime: { + type: Date, + required: true, + default: () => new Date(), + }, + updateTime: { + type: Date, + required: true, + default: () => new Date() + }, + creator: String, + editor: String, + isTest: { + type: Boolean, + default: false + }, + testStatusId: Boolean, + testStatusMsg: String, + needTest: { + type: Boolean, + default: false + }, + delay: Number, //单位:毫秒 + state: { + type: Object, + enum: [{ + _id: false, + id: 0, + name: '待测试' + }, { + _id: false, + id: -1, + name: '测试不通过' + }, { + _id: false, + id: 1, + name: '测试通过' + }] + }, + method: { + type: String, + required: true, + uppercase: true + }, + request: requestSchema, + response: responseSchema +} + +enum method { + get = 'GET', + post = 'POST', + put = 'PUT', + delete = 'DELETE' +} +enum dataType { + string = 'String', + number = 'Number', + boolean = 'Boolean', + object = 'Object', + array = 'Array' +} + +let InterfaceSchema = new Schema(InterfaceSchemaObj) +let InterfaceHistorySchema = new Schema(InterfaceSchemaObj) + +let InterfaceModel = mongoose.model('interface', InterfaceSchema) +let InterfaceHistoryModel = mongoose.model('interfacehistory', InterfaceHistorySchema) + +class Interface extends Model { + name = this.random() + url = '/' + this.random() + method = method.get + version = this.random(4) +} + +export { + InterfaceSchema, + InterfaceHistorySchema, + InterfaceModel, + InterfaceHistoryModel, + method, + dataType, + Interface +} \ No newline at end of file diff --git a/server/src/interface/router.ts b/server/src/interface/router.ts new file mode 100644 index 0000000..d60a0d0 --- /dev/null +++ b/server/src/interface/router.ts @@ -0,0 +1,17 @@ +import Router from '../util/Router' +import Ctrl from './InterfaceCtrl' + +let interfaceRouter = new Router() +let interfaceCtrl = new Ctrl() + +export default interfaceRouter.router + .get('/project/:pid/api', (ctx: any) => interfaceRouter.handle(ctx, interfaceCtrl.get(ctx.params.pid,ctx.user._id, ctx.user.isAdmin))) + .get('/project/:pid/api/module', (ctx: any) => interfaceRouter.handle(ctx, interfaceCtrl.getModule(ctx.params.pid,ctx.user._id, ctx.user.isAdmin))) + .get('/project/:pid/api/:id', (ctx: any) => interfaceRouter.handle(ctx, + ctx.query.version ? interfaceCtrl.getHistoryById(ctx.params.pid, ctx.params.id, ctx.query.version, ctx.user._id, ctx.user.isAdmin) + :interfaceCtrl.getById(ctx.params.pid, ctx.params.id,ctx.user._id, ctx.user.isAdmin))) + .get('/project/:pid/api/:id/version', (ctx: any) => interfaceRouter.handle(ctx, interfaceCtrl.getVersionById(ctx.params.pid, ctx.params.id,ctx.user._id, ctx.user.isAdmin))) + .post('/project/:pid/api', (ctx: any) => interfaceRouter.handle(ctx, interfaceCtrl.post(ctx.params.pid, ctx.request.fields,ctx.user._id, ctx.user.isAdmin, ctx.user.name))) + .put('/project/:pid/api/:id', (ctx: any) => interfaceRouter.handle(ctx, interfaceCtrl.put(ctx.params.pid, ctx.params.id, ctx.request.fields, ctx.user._id, ctx.user.isAdmin, ctx.user.name))) + .del('/project/:pid/api/:id', (ctx: any) => interfaceRouter.handle(ctx, interfaceCtrl.delete(ctx.params.pid, ctx.params.id, ctx.user._id, ctx.user.isAdmin, ctx.user.name))) +.routes() \ No newline at end of file diff --git a/server/src/member/MemberCtrl.ts b/server/src/member/MemberCtrl.ts new file mode 100644 index 0000000..b07b989 --- /dev/null +++ b/server/src/member/MemberCtrl.ts @@ -0,0 +1,96 @@ +import { MemberModel, MemberInterface } from './model' +import { Observable } from 'rxjs/Rx' +import { encrypt } from '../util/crypto' +import { mongoose } from '../util/db' +import BaseCtrl from '../util/BaseCtrl' +import {avatar, staticPath} from '../util/config' +import * as fs from 'fs' +import * as path from 'path' + +export default class MemberCtrl extends BaseCtrl { + private createAvatar(id:string):string { + let str = avatar.replace('TEXT', id.substring(0,1)).replace('COLOR', '#'+Math.random().toString(16).substring(2, 8)) + let filename = path.join(staticPath, id+'.svg') + fs.writeFileSync(filename, str) + return path.join('api', filename) + } + resetPassword(payload:any, user:any, id:string) { + if(user && user.isAdmin) { + return Observable.from(MemberModel.update({_id: mongoose.Types.ObjectId(id)}, { + $set: { + password: encrypt(payload.newPassword) + } + }).exec()) + } else { + return Observable.throw({ + errorCode: 222, + errorMsg: '没有管理员权限' + }) + } + } + get(isAdmin: boolean) { + return Observable.of(isAdmin) + .switchMap((authorized:boolean) => { + if (authorized) { + return Observable.fromPromise(MemberModel.aggregate().project({ + _id: 0, + id: "$_id", + isAdmin: 1, + account: 1, + name: 1 + })) + } else { + return Observable.throw({status: 403, message: '没有操作权限'}) + } + }) + .map((memberList: MemberInterface[]) => ({ memberList })) + } + getInfo(id: string) { + return Observable.from(MemberModel.findOne({_id: mongoose.Types.ObjectId(id)}).exec()) + .map((doc:any) => { + let u = doc._doc + return { + id: u.id, + name: u.name, + account: u.account, + avatar: u.avatar || '', + email: u.email || '', + weixin: u.weixin || '', + apiInform: u.apiInform || '', + testInform: u.testInform || '' + } + }) + } + post(member: any) { + try { + member.password = encrypt(member.password) + member.avatarUrl = this.createAvatar(member.account) + return Observable.fromPromise(new MemberModel(member).save()) + .map((x: any) => ({ id: x._id })) + } catch (e) { + return Observable.throw(e) + } + } + put(id: string, member: any) { + try { + if (member.password) { + member.password = encrypt(member.password) + } + if(member.reportStyle) { + fs.writeFileSync('template', 'template.css') + } + if(member.reportTemplate) { + fs.writeFileSync('template', 'template.body.html') + } + return Observable.fromPromise(MemberModel.update({ _id: mongoose.Types.ObjectId(id) }, {$set:member}) + .exec()) + .map((x: any) => ({ id: x._id })) + } catch (e) { + return Observable.throw(e) + } + } + delete(_id: string) { + return Observable.fromPromise(MemberModel.remove({ _id }).exec()) + .map((res: any) => ({ num: res.result.n })) + } +} \ No newline at end of file diff --git a/server/src/team/member.test.ts b/server/src/member/member.test.ts similarity index 63% rename from server/src/team/member.test.ts rename to server/src/member/member.test.ts index 3b62ce8..13cb540 100644 --- a/server/src/team/member.test.ts +++ b/server/src/member/member.test.ts @@ -11,17 +11,11 @@ test.serial('memeber.post', (t: any) => { test.serial('member.get', (t: any) => { return memberCtrl.get() .do((res: any) => { - member.id = res.list.filter((x: any) => member.account === x.account)[0].id + member.id = res.memberList.filter((x: any) => member.account === x.account)[0].id }) - .do((res: any) => t.truthy(res.list.length > 0)) + .do((res: any) => t.truthy(res.memberList.length > 0)) }) -test.serial('member.getMap', (t: any) => { - return memberCtrl.getMap() - .do((res:any) => { - t.truthy(res[member.id]) - }) -}) test.serial('member.delete', (t: any) => { return memberCtrl.delete(member.id) diff --git a/server/src/team/member.md.ts b/server/src/member/model.ts similarity index 83% rename from server/src/team/member.md.ts rename to server/src/member/model.ts index 1506732..ad603a6 100644 --- a/server/src/team/member.md.ts +++ b/server/src/member/model.ts @@ -22,24 +22,25 @@ let MemberSchema = new Schema({ }, avatarUrl: String, email: String, - openid: String, - interfaceNotify: Boolean, - testNotify: Boolean + weixin: String, + apiInform: Boolean, + testInform: Boolean, + loginTime: { + type: Number, + set(v:number) { + return Math.round(v) + } + } }) let MemberModel = mongoose.model('member', MemberSchema) -enum role { - user = 'user', - admin = 'admin' -} interface MemberInterface { id: string, account: string, name: string, password: string, - role: role, avatarUrl: string, email: string, openid: string, @@ -51,7 +52,6 @@ class Member extends Model { account: string = this.random() name: string = this.random() password: string = this.random() - role: role = Math.random() > .5 ? role.admin : role.user avatarUrl?: string email?: string openid?: string @@ -62,7 +62,6 @@ class Member extends Model { export { MemberSchema, MemberModel, - role, MemberInterface, Member } diff --git a/server/src/member/router.ts b/server/src/member/router.ts new file mode 100644 index 0000000..d88b32f --- /dev/null +++ b/server/src/member/router.ts @@ -0,0 +1,15 @@ +import Router from '../util/Router' +import Ctrl from './MemberCtrl' + +let memberRouter = new Router() +let memberCtrl = new Ctrl() + +export default memberRouter.router + .get('/member', (ctx: any) => memberRouter.handle(ctx, memberCtrl.get(ctx.user.isAdmin))) + .get('/user', (ctx: any) => memberRouter.handle(ctx, memberCtrl.getInfo(ctx.user._id))) + .put('/user', (ctx: any) => memberRouter.handle(ctx, memberCtrl.put(ctx.user._id, ctx.request.fields))) + .post('/member', (ctx: any) => memberRouter.handle(ctx, memberCtrl.post(ctx.request.fields))) + .put('/member/:id', (ctx: any) => memberRouter.handle(ctx, memberCtrl.put(ctx.params.id, ctx.request.fields))) + .put('/member/:id/reset', (ctx: any) => memberRouter.handle(ctx, memberCtrl.resetPassword(ctx.request.fields, ctx.user, ctx.params.id))) + .del('/member/:id', (ctx: any) => memberRouter.handle(ctx, memberCtrl.delete(ctx.params.id))) + .routes() \ No newline at end of file diff --git a/server/src/message/MessageCtrl.ts b/server/src/message/MessageCtrl.ts new file mode 100644 index 0000000..0a5d88a --- /dev/null +++ b/server/src/message/MessageCtrl.ts @@ -0,0 +1,46 @@ +import BaseCtrl from '../util/BaseCtrl' +import { MessageModel } from './model' + +export default class MessageCtrl extends BaseCtrl { + protected model = MessageModel + /** + * 通过用户id获取消息 + * @param uid 用户id + */ + get(uid: string) { + return this.aggregate([{ + $match: { + readableUserList: { + $in: [uid] + } + } + }, { + $addFields: { + id: '$_id', + isRead: { + $in: [uid, '$readUserList'] + }, + datetime: { + $dateToString: { + format: '%Y-%m-%d %H:%M:%S', + date: '$createdTime' + } + } + } + }, { + $sort: { createdTime: -1 } + }]) + .map((list: any) => { + return { list } + }) + } + put(uid: string, msgId: string[]) { + return this.update({ + _id: { $in: this.objectId(msgId) } + }, { + $addToSet: { + readUserList: uid + } + }) + } +} \ No newline at end of file diff --git a/server/src/message/model.ts b/server/src/message/model.ts new file mode 100644 index 0000000..95bf194 --- /dev/null +++ b/server/src/message/model.ts @@ -0,0 +1,46 @@ +import { Schema, mongoose } from '../util/db' + +let MessageSchema = new Schema({ + // 被操作对象的id + objectId: Schema.Types.ObjectId, + // 被操作的名称 + objectName: String, + // 被操作的类型 + module: String, + // 操作类型 + operation: { + type: String, + enum: ['create', 'update', 'delete', 'test'] + }, + // 操作者 + operator: { + type: String, + default: '' + }, + // 操作内容描述 + content: { + type: String, + default: '' + }, + //可读用户列表 + readableUserList: { + type: [Schema.Types.ObjectId], + default: [] + }, + //已读用户列表 + readUserList: { + type: [Schema.Types.ObjectId], + default: [] + }, + createdTime: { + type: Date, + default: new Date() + } +}) + +let MessageModel = mongoose.model('message', MessageSchema) + +export { + MessageSchema, + MessageModel +} \ No newline at end of file diff --git a/server/src/message/router.ts b/server/src/message/router.ts new file mode 100644 index 0000000..409dae4 --- /dev/null +++ b/server/src/message/router.ts @@ -0,0 +1,10 @@ +import Router from '../util/Router' +import Ctrl from './MessageCtrl' + +let router = new Router() +let ctrl= new Ctrl() + +export default router.router + .get('/news', (ctx: any) => router.handle(ctx, ctrl.get(ctx.user._id))) + .put('/news/:id', (ctx: any) => router.handle(ctx, ctrl.put(ctx.user._id, [ctx.params.id]))) +.routes() \ No newline at end of file diff --git a/server/src/mock.ts b/server/src/mock.ts deleted file mode 100644 index b228350..0000000 --- a/server/src/mock.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as path2reg from 'path-to-regexp' -import { InterfaceModel, Interface } from './project/interface.md' -import { Observable } from 'rxjs/Rx' -import { mongoose } from './util/db' - -export default (ctx: any, next: any) => { - let { method, path, query, headers, body } = ctx.request - let _path = path.replace('/', '').split('/') - let pid = _path.shift() - path = '/' + _path.join('/') - if(/\w{24}/.test(pid)) { - return Observable.fromPromise(InterfaceModel.find({ pid: mongoose.Types.ObjectId(pid), method })) - .switchMap((res: Interface[]) => Observable.from(res)) - .combineLatest(Observable.of({ path, query, body, headers })) - .throttleTime(500) - .map(([ifc, { path, query, body, headers }]) => { - let re = path2reg(ifc.url) - let match = re.exec(path) - if (match) { - ctx.body = match - } else { - next() - } - }) - .toPromise() - } else { - return next() - } -} \ No newline at end of file diff --git a/server/src/mock/Mock2json.ts b/server/src/mock/Mock2json.ts new file mode 100644 index 0000000..c7471b5 --- /dev/null +++ b/server/src/mock/Mock2json.ts @@ -0,0 +1,52 @@ +import * as Mock from 'mockjs' + +export default class Mock2json { + static makeJsonSchema(list: any[], rootId: string = 'root') { + return Mock.toJSONSchema(this.makeMockJson(list, rootId)) + } + + static makeJson(list: any[], rootId: string = 'root') { + return Mock.mock(this.makeMockJson(list, rootId)) + } + + static makeMockJson(list: any, pid: any, isArray?: boolean) { + if (isArray) { + let arr: any[] = [] + list.forEach((p: any) => { + if (p.ancestor[p.ancestor.length - 1] === pid) { + let t: any = p.type + if (t === 'Number' || t === 'String' || t === 'Boolean') { + arr.push(p.mock) + } else if (t === 'Object') { + arr.push(this.makeMockJson(list, p.id)) + } else if (t === 'Array') { + arr.push(this.makeMockJson(list, p.id, true)) + } + } + }) + return arr + } else { + let obj: any = {} + list.forEach((p: any) => { + if (p.ancestor[p.ancestor.length - 1] === pid && p.name) { + let t = p.type + if (t === 'Number' || t === 'String' || t === 'Boolean') { + let key: any = p.name + ((p.mock.split('|')[1] ? ('|' + p.mock.split('|')[1]) : '')) + obj[key] = p.mock.split('|')[0] + } else if (t === 'Object') { + let key: any = p.name + (p.mock ? '|' + p.mock : '') + obj[key] = this.makeMockJson(list, p.id) + } else if (t === 'Array') { + let key: any = p.name + (p.mock ? '|' + p.mock : '') + obj[key] = this.makeMockJson(list, p.id, true) + } + } + }) + return obj + } + } + + static valid(template:string, obj:any):any { + return Mock.valid(template, obj) + } +} \ No newline at end of file diff --git a/server/src/mock/mock.ts b/server/src/mock/mock.ts new file mode 100644 index 0000000..50135f3 --- /dev/null +++ b/server/src/mock/mock.ts @@ -0,0 +1,96 @@ +import * as path2reg from 'path-to-regexp' +import { InterfaceModel, Interface } from '../interface/model' +import { Observable } from 'rxjs/Rx' +import { mongoose } from '../util/db' +import Probability from './probability' +import Mock2json from './Mock2json' + +export default (ctx: any, next: any) => { + let { method, path, query, headers, body } = ctx.request + let _path = path.replace('/', '').split('/') + let pid = _path.shift() + path = '/' + _path.join('/') + if (/\w{24}/.test(pid)) { + return Observable.fromPromise(InterfaceModel.find({ pid: mongoose.Types.ObjectId(pid), method })) + .switchMap((res: Interface[]) => Observable.from(res)) + .combineLatest(Observable.of({ path, query, body, headers })) + .throttleTime(500) + .map(([ifc, { path, query, body, headers }]) => { + var keys: any = [] + let re = path2reg(ifc.url, keys, { strict: true }) + let match = re.exec(path) + if (match) { + // 地址栏参数对象 + let query = ctx.query + // 请求体对象 + let payload = ctx.request.fields + // 占位符参数 + let param: any = {} + keys.forEach((key: any, index: number) => { + param[key.name] = match[index + 1] + }) + let paramValid: any = [] + // 校验占位符和地址栏参数 + if(ifc.request.urlParams) { + ifc.request.urlParams.forEach((it: any) => { + if (it.required) { + if (!param[it.name] && !query[it.name]) { + paramValid.push(`缺少URL参数'${it.name}'`) + } + } + }) + } + // 校验请求体 + let bodyTemplate = Mock2json.makeMockJson(ifc.request.paramList || [], 'root') + let bodyValid = [] + let errorMsg: any = [] + bodyValid = Mock2json.valid(bodyTemplate, ctx.request.fields || {}) + errorMsg = paramValid.concat(bodyValid) + if (errorMsg.length) { + ctx.body = { + errorCode: 400, + errorMsg + } + } else { + return new Promise((resolve, rejct) => { + setTimeout(() => { + if(ifc.response.headerList) { + ifc.response.headerList.forEach((it: any) => { + ctx.set(it.key, it.value) + }) + } + if (ifc.response.errList && ifc.response.errList.length) { + let list:any = [] + ifc.response.errList.forEach((item: any) => { + if (item.enabled) { + let response + try { + response = eval(item.data) + } catch (e) { + console.error(e) + response = item.data + } + list.push({ + p: item.probability + '%', + fn: () => ctx.body = item.response + }) + } + }) + let p = new Probability(list) + p.roll() + } else { + ctx.body = Mock2json.makeJson(ifc.response.paramList, 'root') + } + resolve() + }, ifc.delay) + }) + } + } else { + next() + } + }) + .toPromise() + } else { + return next() + } +} \ No newline at end of file diff --git a/server/src/mock/probability.ts b/server/src/mock/probability.ts new file mode 100644 index 0000000..8d8e698 --- /dev/null +++ b/server/src/mock/probability.ts @@ -0,0 +1,33 @@ +export default class Probability { + private probas: number[] = [] + private functions: any = [] + constructor(args: any[]) { + let sum = 0 + args.concat([{ p: 0, f: () => { } }]).forEach((item: any, i) => { + let p = Math.abs(parseFloat(item.p)), + f = item.f + if (isNaN(p) || typeof f !== 'function') { + throw new TypeError('Probability.js: Invalid probability object in argument ' + i + '.') + } + if (/%/.test(item.p)) { + p = p / 100.0 + } + sum += p + if (sum > 1.0) { + throw new TypeError('Probability.js: Probability exceeds "1.0" (=100%) in argument ' + i + ': p="' + p + '" (=' + p * 100 + '%), sum="' + sum + '" (=' + sum * 100 + '%).') + } + this.probas[i] = sum; + this.functions[i] = f; + }) + } + roll() { + let random = Math.random(); + let fn = () => { } + for (let i = 0; i < this.probas.length; i++) { + if (random >= this.probas[i] && random < this.probas[i + 1]) { + fn = this.functions[i] + } + } + return fn.apply(this, arguments); + } +} diff --git a/server/src/project/ProjectCtrl.ts b/server/src/project/ProjectCtrl.ts new file mode 100644 index 0000000..c3b25ec --- /dev/null +++ b/server/src/project/ProjectCtrl.ts @@ -0,0 +1,218 @@ +import { ProjectModel, ProjectInterface, role } from './model' +import { TestModel } from '../test/model' +import { MemberModel } from '../member/model' +import { InterfaceModel } from '../interface/model' +import { Observable } from 'rxjs/Rx' +import { Schema, mongoose } from '../util/db' +import exp from './export' +import BaseCtrl from '../util/BaseCtrl' +import * as _ from 'underscore' + +export default class ProjectCtrl extends BaseCtrl { + module = '项目' + /** + * 项目成员可查看所属项目,管理员可查看所有项目 + */ + get(uId: string, isAdmin: boolean) { + let gg = ProjectModel.aggregate() + if (!isAdmin) { + gg.append({ + $addFields: { + memberList: { $concatArrays: ['$developerList', '$masterList', '$guestList'] } + } + }) + .match({ memberList: { $in: [mongoose.Types.ObjectId(uId)] } }) + } + return Observable.fromPromise(gg + .lookup({ + from: InterfaceModel.collection.collectionName, + localField: '_id', + foreignField: 'pid', + as: 'list' + }) + .project({ + id: '$_id', + name: 1, + api: { + total: { $size: '$list' }, + pass: { $literal: 0 }, + untest: { $size: '$list' } + } + }) + .exec()) + .map((list: any) => ({ list })) + } + getById(id: string, uId: string, isAdmin: boolean) { + return this.verifyAuth(isAdmin, id, uId) + .switchMap((authorized: boolean) => { + return Observable.fromPromise(ProjectModel.aggregate() + .match({ _id: mongoose.Types.ObjectId(id) }) + .lookup({ + from: MemberModel.collection.collectionName, + localField: 'masterList', + foreignField: '_id', + as: 'm' + }) + .lookup({ + from: MemberModel.collection.collectionName, + localField: 'developerList', + foreignField: '_id', + as: 'd' + }) + .lookup({ + from: MemberModel.collection.collectionName, + localField: 'guestList', + foreignField: '_id', + as: 'g' + }) + .append({ + $addFields: { + m: { + $map: { + input: '$m', + as: 'ml', + in: { id: '$$ml._id', name: '$$ml.name', role: 'master' } + } + }, + d: { + $map: { + input: '$d', + as: 'dl', + in: { id: '$$dl._id', name: '$$dl.name', role: 'developer' } + } + }, + g: { + $map: { + input: '$g', + as: 'gl', + in: { id: '$$gl._id', name: '$$gl.name', role: 'guest' } + } + } + } + }) + .project({ + _id: 0, + id: '$_id', + name: 1, + description: 1, + openTest: 1, + testUrl: 1, + testFailedInform: 1, + apiChangedInform: 1, + members: { $concatArrays: ['$m', '$d', '$g'] } + }) + .exec()) + .map((res: any) => { + return res.pop() + }) + }) + } + getRole() { + return Observable.of({ + "roleList": [ + { + "name": "master", + "editProject": true, + "editApi": true, + "readApi": true + }, + { + "name": "developer", + "editProject": false, + "editApi": true, + "readApi": true + }, + { + "name": "guest", + "editProject": false, + "editApi": false, + "readApi": true + } + ] + }) + } + getMemberList(id: string) { + return Observable.from(ProjectModel.findOne({_id: mongoose.Types.ObjectId(id)})) + .map((project:any) => { + let list = project.masterList.concat(project.developerList, project.guestList) + let idStr:any = [] + list.forEach((item:any) => idStr.push(item.toString())) + return idStr + }) + } + post(project: any, uname: string) { + try { + return Observable.zip(Observable.of(project), this.devideMember(project.members), (x, y) => Object.assign(x, y)) + .switchMap((p: any) => { + return Observable.fromPromise(ProjectModel.create(p)) + .map((proj: ProjectInterface) => ({ id: proj._id })) + }) + .do((pro: any) => { + this.newCreateMessage(pro.id, project.name, uname, _.pluck(project.members, 'id')) + }) + } catch (e) { + return Observable.throw(e) + } + } + put(_id: string, project: any, uid: string, isAdmin: boolean, uname: string) { + return this.verifyAuth(isAdmin, _id, uid, [role.master]) + .switchMap(() => { + return Observable.zip(Observable.of(project), this.devideMember(project.members), (x, y) => Object.assign(x, y)) + .switchMap((p: any) => { + return Observable.fromPromise(ProjectModel.updateOne({ _id }, { $set: p }).exec()) + .map((res: any) => ({ num: res.n })) + }) + .do((pro: any) => { + this.newUpdateMessage(_id, project.name, uname, _.pluck(project.members, 'id')) + }) + }) + } + delete(id: string, uid: string, isAdmin: boolean, uname: string) { + return this.verifyAuth(isAdmin, id, uid, [role.master]) + .switchMap((authorized: boolean) => { + if (authorized) { + return Observable.fromPromise(ProjectModel.findOneAndRemove({ _id: mongoose.Types.ObjectId(id) }).exec()) + .do((pro: any) => { + let list = pro.masterList.concat(pro.developerList, pro.guestList) + this.newDeleteMessage(id, pro.name, uname, _.pluck(list, 'id')) + }) + .map((res: any) => { + let num = res ? 1 : 0 + return { num } + }) + .do(() => { + InterfaceModel.remove({ pid: mongoose.Types.ObjectId(id) }).exec() + }) + } else { + return Observable.throw({ status: 403, message: '没有访问权限' }) + } + }) + } + export(pid: string, uid: string, isAdmin: boolean) { + return this.verifyAuth(isAdmin, pid, uid) + .switchMap((authorized: boolean) => { + return exp.gen(pid) + .map((url: any) => { + return { url } + }) + }) + } + report(file: string) { + return exp.readFile(file) + } + private devideMember(memberList: any = []) { + return Observable.of(memberList) + .map((list: any) => { + let obj: any = { + masterList: [], + developerList: [], + guestList: [] + } + memberList.forEach((it: any) => { + obj[it.role + 'List'].push(mongoose.Types.ObjectId(it.id)) + }) + return obj + }) + } +} + diff --git a/server/src/project/export.ts b/server/src/project/export.ts index 7ebc695..e4a51eb 100644 --- a/server/src/project/export.ts +++ b/server/src/project/export.ts @@ -1,12 +1,14 @@ -import { InterfaceModel } from './interface.md' +import { InterfaceModel } from '../interface/model' import { mongoose } from '../util/db' import { Observable } from 'rxjs/Rx' import * as _ from 'lodash' import * as fs from 'fs' import * as path from 'path' +import Mock2json from '../mock/Mock2json' -const src = 'template' +const prefix = 'api' const dist = 'report' +const src = 'template' const title = 'API文档' const getFile = (name: string) => { @@ -31,7 +33,7 @@ const setFile = (name: string, data: string) => { const exist = (name: string) => fs.existsSync(path.join('template', name)) const getIfc = (ifc: any) => { - let obj:any = { + let obj: any = { name: ifc.name, method: ifc.method, desc: ifc.desc, @@ -45,25 +47,33 @@ const getIfc = (ifc: any) => { ifc.request.paramList.forEach((it: any) => { obj.requestParams.push({ name: it.name, - desc: it.desc, - type: it.type, - validator: it.validator + desc: it.description, + type: it.type }) }) ifc.response.paramList.forEach((it: any) => { obj.requestParams.push({ name: it.name, - desc: it.desc, - type: it.type, - validator: it.validator + desc: it.description, + type: it.type }) }) - ifc.exceptionList.forEach((it: any) => { + ifc.response.errList.forEach((it: any) => { obj.exceptions.push({ - param: it.param, - desc: it.desc + param: it.mock, + desc: it.remark }) }) + try { + if(ifc.request.paramList.length>0) { + obj.requestExample = JSON.stringify(Mock2json.makeJson(ifc.request.paramList, 'root'), null, 2) + } + if(ifc.response.paramList.length>0) { + obj.responseExample = JSON.stringify(Mock2json.makeJson(ifc.response.paramList, 'root'), null, 2) + } + } catch(e) { + console.error(e) + } return obj } @@ -74,7 +84,7 @@ export default { let bodyTpl = exist('template.body.html') ? getFile('template.body.html') : getFile('default.body.html') let css = exist('template.css') ? getFile('template.css') : getFile('default.css') let head = _.template(headTpl)({ css: `${pid}.css`, title }) - Observable.fromPromise(InterfaceModel.find({ pid: mongoose.Types.ObjectId(pid) })) + return Observable.fromPromise(InterfaceModel.find({ pid: mongoose.Types.ObjectId(pid) })) .map((interfaceList: any) => { let str = '' interfaceList.forEach((ifc: any) => str += _.template(bodyTpl)(getIfc(ifc))) @@ -84,11 +94,14 @@ export default { console.error(e) return Observable.of(e) }) - .do((body: any) => { + .map((body: any) => { let html = _.template(htmlTpl)({ head, body }) setFile(`${pid}.css`, css) setFile(`${pid}.html`, html) + return path.join('/', prefix, dist, `${pid}.html`) }) - .subscribe() + }, + readFile(filePath: string) { + return Observable.of(fs.readFileSync(path.join(dist, filePath))) } } \ No newline at end of file diff --git a/server/src/project/interface.ctrl.ts b/server/src/project/interface.ctrl.ts deleted file mode 100644 index 33ffa16..0000000 --- a/server/src/project/interface.ctrl.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { InterfaceModel } from './interface.md' -import { InterfaceLogInterface, InterfaceLogModel } from './interfaceArchive.md' -import { ProjectModel } from './project.md' -import { MemberInterface, MemberModel } from '../team/member.md' -import { Observable } from 'rxjs/Rx' -import { mongoose } from '../util/db' -import { rename } from '../util/fun' -import * as _ from 'lodash' - -export interface version extends InterfaceLogInterface { - version: string - updateTime: string - updateMember: string - editorId: string -} - -export const interfaceCtrl = { - /** - * 查询项目下的接口列表 - * @param pid 项目id - */ - get(pid: string) { - return Observable.fromPromise(ProjectModel.aggregate() - .lookup({ - from: InterfaceModel.collection.collectionName, - localField: '_id', - foreignField: 'pid', - as: 'apiList' - }) - .append({ - $addFields: { - id: '$_id', - total: { $size: '$apiList' } - } - }) - .exec()) - .map((list: any) => { - let result = list.pop() || {} - result.apiList = rename(result.apiList, [['_id', 'id']]) - return result - }) - }, - /** - * 查询某个接口 - * @param pid 项目id - * @param iid 接口id - */ - getById(pid: string, iid: string) { - return Observable.fromPromise(InterfaceModel.aggregate() - .match({ _id: mongoose.Types.ObjectId(iid) }) - .append({ - $addFields: { - id: '$_id', - currentVersion: '$version', - latestVersion: '$version', - } - }) - .exec()) - .map((x: any) => x.pop()) - }, - /** - * 查询指定版本接口信息 - * @param iid 接口id - * @param version 接口版本 - */ - getHistoryById(iid: string, version: string) { - return Observable.fromPromise(InterfaceLogModel.aggregate() - .match({ iid: mongoose.Types.ObjectId(iid), version }) - .append({ - $addFields: { - id: '$_id', - currentVersion: '$version' - } - }) - .exec()) - .map((res:any) => res.pop()) - }, - /** - * 查询接口历史版本列表 - * @param iid 接口id - */ - getVersionById(iid: string) { - return Observable.fromPromise(InterfaceLogModel.aggregate() - .match({ iid: mongoose.Types.ObjectId(iid) }) - .lookup({ - from: MemberModel.collection.collectionName, - localField: 'editorId', - foreignField: '_id', - as: 'm' - }) - .unwind('$m') - .project({ - version: 1, - updateTime: { $dateToString: { format: '%Y-%m-%d %H:%M:%S', date: '$updateTime' } }, - updateMember: '$m.name' - }) - .exec()) - .map((versionList: any) => ({ versionList })) - }, - /** - * 新增接口 - * @param pid 项目id - * @param ifc 接口数据 - */ - post(pid: string, ifc: any) { - ifc.pid = mongoose.Types.ObjectId(pid) - return Observable.fromPromise(new InterfaceModel(ifc).save()) - }, - /** - * 修改接口 - * @param pid 项目id - * @param iid 接口id - * @param ifc 接口数据 - */ - put(pid: string, iid: string, ifc: any) { - return Observable.fromPromise(InterfaceModel.findOneAndUpdate({ _id: mongoose.Types.ObjectId(iid) }, {$set:ifc}).exec()) - .switchMap((doc: any) => { - if(doc) { - let log = doc.toObject() - log.iid = iid - delete log._id - return Observable.fromPromise(new InterfaceLogModel(log).save()) - } else { - return Observable.throw('更新接口失败') - } - }) - }, - /** - * 删除一个接口 - * @param pid 项目id - * @param iid 接口id - */ - delete(pid: string, iid: string) { - return Observable.fromPromise(InterfaceModel.remove({ _id: mongoose.Types.ObjectId(iid) }).exec()) - .do(() => InterfaceLogModel.remove({ iid: mongoose.Types.ObjectId(iid) }).exec()) - .do(() => ProjectModel.findByIdAndUpdate(mongoose.Types.ObjectId(pid), { - $pull: { - testInterfaceList: mongoose.Types.ObjectId(iid) - } - })) - } -} \ No newline at end of file diff --git a/server/src/project/interface.md.ts b/server/src/project/interface.md.ts deleted file mode 100644 index 355cbac..0000000 --- a/server/src/project/interface.md.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { Schema, mongoose, Model } from '../util/db' - -let InterfaceSchema = new Schema({ - pid: { - type: Schema.Types.ObjectId, - required: true, - set: (v:string|any) => mongoose.Types.ObjectId(v) - }, - url: { - type: String, - match: /^\//, - required: true - }, - name: { - type: String, - maxlength: 40, - required: true - }, - version: { - type: String, - required: true - }, - desc: { - type: String, - maxlength: 200, - default: '' - }, - createdTime: { - type: Date, - required: true, - default: new Date(), - }, - updateTime: { - type: Date, - required: true, - default: new Date() - }, - creatorId: { - type: Schema.Types.ObjectId, - required: true - }, - editorId: { - type: Schema.Types.ObjectId, - required: true, - alias: 'updateMember' - }, - isTest: { - type: Boolean, - default: false - }, - testStatusId: Boolean, - testStatusMsg: String, - needTest: { - type: Boolean, - default: false - }, - delay: Number, - state: { - type: Object, - enum: [{ - _id: false, - id: 0, - name: '待测试' - }, { - _id: false, - id: -1, - name: '测试不通过' - }, { - _id: false, - id: 1, - name: '测试通过' - }] - }, - method: { - type: String, - enum: ['GET', 'POST', 'PUT', 'DELETE'], - required: true - }, - exceptionList: [{ - _id: false, - enabled: Boolean, - result: String, - desc: String, - probability: { - type: Number, - min: 0, - max: 100 - } - }], - request: { - headerList: [{ - _id: false, - id: String, - key: String, - value: String - }], - paramList: [{ - _id: false, - id: String, - isNecessary: Boolean, - dataType: { - type: String, - enum: ['String', 'Number', 'Boolean', 'Object', 'Array'] - }, - mockData: String, - validator: String, - desc: String - }] - }, - response: { - headerList: [{ - _id: false, - id: String, - key: String, - value: String - }], - paramList: [{ - _id: false, - id: String, - isNecessary: Boolean, - dataType: { - type: String, - enum: ['String', 'Number', 'Boolean', 'Object', 'Array'] - }, - mockData: String, - validator: String, - desc: String - }] - } -}) - -enum method { - get = 'GET', - post = 'POST', - put = 'PUT', - delete = 'DELETE' -} -enum dataType { - string = 'String', - number = 'Number', - boolean = 'Boolean', - object = 'Object', - array = 'Array' -} - -interface InterfaceInterface { - pid: string, - id: string, - url: string, - name: string - version: string, - desc: string, - createdTime: string, - updateTime: string, - creatorId: string, - editorId: string, - delay?: number, - state?: { - id: number, - name: string - }, - method: method, - exceptionList?: [{ - enabled: boolean, - result: string, - desc: string, - probability: number - }], - request?: { - headerList: [{ - key: string, - value: string - }], - paramList: [{ - id: string, - isNecessary: boolean, - dataType: dataType, - mockData: string, - validator: string, - desc: string - }] - }, - response?: { - headerList: [{ - key: string, - value: string - }], - paramList: [{ - id: string, - isNecessary: boolean, - dataType: dataType, - mockData: string, - validator: string, - desc: string - }] - } -} - -const InterfaceModel = mongoose.model('interface', InterfaceSchema) - -class Interface extends Model{ - name = this.random() - url = '/' + this.random() - method = method.get - version = this.random(4) -} - -export { - InterfaceSchema, - InterfaceInterface, - InterfaceModel, - method, - dataType, - Interface -} \ No newline at end of file diff --git a/server/src/project/interface.test.ts b/server/src/project/interface.test.ts deleted file mode 100644 index a6ed280..0000000 --- a/server/src/project/interface.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import test from 'ava' -import { Observable } from 'rxjs/Rx' -import { interfaceCtrl } from './interface.ctrl' -import { Interface } from './interface.md' -import { projectCtrl } from './project.ctrl' -import { Project } from './project.md' -import { memberCtrl } from '../team/member.ctrl' -import { Member } from '../team/member.md' - -let project = new Project() -let member = new Member() -let ifc = new Interface() - -test.before('Create project,member', (t: any) => { - return memberCtrl.post(member) - .switchMap((x: any) => { - member.id = x.id - ifc.editorId = x.id - ifc.creatorId = x.id - t.truthy(member.id) - project.memberList = [{ - id: x.id, - role: 'master' - }] - return projectCtrl.post(project).do((x: any) => { - project.id = x.id - t.truthy(project.id) - }) - }) -}) - -test.after('Delete project.member', (t: any) => { - projectCtrl.delete(project.id).subscribe() - memberCtrl.delete(member.id).subscribe() -}) - -test.serial('interface.post', (t: any) => { - return interfaceCtrl.post(project.id, ifc) - .do((res: any) => { - ifc.id = res.id - t.truthy(res.id) - }) -}) - -test.serial('interface.get', (t: any) => { - return interfaceCtrl.get(project.id) - .do((res: any) => { - t.truthy(res.apiList.length > 0) - }) -}) - -test.serial('interface.getVersionById', (t: any) => { - return interfaceCtrl.getVersionById(ifc.id) - .do((res: any) => { - t.truthy(res.versionList) - }) -}) - -test.serial('interface.put', (t:any) => { - return interfaceCtrl.put(project.id, ifc.id, {name: 'nihao'}) - .do((res: any) => { - t.truthy(res) - }) -}) - -// test.serial('interface.getHistoryById', (t: any) => { -// return interfaceCtrl.getHistoryById(ifc.id, ifc.version) -// .do((res: any) => { -// t.deepEqual(res.name, ifc.name) -// }) -// }) - -test.serial('interface.delete', (t: any) => { - return interfaceCtrl.delete(project.id, ifc.id) - .do((res: any) => { - t.truthy(res) - }) -}) \ No newline at end of file diff --git a/server/src/project/interfaceArchive.md.ts b/server/src/project/interfaceArchive.md.ts deleted file mode 100644 index ea78244..0000000 --- a/server/src/project/interfaceArchive.md.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Schema, mongoose } from '../util/db' -import { InterfaceSchema, InterfaceInterface } from './interface.md' - -const InterfaceLogSchema = InterfaceSchema.clone() - -InterfaceLogSchema.iid = { - type: Schema.Types.ObjectId -} - -delete InterfaceLogSchema.needTest - -const InterfaceLogModel = mongoose.model('interfaceLog', InterfaceLogSchema) - -interface InterfaceLogInterface extends InterfaceInterface{ - iid: string -} - -export { - InterfaceLogSchema, - InterfaceLogModel, - InterfaceLogInterface -} \ No newline at end of file diff --git a/server/src/project/project.md.ts b/server/src/project/model.ts similarity index 58% rename from server/src/project/project.md.ts rename to server/src/project/model.ts index df483df..7c731d0 100644 --- a/server/src/project/project.md.ts +++ b/server/src/project/model.ts @@ -1,39 +1,66 @@ import { Schema, mongoose, Model } from '../util/db' -import { MemberSchema } from '../team/member.md' -import { TestSchema, TestInterface } from '../test/test.md' +import { MemberSchema } from '../member/model' +import { TestSchema, TestInterface } from '../test/model' -let ProjectSchema = new Schema({ +let informObj = { + token: { + type: String, + default: '' + }, + createEnabled: { + type: Boolean, + default: false + }, + updateEnabled: { + type: Boolean, + default: false + }, + deleteEnabled: { + type: Boolean, + default: false + }, + testEnabled: { + type: Boolean, + default: false + }, + memberList: { + type: Array, + default: [] + } +} + +let schemaObj = { name: { type: String, maxlength: 20, required: true }, - desc: { + description: { type: String, - maxlength: 200, - alias: 'description' + maxlength: 200 }, testUrl: { type: String, maxlength: 200, alias: 'testAddress' }, - apiChangedInform: { - type: Boolean, - default: false - }, - testFailedInform: { - type: Boolean, - default: false - }, + dingInform: informObj, + towerInform: Object.assign({ + projectId: { + type: String, + default: '' + } + }, informObj), openTest: { type: Boolean, default: false }, masterList: [Schema.Types.ObjectId], developerList: [Schema.Types.ObjectId], - GuestList: [Schema.Types.ObjectId], -}) + guestList: [Schema.Types.ObjectId], +} + +let ProjectSchema = new Schema(schemaObj) enum role { guest = 'guest', @@ -49,7 +76,6 @@ interface ProjectInterface { name: string, desc: string, testUrl: string - memberList: string[], apiChangedInform: boolean, testFailedInform: boolean, openTest: boolean diff --git a/server/src/project/project.ctrl.ts b/server/src/project/project.ctrl.ts deleted file mode 100644 index 1a93b89..0000000 --- a/server/src/project/project.ctrl.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { ProjectModel, ProjectInterface, role } from './project.md' -import { TestModel } from '../test/test.md' -import { MemberModel } from '../team/member.md' -import { InterfaceModel } from './interface.md' -import { GroupModel } from '../team/group.md' -import { Observable } from 'rxjs/Rx' -import { Schema, mongoose } from '../util/db' -import { rename } from '../util/fun' - -export const projectCtrl = { - get() { - return Observable.fromPromise(ProjectModel.aggregate() - .lookup({ - from: TestModel.collection.collectionName, - localField: '_id', - foreignField: 'pid', - as: 'testList' - }) - .lookup({ - from: InterfaceModel.collection.collectionName, - localField: '_id', - foreignField: 'pid', - as: 'interfaceList' - }) - .exec()) - .map((res: any) => { - let result:any = { - total: res.length, - list: [] - } - res.forEach((p:any) => { - let test = p.testList.pop() || {} - let api:any = { - total: p.interfaceList.length, - pass: test.successTest || 0, - } - api.untest = api.total - api.pass - result.list.push({ - id: p._id, - name: p.name, - api - }) - }) - return result - }) - }, - getById(id: string) { - return Observable.fromPromise(ProjectModel.aggregate() - .match({ _id: mongoose.Types.ObjectId(id) }) - .lookup({ - from: MemberModel.collection.collectionName, - localField: 'masterList', - foreignField: '_id', - as: 'm' - }) - .lookup({ - from: MemberModel.collection.collectionName, - localField: 'developerList', - foreignField: '_id', - as: 'd' - }) - .lookup({ - from: MemberModel.collection.collectionName, - localField: 'guestList', - foreignField: '_id', - as: 'g' - }) - .append({ - $addFields: { - m: { - $map: { - input: '$m', - as: 'ml', - in: {id: '$$ml._id', name: '$$ml.name', role: 'master'} - } - }, - d: { - $map: { - input: '$d', - as: 'dl', - in: {id: '$$dl._id', name: '$$dl.name', role: 'developer'} - } - }, - g: { - $map: { - input: '$g', - as: 'gl', - in: {id: '$$gl._id', name: '$$gl.name', role: 'guest'} - } - } - } - }) - .project({ - _id:0, - id: '$_id', - name: 1, - descriptions: '$desc', - openTest: 1, - testFailedInform: 1, - apiChangedInform: 1, - members: {$concatArrays:['$m', '$d', '$g']} - }) - .exec()) - .map((res: any) => { - return res.pop() - }) - }, - post(project: any) { - project.masterList = [] - project.developerList = [] - project.guestList = [] - project.members.forEach((it:any) => project[it.role+'List'].push(it)) - return Observable.fromPromise(ProjectModel.create(project)) - .map((proj: ProjectInterface) => ({ id: proj._id })) - }, - put(_id:string, project: any) { - try { - return Observable.fromPromise(ProjectModel.updateOne({ _id}, project).exec()) - .map((res:any) => ({num: res.n})) - } catch(e) { - return Observable.throw(e) - } - }, - delete(id: string) { - return Observable.fromPromise(ProjectModel.remove({ _id: mongoose.Types.ObjectId(id) })) - .map((res: any) => ({ - num: res.result.n - })) - } -} \ No newline at end of file diff --git a/server/src/project/project.test.ts b/server/src/project/project.test.ts index 0f13e3e..204956c 100644 --- a/server/src/project/project.test.ts +++ b/server/src/project/project.test.ts @@ -16,7 +16,7 @@ test.before('create member,project', (t: any) => { .do((res: any) => { member.id = res.id t.truthy(res.id) - project.memberList = [{ + project.members = [{ role: role.master, id: res.id }] @@ -32,7 +32,11 @@ test.serial('project.post', (t: any) => { test.serial('project.put', (t: any) => { return projectCtrl.put(project.id, { - name: 'hello' + name: 'hello', + members: [{ + id: member.id, + role: role.guest + }] }) .do((res: any) => { t.truthy(res.num) @@ -41,35 +45,35 @@ test.serial('project.put', (t: any) => { test.serial('project.getById', (t: any) => { return projectCtrl.getById(project.id).do((res: any) => { - t.deepEqual(project.id, res.id) - t.truthy(res.members.length>0) + t.deepEqual('hello', res.name) + t.deepEqual(res.members[0].role, role.guest) }) }) -test('project.put.error', (t:any) => { +test('project.put.error', (t: any) => { t.plan(1) return projectCtrl.put('', { name: '111' - }).catch((res:any) => { + }).catch((res: any) => { t.truthy(res) return Observable.of() }).switchMap(() => t.pass()) }) -test('project.post.error', (t:any) => { +test('project.post.error', (t: any) => { t.plan(1) - return projectCtrl.post({}).catch((res:any) => { + return projectCtrl.post({}).catch((res: any) => { t.truthy(res) return Observable.of() }).switchMap(() => t.pass()) }) test.serial('project.get', (t: any) => { - return projectCtrl.get().do((res:any) => { - t.truthy(res) + return projectCtrl.get(member.id, member.isAdmin).do((res: any) => { + t.truthy(res.list.length > 0) }) }) -// test.serial('project.delete', (t: any) => { -// return projectCtrl.delete(project.id).do((res: any) => t.deepEqual(res.num, 1)) -// }) \ No newline at end of file +test.serial('project.delete', (t: any) => { + return projectCtrl.delete(project.id).do((res: any) => t.deepEqual(res.num, 1)) +}) \ No newline at end of file diff --git a/server/src/project/router.ts b/server/src/project/router.ts new file mode 100644 index 0000000..8c48246 --- /dev/null +++ b/server/src/project/router.ts @@ -0,0 +1,16 @@ +import Router from '../util/Router' +import Ctrl from './ProjectCtrl' + +let projectRouter = new Router() +let projectCtrl = new Ctrl() + +export default projectRouter.router + .get('/project', (ctx: any) => projectRouter.handle(ctx, projectCtrl.get(ctx.user._id, ctx.user.isAdmin))) + .get('/project/:id', (ctx: any) => projectRouter.handle(ctx, projectCtrl.getById(ctx.params.id, ctx.user._id, ctx.user.isAdmin))) + .post('/project', (ctx: any) => projectRouter.handle(ctx, projectCtrl.post(ctx.request.fields, ctx.user.name))) + .put('/project/:id', (ctx: any) => projectRouter.handle(ctx, projectCtrl.put(ctx.params.id, ctx.request.fields, ctx.user._id, ctx.user.isAdmin, ctx.user.name))) + .del('/project/:id', (ctx: any) => projectRouter.handle(ctx, projectCtrl.delete(ctx.params.id, ctx.user._id, ctx.user.isAdmin, ctx.user.name))) + .get('/project/:id/doc', (ctx: any) => projectRouter.handle(ctx, projectCtrl.export(ctx.params.id, ctx.user._id, ctx.user.isAdmin))) + .get('/report/:file', (ctx: any) => projectRouter.handleStatic(ctx, projectCtrl.report(ctx.params.file))) + .get('/role', (ctx: any) => projectRouter.handle(ctx, projectCtrl.getRole())) + .routes() \ No newline at end of file diff --git a/server/src/router.ts b/server/src/router.ts deleted file mode 100644 index 72131d3..0000000 --- a/server/src/router.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as Router from 'koa-router' -import { Observable } from 'rxjs/Rx' -import { projectCtrl } from './project/project.ctrl' -import { interfaceCtrl } from './project/interface.ctrl' -import { memberCtrl } from './team/member.ctrl' -import { groupCtrl } from './team/group.ctrl' - -let router = new Router({ - prefix: '/api' -}) - -const handle = (ctx: any, ob: Observable): Promise => ob.catch((e: any) => { - ctx.body = { - errCode: 110, - errMsg: e.message || '操作失败' - } - console.error(e) - return Observable.of() -}) - .do((res: any) => ctx.body = Object.assign({ - errCode: 0 - }, res)) - .toPromise() - -router -// 项目 - .get('/project', (ctx:any) => handle(ctx, projectCtrl.get())) - .get('/project/:id', (ctx:any) => handle(ctx, projectCtrl.getById(ctx.params.id))) - .post('/project', (ctx:any) => handle(ctx, projectCtrl.post(ctx.request.body))) - .put('/project/:id', (ctx:any) => handle(ctx, projectCtrl.put(ctx.params.id, ctx.request.body))) - .del('/project/:id', (ctx:any) => handle(ctx, projectCtrl.delete(ctx.params.id))) - // 接口 - .get('/project/:pid/api', (ctx:any) => handle(ctx, interfaceCtrl.get(ctx.params.pid))) - .get('/project/:pid/api/:id', (ctx:any) => handle(ctx, interfaceCtrl.getById(ctx.params.pid, ctx.params.id))) - .get('/project/:pid/api/:id/version', (ctx:any) => handle(ctx, interfaceCtrl.getVersionById(ctx.params.iid))) - .post('/project/:pid', (ctx:any) => handle(ctx, interfaceCtrl.post(ctx.params.id, ctx.request.body))) - .put('/project/:pid/api/:id', (ctx:any) => handle(ctx, interfaceCtrl.put(ctx.params.pid, ctx.params.id, ctx.request.body))) - .del('/project/:pid/api/:id', (ctx:any) => handle(ctx, interfaceCtrl.delete(ctx.param.pid, ctx.params.id))) -// 成员 - .get('/group', (ctx:any) => handle(ctx, groupCtrl.get())) - .get('/role', (ctx:any) => handle(ctx, groupCtrl.getRole())) - .get('/member', (ctx:any) => handle(ctx, memberCtrl.get())) - .post('/member', (ctx:any) => handle(ctx, memberCtrl.post(ctx.request.body))) - .post('/user/login', (ctx:any) => handle(ctx, memberCtrl.login(ctx.request.body.user, ctx.request.body.password))) - -export default router.routes() \ No newline at end of file diff --git a/server/src/shim.d.ts b/server/src/shim.d.ts index 1d17cdd..2126984 100644 --- a/server/src/shim.d.ts +++ b/server/src/shim.d.ts @@ -1,12 +1,21 @@ /* node modules */ declare module 'process' declare module 'crypto' +declare module 'fs' +declare module 'path' +declare module 'events' /* 3rd modules */ declare module 'koa' -declare module 'koa-bodyparser' +declare module 'koa-better-body' declare module 'koa-router' declare module 'mongoose' declare module 'sinon' declare module 'ava' -declare module 'lodash' \ No newline at end of file +declare module 'path-to-regexp' +declare module 'lodash' +declare module 'jsonwebtoken' +declare module 'mockjs' +declare module 'jsonschema' +declare module 'underscore' +declare module 'axios' \ No newline at end of file diff --git a/server/src/system/SystemCtrl.ts b/server/src/system/SystemCtrl.ts new file mode 100644 index 0000000..133fec9 --- /dev/null +++ b/server/src/system/SystemCtrl.ts @@ -0,0 +1,96 @@ +import { Observable } from 'rxjs/Rx' +import * as fs from 'fs' +import { staticPath } from '../util/config' +import * as path from 'path' +import BaseCtrl from '../util/BaseCtrl' +import { SystemModel } from './model' +import MemberCtrl from '../member/MemberCtrl' + +export default class SystemCtrl extends BaseCtrl { + initSystem() { + Observable.from(SystemModel.findOne().exec()) + .debounceTime(1000) + .subscribe((doc: any) => { + if (!doc) { + new SystemModel().save(() => { + console.log('初始化系统配置成功') + }, (e: any) => { + console.error('初始化系统配置失败:', e) + }) + } + }) + } + initMember() { + let memberCtrl = new MemberCtrl() + memberCtrl.get(true) + .debounceTime(1000) + .subscribe((doc: any) => { + if (!doc.memberList || !doc.memberList.length) { + const password = Math.random().toString(32).substring(2, 8) + memberCtrl.post({ + password, + account: 'admin', + name: '系统管理员', + isAdmin: true + }) + .subscribe(() => console.log('创建管理员账户成功!账号:admin 密码:' + password), () => console.error('创建管理失败')) + } + }) + } + get() { + return Observable.fromPromise(SystemModel.findOne().exec()) + .map((doc: any) => { + let reportStyle + let reportTemplate + const templatePath = 'template' + let cssPath = path.join(templatePath, 'template.css') + let htmlPath = path.join(templatePath, 'template.html') + if (!fs.existsSync(cssPath)) { + cssPath = path.join(templatePath, 'default.css') + } + if (!fs.existsSync(htmlPath)) { + htmlPath = path.join(templatePath, 'default.body.html') + } + reportStyle = fs.readFileSync(cssPath).toString() + reportTemplate = fs.readFileSync(htmlPath).toString() + return Object.assign({ + reportStyle, + reportTemplate + }, doc._doc) + }) + } + put(configInfo: any, isAdmin: boolean) { + if (isAdmin) { + try { + fs.writeFileSync(path.join('template', 'template.css'), configInfo.reportStyle) + fs.writeFileSync(path.join('template', 'template.body.html'), configInfo.reportTemplate) + return Observable.from(SystemModel.update(configInfo).exec()) + } catch (e) { + return Observable.throw(e) + } + } else { + return Observable.throw({ + errorCode: '403', + errorMsg: '没有操作权限' + }) + } + } + upload(files: any, isAdmin: boolean = false) { + try { + if (isAdmin) { + let file = files.pop() + let name = path.join(staticPath, file.name.replace(/.*\/(.*)/ig, '$1')) + fs.renameSync(file.path, name) + let imgUrl = '/api/' + name + return Observable.of({ imgUrl }) + } else { + return Observable.throw({ status: 403, message: '没有上传权限' }) + } + } catch (e) { + return Observable.throw(e) + } + } + readFile(filePath: string) { + return Observable.of(fs.readFileSync(path.join(staticPath, filePath))) + } +} \ No newline at end of file diff --git a/server/src/system/model.ts b/server/src/system/model.ts new file mode 100644 index 0000000..60aec46 --- /dev/null +++ b/server/src/system/model.ts @@ -0,0 +1,52 @@ +import { Schema, mongoose } from '../util/db' + +let SystemSchema = new Schema({ + companyLogo: { + type: String, + default: '' + }, + companyName: { + type: String, + default: '' + }, + backupSize: { + type: Number, + default: 0 + }, + testAuto: { + type: Boolean, + default: 0 + }, + testInterval: { + type: String, + enum: ['', 'hourly', 'daily', 'monthly', 'weekly'], + default: '' + }, + testTime: { + type: Number, + default: '' + } +}) + +let SystemModel = mongoose.model('system', SystemSchema) + +enum SystemInterval { + hourly = 'hourly', + daily = 'daily', + monthly = 'monthly', + weekly = 'weekly' +} + +enum SystemInformOperation { + addInterface = "addInterface", + editInterface = "editInterface", + deleteInterface = "deleteInterface", + autoTestInterface = "autoTestInterface" +} + +export { + SystemSchema, + SystemModel, + SystemInterval, + SystemInformOperation +} diff --git a/server/src/system/router.ts b/server/src/system/router.ts new file mode 100644 index 0000000..aee3f58 --- /dev/null +++ b/server/src/system/router.ts @@ -0,0 +1,13 @@ +import Router from '../util/Router' +import Ctrl from './SystemCtrl' +import { staticPath } from '../util/config' + +let systemRouter = new Router() +let systemCtrl = new Ctrl() + +export default systemRouter.router + .get(`/${staticPath}/:file`, (ctx: any) => systemRouter.handleStatic(ctx, systemCtrl.readFile(ctx.params.file))) + .get('/setting', (ctx:any) => systemRouter.handle(ctx, systemCtrl.get())) + .put('/setting', (ctx:any) => systemRouter.handle(ctx, systemCtrl.put(ctx.request.fields, ctx.user.isAdmin))) + .post('/upload/img', (ctx: any) => systemRouter.handle(ctx, systemCtrl.upload(ctx.request.files, ctx.user.isAdmin))) + .routes() \ No newline at end of file diff --git a/server/src/system/system.md.ts b/server/src/system/system.md.ts deleted file mode 100644 index fafc8c7..0000000 --- a/server/src/system/system.md.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Schema, mongoose } from '../util/db' - -let SystemSchema = new Schema({ - coverUrl: String, - name: String, - backupSize: Number, - autoTestEnabled: Boolean, - autoTestInterval: { - interval: { - type: String, - enum: ['hourly', 'daily', 'monthly', 'weekly'] - }, - time: Number - } -}) - -let SystemModel = mongoose.model('system', SystemSchema) - -enum SystemInterval { - hourly = 'hourly', - daily = 'daily', - monthly = 'monthly', - weekly = 'weekly' -} - -interface SystemInterface { - coverUrl?: string, - name?: string, - backupSize?: number, - autoTestEnabled?: boolean, - autoTestInterval?: { - interval: SystemInterval - time: number - } -} - -export { - SystemSchema, - SystemModel, - SystemInterface, - SystemInterval -} diff --git a/server/src/team/group.ctrl.ts b/server/src/team/group.ctrl.ts deleted file mode 100644 index 5fdb887..0000000 --- a/server/src/team/group.ctrl.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { GroupModel, GroupInterface } from './group.md' -import { Observable } from 'rxjs/Rx' -import { MemberModel } from './member.md' -import { rename } from '../util/fun' - -export const groupCtrl = { - get() { - return Observable.fromPromise(GroupModel.aggregate() - .lookup({ - from: MemberModel.collection.collectionName, - localField: 'memberList', - foreignField: '_id', - as: 'members' - }) - .project({ - _id: 0, - id: '$_id', - members: { - $map: { - input: '$members', - as: 'm', - in: { - id: '$$m._id', - name: '$$m.name' - } - } - } - }) - .exec()) - .map((groups: any) => ({groups})) - }, - getRole() { - return Observable.of({ - "roleList": [ - { - "name": "master", - "editProject": true, - "editApi": true, - "readApi": true - }, - { - "name": "developer", - "editProject": false, - "editApi": true, - "readApi": true - }, - { - "name": "guest", - "editProject": false, - "editApi": false, - "readApi": true - } - ] - }) - }, - post(group: any) { - return Observable.fromPromise(new GroupModel(group).save()) - }, - put(group: any) { - return Observable.fromPromise(GroupModel.updateOne({ _id: group.id }, group).exec()) - .switchMap((res: any) => res.n > 0 ? Observable.of({ num: res.n }) : Observable.throw('更新小组失败')) - }, - delete(id: string) { - return Observable.fromPromise(GroupModel.remove({ _id: id })) - .map(() => ({ num: 1 })) - } -} \ No newline at end of file diff --git a/server/src/team/group.md.ts b/server/src/team/group.md.ts deleted file mode 100644 index 77de693..0000000 --- a/server/src/team/group.md.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Schema, mongoose, Model } from '../util/db' - -let GroupSchema = new Schema({ - name: { - type: String, - maxlength: 20, - required: true - }, - memberList: [Schema.Types.ObjectId] -}) - -let GroupModel = mongoose.model('group', GroupSchema) - -// 用于限制返回值 -interface GroupInterface { - id: string, - name: string, - memberList: string[] -} - -// 用于生成数据 -class Group extends Model{ - name: string = this.random() - memberList: string[] = [] -} - -export { - GroupSchema, - GroupModel, - GroupInterface, - Group -} \ No newline at end of file diff --git a/server/src/team/group.test.ts b/server/src/team/group.test.ts deleted file mode 100644 index 13075d5..0000000 --- a/server/src/team/group.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import test from 'ava' -import { groupCtrl } from './group.ctrl' -import { Group } from './group.md' -import { Member } from './member.md' -import { memberCtrl } from './member.ctrl' -import { Observable } from 'rxjs/Rx' - -let group = new Group() -let member = new Member() -let ready: any - -test.before('create Member', (t: any) => { - return memberCtrl.post(member) - .do((res: any) => { - t.truthy(res.id) - group.memberList = [res.id] - }) -}) - -test.serial('group.getRole', (t: any) => { - return groupCtrl.getRole() - .do((res: any) => { - t.truthy(res.roleList.length > 0) - }) -}) - -test.serial('group.post', (t: any) => { - return groupCtrl.post(group) - .do((res: any) => { - group.id = res.id - t.truthy(res.id) - }) -}) - - -test.serial('group.get', (t: any) => { - return groupCtrl.get() - .do((res: any) => { - t.truthy(res.groups.length > 0) - let g = res.groups.filter((x: any) => x.id == group.id)[0] - t.truthy(g.members.length === 1) - t.truthy(g.members.id) - }) -}) - -test.serial('group.put', (t: any) => { - group.name = 'test4api' - return groupCtrl.put(group) - .switchMap((res: any) => groupCtrl.get().do((rest: any) => { - let newGroup = rest.groups.filter((x: any) => x.id == group.id)[0] - t.truthy(newGroup.name === 'test4api') - })) -}) - -test.serial('group.delete', (t: any) => { - return groupCtrl.delete(group.id) - .do((res: any) => { - t.truthy(res) - }) -}) - -test('group.post.error', (t:any) => { - return groupCtrl.post({}) - .catch((e:any) => { - t.truthy(e) - return Observable.of() - }) -}) - -test('group.put.error', (t:any) => { - return groupCtrl.put({}) - .catch((e:any) => { - t.truthy(e) - return Observable.of() - }) -}) \ No newline at end of file diff --git a/server/src/team/member.ctrl.ts b/server/src/team/member.ctrl.ts deleted file mode 100644 index 0c75bc1..0000000 --- a/server/src/team/member.ctrl.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MemberModel, MemberInterface } from './member.md' -import { Observable } from 'rxjs/Rx' -import { encrypt } from '../util/crypto' - -export const memberCtrl = { - getMap() { - let map: any = {} - return Observable.zip(Observable.fromPromise(MemberModel.find({})) - .switchMap((list: any) => Observable.from(list)) - .map((x: any) => ({ [x._id]: x })) - .toArray(), (arg: any) => Object.assign.apply(null, arg)) - }, - get() { - return Observable.fromPromise(MemberModel.aggregate().project({ - "id": "$_id", - "isAdmin": 1, - "account": 1, - "name": 1 - })) - .map((list: MemberInterface[]) => ({ list })) - }, - post(member: any) { - member.password = encrypt(member.password) - return Observable.fromPromise(new MemberModel(member).save()) - .map((x: any) => ({ id: x._id })) - }, - delete(_id: string) { - return Observable.fromPromise(MemberModel.remove({ _id }).exec()) - .map((res: any) => ({ num: res.result.n })) - }, - login(account: string, password: string) { - return Observable.fromPromise(MemberModel.findOne({ - account, - password - })) - .map((res: MemberInterface | undefined) => { - let result = {} - if (res) { - result = { - user: { - name: res.account, - role: res.role, - avatar: '' - } - } - } else { - result = { - code: -1, - message: '用户名或密码错误' - } - } - return result - }) - } -} \ No newline at end of file diff --git a/server/src/test/model.ts b/server/src/test/model.ts new file mode 100644 index 0000000..b006581 --- /dev/null +++ b/server/src/test/model.ts @@ -0,0 +1,55 @@ +import { Schema, mongoose, Model } from '../util/db' + +const TestSchema = new Schema({ + pid: { + type: Schema.Types.ObjectId, + required: true + }, + startTime: Date, + endTime: { + type: Date, + default: new Date + }, + totalTime: Number, + operatorId: String, + totalTest: { + type:Number, + default: 0 + }, + successTest: { + type: Number, + default: 0 + }, + result: { + type: String, + default: '' + }, + errorLog: String +}) + +interface TestInterface { + pid: string, + startTime: Date, + endTime: Date, + totalTime: number, + operatorId: string, + totalTest: number, + successTest: number, + result: string, + errorLog: string +} + +class Test extends Model { + startTime = new Date + totalTest = Math.ceil(Math.random() * 1000) + successTest = Math.ceil(Math.random() * 100) +} + +const TestModel = mongoose.model('Test', TestSchema) + +export { + TestSchema, + TestModel, + TestInterface, + Test +} \ No newline at end of file diff --git a/server/src/thirdparty/BaseExternalMessage.ts b/server/src/thirdparty/BaseExternalMessage.ts new file mode 100644 index 0000000..df0d24c --- /dev/null +++ b/server/src/thirdparty/BaseExternalMessage.ts @@ -0,0 +1,15 @@ +import * as axios from 'axios' +import {Observable} from 'rxjs/Rx' + +export default abstract class BaseExternalMessage { + protected baseUrl = '' + protected request = axios + protected obs = Observable + protected from = Observable.from + protected token = '' + constructor(token: string) { + this.token = token + } + abstract send(content: string, toUser?: String[]):void +} + diff --git a/server/src/thirdparty/Dingding.ts b/server/src/thirdparty/Dingding.ts new file mode 100644 index 0000000..ec9dc40 --- /dev/null +++ b/server/src/thirdparty/Dingding.ts @@ -0,0 +1,23 @@ +import BaseExternalMessage from './BaseExternalMessage' + +export default class Dingding extends BaseExternalMessage{ + protected baseUrl = 'https://oapi.dingtalk.com' + send(text: string, user?: string[]) { + const url = `/robot/send?access_token=${this.token}` + this.from(this.request({ + url: this.baseUrl + url, + method: 'POST', + data: { + msgtype: 'markdown', + markdown: { + title: 'API变更', + text + } + } + })) + .debounceTime(2000) + .subscribe(() => {}, (e:any) => { + console.error('发送钉钉消息失败:', e) + }) + } +} diff --git a/server/src/thirdparty/ThirdPartyEngine.ts b/server/src/thirdparty/ThirdPartyEngine.ts new file mode 100644 index 0000000..9385451 --- /dev/null +++ b/server/src/thirdparty/ThirdPartyEngine.ts @@ -0,0 +1,56 @@ +/** + * 接收接口事件,并调用对应第三方接口 + */ +import Tower from './Tower' +import Dingding from './Dingding' +import * as events from 'events' +import {ProjectModel} from '../project/model' +import SystemCtrl from '../system/SystemCtrl' +import { Observable } from 'rxjs/Rx' + +enum operation { + create = 'create', + delete = 'delete', + update = 'update', + test = 'test' +} + +export default class ThirdPartyEngine { + private static systemCtrl = new SystemCtrl() + static c = operation.create + static d = operation.delete + static u = operation.update + static t = operation.test + + static notify(type: operation, ifc: any):void { + // 生成消息内容 + let genContent = this.systemCtrl.get() + .map((cfg: any) => { + let content = + `API名称:${ifc.name}\n + 修改者:${ifc.editor}\n + 操作:${type}\n + [详情](${cfg.apiUrl}/project/${ifc.pid}/api/${ifc.id}/detail) + ` + return content + }) + // 生成需要执行的任务 + let genTask = Observable.from(ProjectModel.findOne({_id:ifc.pid})) + .map((project: any) => { + let taskList = [] + if (project.dingInform.token && project.dingInform[type+'Enabled']) { + taskList.push(new Dingding(project.dingInform.token)) + } + if (project.towerInform.token && project.towerInform[type+'Enabled']) { + taskList.push(new Dingding(project.towerInform.totken)) + } + return taskList + }) + Observable.combineLatest(genContent, genTask) + .subscribe((todo: any) => { + todo[1].forEach((task: any) => { + task.send(todo[0]) + }) + }) + } +} \ No newline at end of file diff --git a/server/src/thirdparty/Tower.ts b/server/src/thirdparty/Tower.ts new file mode 100644 index 0000000..581f7a1 --- /dev/null +++ b/server/src/thirdparty/Tower.ts @@ -0,0 +1,21 @@ +import BaseExternalMessage from './BaseExternalMessage' + +export default class Tower extends BaseExternalMessage { + protected baseUrl = 'https://api.tower.im/v1' + private headers = {} + protected getMessage(id: string) { + this.from(this.request({ + baseUrl: this.baseUrl, + url: `/todolists/${id}` + })) + } + send(content: string, user?:string[]) { + this.from(this.request({ + baseUrl: this.baseUrl, + url: '' + })) + .subscribe(() => {}, (e:any) => { + console.error('生成tower任务失败:', e) + }) + } +} \ No newline at end of file diff --git a/server/src/util/BaseCtrl.ts b/server/src/util/BaseCtrl.ts new file mode 100644 index 0000000..d471902 --- /dev/null +++ b/server/src/util/BaseCtrl.ts @@ -0,0 +1,140 @@ +import { ProjectModel, role } from '../project/model' +import { Observable } from 'rxjs/Rx' +import { mongoose } from './db' +import { MessageModel } from '../message/model' +import * as _ from 'underscore' + +export default class BaseCtrl { + protected model: any + protected from = Observable.from + protected module: string + + protected operate(op: string, ...arg:any[]) { + return this.from(this.model[op](...arg).exec()) + } + protected aggregate(...arg: any[]) { + return this.operate('aggregate', ...arg) + } + protected update(...arg: any[]) { + return this.operate('update', ...arg) + } + /** + * 记录操作,生成系统消息 + * @param id 数据id + * @param name 数据名 + * @param content 操作内容 + * @param operation 操作 + */ + protected newMessage(objectId: string, objectName: string, operation: string, operator: string, readableUserList: string[], content?: string) { + if (this.module) { + MessageModel.create({ + objectId, + objectName, + module: this.module, + content, + operator, + readableUserList, + operation + }).then(() => { }, (e: any) => console.error(e)) + } else { + console.error('请先设置模块') + } + } + + protected newCreateMessage(id: string, name: string, operator: string, userList: string[], content?: string) { + return this.newMessage(id, name, 'create', operator, userList, content) + } + + protected newUpdateMessage(id: string, name: string, operator: string, userList: string[], content?: string) { + return this.newMessage(id, name, 'update', operator, userList, content) + } + + protected newDeleteMessage(id: string, name: string, operator: string, userList: string[], content?: string) { + return this.newMessage(id, name, 'delete', operator, userList, content) + } + + protected newTestMessage(id: string, name: string, operator: string, userList: string[], content?: string) { + return this.newMessage(id, name, 'test', operator, userList, content) + } + + /** + * 数据模型 + */ + protected objectId(id?: string | string[]) { + if (id) { + if (typeof id === 'string' && id.length === 24) { + return mongoose.Types.ObjectId(id) + } else if (_.isArray(id)) { + let list:string[] = [] + Array.prototype.forEach.call(id, (i:string)=> { + list.push(mongoose.Types.ObjectId(i)) + }) + return list + } else { + return id + } + } else { + return mongoose.Types.ObjectId() + } + } + /** + * 根据项目id和用户id查询对应角色 + * @param pid 项目id + * @param uid 用户id + */ + protected getProjectRole(pid: string, uid: string): Observable { + return Observable.from(ProjectModel.aggregate() + .match({ _id: mongoose.Types.ObjectId(pid) }) + .project({ + m: { + $map: { + input: '$masterList', + as: 'ml', + in: { id: '$$ml', role: 'master' } + } + }, + d: { + $map: { + input: '$developerList', + as: 'dl', + in: { id: '$$dl', role: 'developer' } + } + }, + g: { + $map: { + input: '$guestList', + as: 'gl', + in: { id: '$$gl', role: 'guest' } + } + } + }) + .project({ members: { $concatArrays: ['$m', '$d', '$g'] } }) + .exec()) + .map((projectList: any = []) => { + let project = projectList.pop() || { members: [] } + let member = project.members.filter((x: { id: string, role: string }) => x.id.toString() == uid.toString()).pop() || {} + return member.role + }) + } + /** + * 根据项目id和用户id,查询是否属于该角色是否具有对应权限 + * @param pid 项目id + * @param uid 用户id + * @param roleList 角色列表 + */ + protected verifyProjectRole(pid: string, uid: string, roleList?: role[]) { + return this.getProjectRole(pid, uid) + .map((r: role) => r ? (roleList || [r]).includes(r) : false) + } + /** + * 查询用户是否具有权限 + * @param isAdmin 是否管理员 + * @param pid 项目id + * @param uid 用户id + * @param roleList 角色列表 + */ + protected verifyAuth(isAdmin: boolean, pid: string, uid: string, roleList?: role[]) { + return Observable.from(isAdmin ? Observable.of(true) : this.verifyProjectRole(pid, uid, roleList)) + .switchMap((authorized: boolean) => authorized ? Observable.of(true) : Observable.throw({ status: 403, message: '访问权限不够' })) + } +} diff --git a/server/src/util/Cache.ts b/server/src/util/Cache.ts new file mode 100644 index 0000000..ea5b747 --- /dev/null +++ b/server/src/util/Cache.ts @@ -0,0 +1,46 @@ +import * as _ from 'lodash' + +export default class CacheFactory { + private static cacheStore: any = {} + /** + * 从缓存池获取缓存 + * @param name 缓存名称 + */ + static of(name: string) { + return this.cacheStore[name] = this.cacheStore[name] || new Cache() + } +} + +class Cache { + private memCache: any = {} + + private getSize(...arg: any[]): number { + let total = 0 + Array.prototype.forEach.call(arg, (it: any) => { + total += _.isArray(arg[0]) ? it.length : it.keys().length + }) + return total + } + + remove(key?: string): void { + if (key) delete this.memCache[key] + else this.memCache = {} + } + + set(key: string, value: any = '') { + this.memCache[key] = value + } + + append(key: string, ...arg: any[]) { + let cache = this.memCache[key] = this.memCache[key] || [] + if (_.isArray(cache)) { + Array.prototype.forEach.call(arg, (it: any) => cache.push(it)) + } else { + cache = Object.assign(arg) + } + } + + get(key: string, dft: any): any { + return key ? this.memCache[key] : dft || '' + } +} \ No newline at end of file diff --git a/server/src/util/Router.ts b/server/src/util/Router.ts new file mode 100644 index 0000000..48b122f --- /dev/null +++ b/server/src/util/Router.ts @@ -0,0 +1,76 @@ +import * as koaRouter from 'koa-router' +import { Observable } from 'rxjs/Rx' + +export default class Router { + router = new koaRouter({ + prefix: '/api' + }) + constructor() { + this.router + .param('pid', (id: string, ctx: any, next: any) => { + if (!/\w{24}/.test(id)) { + return ctx.throw(404, '项目id格式不正确') + } else { + return next() + } + }) + .param('id', (id: string, ctx: any, next: any) => { + if (!/\w{24}/.test(id)) { + return ctx.throw(404, 'id格式不正确') + } else { + return next() + } + }) + } + + public handleProxy(ctx: any, ob: Observable): Promise { + return ob.catch((e: any) => { + ctx.body = { + data:e.response.data, + headers: e.response.headers, + status: e.response.status, + statusText: e.response.statusText + } + return Observable.of() + }) + .do((res: any) => ctx.body = res) + .toPromise() + } + + public handle(ctx: any, ob: Observable): Promise { + return ob.catch((e: any) => { + console.error('请求出错', e) + if (e.status) { + ctx.throw(e.status, e.message || '操作失败') + } else { + ctx.body = { + errCode: 110, + errMsg: e.message || '操作失败' + } + } + return Observable.of() + }) + .do((res: any) => ctx.body = Object.assign({ errCode: 0 }, res)) + .toPromise() + } + + public handleStatic(ctx: any, ob: Observable): Promise { + return ob.catch((e: any) => { + console.error(e) + ctx.throw(404, '未找到资源') + return Observable.of() + }) + .do((res: any) => { + if (/(\.png|\.jpg|\.jpeg)$/.test(ctx.path)) { + ctx.type = 'image/' + ctx.path.replace(/.*\.(.*)$/, '$1') + } else if (/.svg/.test(ctx.path)) { + ctx.type = 'image/svg+xml' + } else { + ctx.type = 'text/' + ctx.path.replace(/.*\.(html|css)$/, '$1') + } + ctx.body = res + }) + .toPromise() + } + +} \ No newline at end of file diff --git a/server/src/util/config.ts b/server/src/util/config.ts index 5f1ea72..d935875 100644 --- a/server/src/util/config.ts +++ b/server/src/util/config.ts @@ -1,6 +1,52 @@ -import {env} from 'process' +import { env } from 'process' +import * as path from 'path' +import * as fs from 'fs' +import * as crypto from 'crypto' export const dbAddr = env.DB_HOST || 'api-db' export const dbPort = env.DB_PORT || 27017 export const dbName = 'api' -export const salt = 'Api is good for developers' \ No newline at end of file +export const uploadPath = 'upload' +export const staticPath = 'static' +export const avatar = ` + + + + TEXT + +` + +export let key = '' + +try { + if (!fs.existsSync('key')) { + key = Math.random().toString(32).substring(2) + fs.writeFileSync('key', key) + } else { + key = fs.readFileSync('key').toString() + } +} catch (e) { + console.error('初始化系统文件失败', e) +} + +[uploadPath, staticPath].forEach((path: string) => { + try { + if (!fs.existsSync(path)) { + fs.mkdirSync(path) + } + } catch (e) { + console.error('创建目录%s失败,可能导致系统无法正常使用', path, e) + } +}) \ No newline at end of file diff --git a/server/src/util/crypto.ts b/server/src/util/crypto.ts index aaa0dd4..f72b0df 100644 --- a/server/src/util/crypto.ts +++ b/server/src/util/crypto.ts @@ -1,10 +1,25 @@ import * as crypto from 'crypto' -import { salt } from './config' +import { key } from './config' -const cipher = crypto.createCipher('aes192', salt); -export const encrypt = (text: string) => { - let encrypted = cipher.update(text, 'utf8', 'hex'); +const encrypt = (text: string='') => { + let cipher = crypto.createCipher('aes192', key) + let encrypted = cipher.update(text, 'utf8', 'hex') encrypted += cipher.final('hex'); return encrypted -} \ No newline at end of file +} + +const decrypt = (text: string='') => { + let decipher = crypto.createDecipher('aes192', key) + let decrypted = decipher.update(text, 'hex', 'utf8') + decrypted += decipher.final('hex') + return decrypted +} + +const hash = (text: string) => crypto.createHash('md5').update(text).digest('base64') + +export { + encrypt, + decrypt, + hash +} diff --git a/server/template/default.body.html b/server/template/default.body.html index 09848d7..81fc65e 100644 --- a/server/template/default.body.html +++ b/server/template/default.body.html @@ -13,7 +13,10 @@

参数 类型 说明 +<<<<<<< HEAD 校验 +======= +>>>>>>> 47fa4b2307a070bbd57f634601c0e6c52d3650d2 <% requestParams.forEach(it => { %> @@ -26,6 +29,7 @@

<%= it.desc %> +<<<<<<< HEAD <%= it.rule %> @@ -34,6 +38,15 @@

请求示例

<%=requestExample%>
+======= + + <% }) %> + +
> +

请求示例

+
<%=requestExample%>
+
+>>>>>>> 47fa4b2307a070bbd57f634601c0e6c52d3650d2
0 ? '' : 'style="display:none"'%> >

返回参数

@@ -42,7 +55,10 @@

参数 类型 说明 +<<<<<<< HEAD 校验 +======= +>>>>>>> 47fa4b2307a070bbd57f634601c0e6c52d3650d2 <% responseParams.forEach(it => { %> @@ -55,6 +71,7 @@

<%=it.desc%> +<<<<<<< HEAD <%=it.rule%> @@ -63,6 +80,15 @@

返回示例

<%=responseExample%>
+======= + + <% }) %> + +
> +

返回示例

+
<%=responseExample%>
+
+>>>>>>> 47fa4b2307a070bbd57f634601c0e6c52d3650d2

0 ? '' : 'style="display:none"'%> >

异常处理

diff --git a/server/template/template.html b/server/template/template.html new file mode 100644 index 0000000..710758d --- /dev/null +++ b/server/template/template.html @@ -0,0 +1,79 @@ +

+ <%= name %> +

+
+
<%= method %>   <%= url %>
+

+ <%= desc %> +

+
0 ? '' : 'style="display:none"'%> > +

请求参数

+ + + + + + + <% requestParams.forEach(it => { %> + + + + + + <% }) %> +
参数类型说明
+ <%= it.name %> + + <%= it.type %> + + <%= it.desc %> +
+
> +

请求示例

+
<%=requestExample%>
+
+
+
0 ? '' : 'style="display:none"'%> > +

返回参数

+ + + + + + + <% responseParams.forEach(it => { %> + + + + + + <% }) %> +
参数类型说明
+ <%=it.name%> + + <%=it.type%> + + <%=it.desc%> +
+
+

返回示例

+
<%=responseExample%>
+
+
+
0 ? '' : 'style="display:none"'%> > +

异常处理

+ + + + + + + + + +
返回值说明
+ <%=exceptions.response%> + + <%=exceptions.desc%> +
+
\ No newline at end of file diff --git a/web/1.log b/web/1.log deleted file mode 100644 index ed8d51a..0000000 --- a/web/1.log +++ /dev/null @@ -1,393 +0,0 @@ - -> api-web@1.0.0 dev /app -> node build/dev-server.js - -[HPM] Proxy created: /api -> http://api-server:2018 -> Starting dev server... -ts-loader: Using typescript@2.5.2 and /app/tsconfig.json - DONE Compiled successfully in 7614ms2:23:41 PM - -> Listening at http://localhost:2017 - - WAIT Compiling...2:48:28 PM - - DONE Compiled successfully in 1659ms2:48:30 PM - - WAIT Compiling...2:48:30 PM - - DONE Compiled successfully in 1248ms2:48:32 PM - - WAIT Compiling...2:50:59 PM - - DONE Compiled successfully in 1604ms2:51:01 PM - - WAIT Compiling...2:51:01 PM - - DONE Compiled successfully in 997ms2:51:02 PM - - WAIT Compiling...3:30:45 PM - - DONE Compiled successfully in 1477ms3:30:47 PM - - WAIT Compiling...3:30:47 PM - - DONE Compiled successfully in 1298ms3:30:49 PM - - WAIT Compiling...3:31:39 PM - - DONE Compiled successfully in 511ms3:31:40 PM - - WAIT Compiling...3:31:40 PM - - DONE Compiled successfully in 1748ms3:31:42 PM - - WAIT Compiling...4:15:21 PM - - DONE Compiled successfully in 1551ms4:15:23 PM - - WAIT Compiling...4:15:23 PM - - DONE Compiled successfully in 1201ms4:15:24 PM - - WAIT Compiling...5:14:35 PM - - DONE Compiled successfully in 1265ms5:14:36 PM - - WAIT Compiling...5:14:37 PM - - DONE Compiled successfully in 1324ms5:14:38 PM - - WAIT Compiling...5:52:40 PM - - DONE Compiled successfully in 1419ms5:52:42 PM - - WAIT Compiling...5:52:42 PM - - DONE Compiled successfully in 1173ms5:52:43 PM - - WAIT Compiling...5:54:37 PM - - DONE Compiled successfully in 1362ms5:54:39 PM - - WAIT Compiling...5:54:39 PM - - DONE Compiled successfully in 1288ms5:54:40 PM - - WAIT Compiling...5:58:25 PM - - DONE Compiled successfully in 1459ms5:58:27 PM - - WAIT Compiling...5:58:27 PM - - DONE Compiled successfully in 1011ms5:58:28 PM - - WAIT Compiling...9:12:06 AM - - DONE Compiled successfully in 1384ms9:12:08 AM - - WAIT Compiling...9:12:08 AM - - DONE Compiled successfully in 1292ms9:12:09 AM - - WAIT Compiling...9:42:37 AM - - DONE Compiled successfully in 1286ms9:42:39 AM - - WAIT Compiling...9:42:39 AM - - DONE Compiled successfully in 1623ms9:42:41 AM - - WAIT Compiling...9:59:53 AM - - DONE Compiled successfully in 1234ms9:59:55 AM - - WAIT Compiling...9:59:55 AM - - DONE Compiled successfully in 1420ms9:59:56 AM - - WAIT Compiling...10:22:54 AM - - ERROR Failed to compile with 2 errors10:22:55 AM - - error in /app/src/components/project/proApiDetail.vue.ts - -(273,7): error TS2304: Cannot find name 'level'. - - error in /app/src/components/project/proApiDetail.vue.ts - -(277,14): error TS2304: Cannot find name 'level'. - - WAIT Compiling...10:22:55 AM - - ERROR Failed to compile with 3 errors10:22:57 AM - - error in /app/src/components/project/proApiDetail.vue.ts - -(273,7): error TS2304: Cannot find name 'level'. - - error in /app/src/components/project/proApiDetail.vue.ts - -(277,14): error TS2304: Cannot find name 'level'. - - error in ./src/components/project/proApiDetail.vue - - - ✘ http://eslint.org/docs/rules/no-undef 'level' is not defined - /app/src/components/project/proApiDetail.vue:273:7 - level = row.level + 1 - ^ - - ✘ http://eslint.org/docs/rules/no-undef 'level' is not defined - /app/src/components/project/proApiDetail.vue:277:14 - level: level, - ^ - - -✘ 2 problems (2 errors, 0 warnings) - - -Errors: - 2 http://eslint.org/docs/rules/no-undef - - @ ./src/router.ts 35:20-64 - @ ./src/main.ts - @ multi ./build/dev-client ./src/main.ts - - WAIT Compiling...10:23:29 AM - - ERROR Failed to compile with 3 errors10:23:31 AM - - error in /app/src/components/project/proApiDetail.vue.ts - -(273,7): error TS2304: Cannot find name 'level'. - - error in /app/src/components/project/proApiDetail.vue.ts - -(277,14): error TS2304: Cannot find name 'level'. - - error in ./src/components/project/proApiDetail.vue - - - ✘ http://eslint.org/docs/rules/no-undef 'level' is not defined - /app/src/components/project/proApiDetail.vue:273:7 - level = row.level + 1 - ^ - - ✘ http://eslint.org/docs/rules/no-undef 'level' is not defined - /app/src/components/project/proApiDetail.vue:277:14 - level: level, - ^ - - -✘ 2 problems (2 errors, 0 warnings) - - -Errors: - 2 http://eslint.org/docs/rules/no-undef - - @ ./src/router.ts 35:20-64 - @ ./src/main.ts - @ multi ./build/dev-client ./src/main.ts - - WAIT Compiling...10:23:31 AM - - DONE Compiled successfully in 1010ms10:23:32 AM - - WAIT Compiling...5:18:15 PM - - DONE Compiled successfully in 1285ms5:18:16 PM - - WAIT Compiling...5:18:16 PM - - DONE Compiled successfully in 1620ms5:18:18 PM - - WAIT Compiling...5:20:53 PM - - DONE Compiled successfully in 176ms5:20:53 PM - - WAIT Compiling...5:20:53 PM - - ERROR Failed to compile with 1 errors5:20:56 PM - - error in /app/src/components/project/proApiList.vue.ts - -(88,18): error TS2339: Property 'refreshApiList' does not exist on type 'proApiList'. - - WAIT Compiling...5:20:56 PM - - ERROR Failed to compile with 1 errors5:20:57 PM - - error in /app/src/components/project/proApiList.vue.ts - -(88,18): error TS2339: Property 'refreshApiList' does not exist on type 'proApiList'. - - WAIT Compiling...5:21:23 PM - - ERROR Failed to compile with 1 errors5:21:24 PM - - error in /app/src/components/project/proApiList.vue.ts - -(88,18): error TS2339: Property 'refreshApiList' does not exist on type 'proApiList'. - - WAIT Compiling...5:21:24 PM - - DONE Compiled successfully in 1805ms5:21:26 PM - - WAIT Compiling...5:48:10 PM - - DONE Compiled successfully in 3879ms5:48:14 PM - - WAIT Compiling...5:48:14 PM - - DONE Compiled successfully in 1824ms5:48:16 PM - - WAIT Compiling...9:03:08 AM - - DONE Compiled successfully in 2009ms9:03:11 AM - - WAIT Compiling...9:03:11 AM - - DONE Compiled successfully in 1845ms9:03:13 AM - - WAIT Compiling...9:32:57 AM - - DONE Compiled successfully in 819ms9:32:58 AM - - WAIT Compiling...9:32:58 AM - - DONE Compiled successfully in 2031ms9:33:00 AM - - WAIT Compiling...4:48:14 PM - - DONE Compiled successfully in 3167ms4:48:17 PM - - WAIT Compiling...4:48:17 PM - - DONE Compiled successfully in 1285ms4:48:19 PM - - WAIT Compiling...9:18:29 AM - - DONE Compiled successfully in 7074ms9:18:36 AM - - WAIT Compiling...9:18:36 AM - - DONE Compiled successfully in 1794ms9:18:38 AM - - WAIT Compiling...2:08:24 PM - - DONE Compiled successfully in 1870ms2:08:27 PM - - WAIT Compiling...2:08:27 PM - - DONE Compiled successfully in 1216ms2:08:28 PM - - WAIT Compiling...4:07:13 PM - - DONE Compiled successfully in 353ms4:07:14 PM - - WAIT Compiling...5:57:10 PM - - DONE Compiled successfully in 3984ms5:57:15 PM - - WAIT Compiling...5:57:15 PM - - DONE Compiled successfully in 1418ms5:57:17 PM - - WAIT Compiling...6:04:00 PM - - DONE Compiled successfully in 392ms6:04:01 PM - - WAIT Compiling...6:04:01 PM - - DONE Compiled successfully in 1158ms6:04:02 PM - - WAIT Compiling...6:04:02 PM - - DONE Compiled successfully in 1269ms6:04:04 PM - - WAIT Compiling...1:49:16 PM - - DONE Compiled successfully in 7953ms1:49:24 PM - - WAIT Compiling...1:49:24 PM - - DONE Compiled successfully in 1138ms1:49:26 PM - - WAIT Compiling...4:42:13 PM - - DONE Compiled successfully in 1099ms4:42:14 PM - - WAIT Compiling...4:42:14 PM - - DONE Compiled successfully in 2236ms4:42:16 PM - - WAIT Compiling...5:04:17 PM - - DONE Compiled successfully in 1138ms5:04:19 PM - - WAIT Compiling...5:04:19 PM - - DONE Compiled successfully in 2025ms5:04:21 PM - - WAIT Compiling...8:57:33 AM - - DONE Compiled successfully in 950ms8:57:34 AM - - WAIT Compiling...8:57:34 AM - - DONE Compiled successfully in 1904ms8:57:36 AM - - WAIT Compiling...1:40:03 PM - - DONE Compiled successfully in 1281ms1:40:04 PM - - WAIT Compiling...1:40:04 PM - - DONE Compiled successfully in 2353ms1:40:07 PM - - WAIT Compiling...1:43:51 PM - - ERROR Failed to compile with 1 errors1:43:52 PM - - error in ./src/components/project/index.vue - -Module build failed: TypeError: Cannot read property 'content' of null - at Object.module.exports (/app/node_modules/vue-loader/lib/selector.js:18:27) - - @ ./src/components/project/index.vue 8:2-102 - @ ./src/router.ts - @ ./src/main.ts - @ multi ./build/dev-client ./src/main.ts - - WAIT Compiling...1:43:52 PM - - DONE Compiled successfully in 2147ms1:43:55 PM - - WAIT Compiling...1:47:07 PM - - DONE Compiled successfully in 885ms1:47:08 PM - - WAIT Compiling...1:47:09 PM - - DONE Compiled successfully in 2022ms1:47:11 PM - - WAIT Compiling...2:04:53 PM - - DONE Compiled successfully in 166ms2:04:54 PM - - WAIT Compiling...2:04:54 PM - - DONE Compiled successfully in 1482ms2:04:55 PM - - WAIT Compiling...2:04:55 PM - - DONE Compiled successfully in 1337ms2:04:57 PM - - WAIT Compiling...2:05:52 PM - - DONE Compiled successfully in 1390ms2:05:54 PM - - WAIT Compiling...2:05:54 PM - - DONE Compiled successfully in 1587ms2:05:56 PM - diff --git a/web/src/components/navBar.vue b/web/src/components/navBar.vue index e4ff54d..800291d 100644 --- a/web/src/components/navBar.vue +++ b/web/src/components/navBar.vue @@ -4,22 +4,22 @@ router-link.menu-item(to='/') img.logo(src="../assets/logo.png") small tiduyun - router-link.menu-item(to='/project/list', active-class='menu-active') 项目 - router-link.menu-item(to='/test', active-class='menu-active') 测试 + router-link.menu-item(v-if='user.name', to='/project/list', active-class='menu-active') 项目 + router-link.menu-item(v-if='user.name', to='/test', active-class='menu-active') 测试 router-link.menu-item(v-if='user.isAdmin', to='/member', active-class='menu-active') 成员管理 - router-link.menu-item(to='/doc', active-class='menu-active') 文档 - router-link.menu-item(to='/set', active-class='menu-active') 设置 - router-link.menu-item(to='/message', active-class='menu-active') 动态 + router-link.menu-item(v-if='user.name', to='/doc', active-class='menu-active') 文档 + router-link.menu-item(v-if='user.name', to='/set', active-class='menu-active') 设置 + router-link.menu-item(v-if='user.name', to='/message', active-class='menu-active') 动态 router-link.menu-item.logout(v-if='!isLogin', to='') - span(v-if='!isLogin', @click='showLoginDialog=true') 登录 + span(v-if='!isLogin&&!isLoginPage', @click='showLoginDialog=true') 登录 el-popover(ref="popover1", placement="top-start", width="100", trigger='hover') router-link.sub-menu-item(to='/user/set', active-class='') 个人设置 span.sub-menu-item(@click='logout()') 退出账号 span.d-ib.f-r.user(v-show='isLogin', v-popover:popover1="") span.c-f {{user.name}} - img.avatar(:src='user.avatar', alt='avatar', :title='user.name') + img.avatar(:src='user.avatar', alt='avatar', :title='user.name') //- span.d-ib.avatar {{user.name || ''}} - el-badge.f-r(:value='10', class='message') + el-badge.f-r(v-if="isLogin", :value='10', class='message') i.fa.fa-bell-o //- el-popover(ref='popover2', title='通知' placement='bottom', width='200', trigger='hover') //- router-link.sub-menu-item(to='/message', active-class='') 查看全部 @@ -39,6 +39,8 @@ import Vue from 'vue' import Component from 'vue-class-component' import Cache from '../service/cache.ts' import http from '../service/http.ts' +import { Watch } from 'vue-property-decorator' + @Component export default class navBar extends Vue { $confirm: any @@ -47,13 +49,16 @@ export default class navBar extends Vue { $refs:any user:any = {} avatarElement:any = 'img' - beforeMount() { + @Watch('$route') + routeChanged(to:any) { let u = JSON.parse(Cache.get('user')) this.user = u || this.user this.isLogin = !!u + this.isLoginPage = to.name === 'login' } showLoginDialog: boolean = false isLogin: boolean = false + isLoginPage: boolean = false userForm: any = { account: '', password: '' @@ -96,7 +101,7 @@ export default class navBar extends Vue { this.isLogin = false this.user = {} this.$message({type: 'success', message: '退出成功'}) - this.$router.push('/') + this.$router.push('/user/login') } else { this.$message({type: 'error', message: res.errMsg || '退出失败'}) } diff --git a/web/src/components/user/login.vue b/web/src/components/user/login.vue index ff308a2..d6e13fc 100644 --- a/web/src/components/user/login.vue +++ b/web/src/components/user/login.vue @@ -1,13 +1,12 @@ @@ -20,6 +15,7 @@ box-sizing border-box html, body + background-color #f9fafc margin 0 #app font-family 'Avenir', Helvetica, Arial, sans-serif @@ -33,4 +29,6 @@ ul li margin 0 list-style none +.wrap + padding 30px diff --git a/web/src/assets/logo.png b/web/src/assets/logo.png deleted file mode 100644 index f3d2503fc2a44b5053b0837ebea6e87a2d339a43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6849 zcmaKRcUV(fvo}bjDT-7nLI_nlK}sT_69H+`qzVWDA|yaU?}j417wLi^B1KB1SLsC& zL0ag7$U(XW5YR7p&Ux?sP$d4lvMt8C^+TcQu4F zQqv!UF!I+kw)c0jhd6+g6oCr9P?7)?!qX1ui*iL{p}sKCAGuJ{{W)0z1pLF|=>h}& zt(2Lr0Z`2ig8<5i%Zk}cO5Fm=LByqGWaS`oqChZdEFmc`0hSb#gg|Aap^{+WKOYcj zHjINK)KDG%&s?Mt4CL(T=?;~U@bU2x_mLKN!#GJuK_CzbNw5SMEJorG!}_5;?R>@1 zSl)jns3WlU7^J%=(hUtfmuUCU&C3%8B5C^f5>W2Cy8jW3#{Od{lF1}|?c61##3dzA zsPlFG;l_FzBK}8>|H_Ru_H#!_7$UH4UKo3lKOA}g1(R&|e@}GINYVzX?q=_WLZCgh z)L|eJMce`D0EIwgRaNETDsr+?vQknSGAi=7H00r`QnI%oQnFxm`G2umXso9l+8*&Q z7WqF|$p49js$mdzo^BXpH#gURy=UO;=IMrYc5?@+sR4y_?d*~0^YP7d+y0{}0)zBM zIKVM(DBvICK#~7N0a+PY6)7;u=dutmNqK3AlsrUU9U`d;msiucB_|8|2kY=(7XA;G zwDA8AR)VCA#JOkxm#6oHNS^YVuOU;8p$N)2{`;oF|rQ?B~K$%rHDxXs+_G zF5|-uqHZvSzq}L;5Kcy_P+x0${33}Ofb6+TX&=y;;PkEOpz%+_bCw_{<&~ zeLV|!bP%l1qxywfVr9Z9JI+++EO^x>ZuCK);=$VIG1`kxK8F2M8AdC$iOe3cj1fo(ce4l-9 z7*zKy3={MixvUk=enQE;ED~7tv%qh&3lR<0m??@w{ILF|e#QOyPkFYK!&Up7xWNtL zOW%1QMC<3o;G9_S1;NkPB6bqbCOjeztEc6TsBM<(q9((JKiH{01+Ud=uw9B@{;(JJ z-DxI2*{pMq`q1RQc;V8@gYAY44Z!%#W~M9pRxI(R?SJ7sy7em=Z5DbuDlr@*q|25V)($-f}9c#?D%dU^RS<(wz?{P zFFHtCab*!rl(~j@0(Nadvwg8q|4!}L^>d?0al6}Rrv9$0M#^&@zjbfJy_n!%mVHK4 z6pLRIQ^Uq~dnyy$`ay51Us6WaP%&O;@49m&{G3z7xV3dLtt1VTOMYl3UW~Rm{Eq4m zF?Zl_v;?7EFx1_+#WFUXxcK78IV)FO>42@cm@}2I%pVbZqQ}3;p;sDIm&knay03a^ zn$5}Q$G!@fTwD$e(x-~aWP0h+4NRz$KlnO_H2c< z(XX#lPuW_%H#Q+c&(nRyX1-IadKR-%$4FYC0fsCmL9ky3 zKpxyjd^JFR+vg2!=HWf}2Z?@Td`0EG`kU?{8zKrvtsm)|7>pPk9nu@2^z96aU2<#` z2QhvH5w&V;wER?mopu+nqu*n8p~(%QkwSs&*0eJwa zMXR05`OSFpfyRb!Y_+H@O%Y z0=K^y6B8Gcbl?SA)qMP3Z+=C(?8zL@=74R=EVnE?vY!1BQy2@q*RUgRx4yJ$k}MnL zs!?74QciNb-LcG*&o<9=DSL>1n}ZNd)w1z3-0Pd^4ED1{qd=9|!!N?xnXjM!EuylY z5=!H>&hSofh8V?Jofyd!h`xDI1fYAuV(sZwwN~{$a}MX^=+0TH*SFp$vyxmUv7C*W zv^3Gl0+eTFgBi3FVD;$nhcp)ka*4gSskYIqQ&+M}xP9yLAkWzBI^I%zR^l1e?bW_6 zIn{mo{dD=)9@V?s^fa55jh78rP*Ze<3`tRCN4*mpO$@7a^*2B*7N_|A(Ve2VB|)_o z$=#_=aBkhe(ifX}MLT()@5?OV+~7cXC3r!%{QJxriXo9I%*3q4KT4Xxzyd{ z9;_%=W%q!Vw$Z7F3lUnY+1HZ*lO;4;VR2+i4+D(m#01OYq|L_fbnT;KN<^dkkCwtd zF7n+O7KvAw8c`JUh6LmeIrk4`F3o|AagKSMK3))_5Cv~y2Bb2!Ibg9BO7Vkz?pAYX zoI=B}+$R22&IL`NCYUYjrdhwjnMx_v=-Qcx-jmtN>!Zqf|n1^SWrHy zK|MwJ?Z#^>)rfT5YSY{qjZ&`Fjd;^vv&gF-Yj6$9-Dy$<6zeP4s+78gS2|t%Z309b z0^fp~ue_}i`U9j!<|qF92_3oB09NqgAoehQ`)<)dSfKoJl_A6Ec#*Mx9Cpd-p#$Ez z={AM*r-bQs6*z$!*VA4|QE7bf@-4vb?Q+pPKLkY2{yKsw{&udv_2v8{Dbd zm~8VAv!G~s)`O3|Q6vFUV%8%+?ZSVUa(;fhPNg#vab@J*9XE4#D%)$UU-T5`fwjz! z6&gA^`OGu6aUk{l*h9eB?opVdrHK>Q@U>&JQ_2pR%}TyOXGq_6s56_`U(WoOaAb+K zXQr#6H}>a-GYs9^bGP2Y&hSP5gEtW+GVC4=wy0wQk=~%CSXj=GH6q z-T#s!BV`xZVxm{~jr_ezYRpqqIcXC=Oq`b{lu`Rt(IYr4B91hhVC?yg{ol4WUr3v9 zOAk2LG>CIECZ-WIs0$N}F#eoIUEtZudc7DPYIjzGqDLWk_A4#(LgacooD z2K4IWs@N`Bddm-{%oy}!k0^i6Yh)uJ1S*90>|bm3TOZxcV|ywHUb(+CeX-o1|LTZM zwU>dY3R&U)T(}5#Neh?-CWT~@{6Ke@sI)uSuzoah8COy)w)B)aslJmp`WUcjdia-0 zl2Y}&L~XfA`uYQboAJ1;J{XLhYjH){cObH3FDva+^8ioOQy%Z=xyjGLmWMrzfFoH; zEi3AG`_v+%)&lDJE;iJWJDI@-X9K5O)LD~j*PBe(wu+|%ar~C+LK1+-+lK=t# z+Xc+J7qp~5q=B~rD!x78)?1+KUIbYr^5rcl&tB-cTtj+e%{gpZZ4G~6r15+d|J(ky zjg@@UzMW0k9@S#W(1H{u;Nq(7llJbq;;4t$awM;l&(2s+$l!Ay9^Ge|34CVhr7|BG z?dAR83smef^frq9V(OH+a+ki#q&-7TkWfFM=5bsGbU(8mC;>QTCWL5ydz9s6k@?+V zcjiH`VI=59P-(-DWXZ~5DH>B^_H~;4$)KUhnmGo*G!Tq8^LjfUDO)lASN*=#AY_yS zqW9UX(VOCO&p@kHdUUgsBO0KhXxn1sprK5h8}+>IhX(nSXZKwlNsjk^M|RAaqmCZB zHBolOHYBas@&{PT=R+?d8pZu zUHfyucQ`(umXSW7o?HQ3H21M`ZJal+%*)SH1B1j6rxTlG3hx1IGJN^M7{$j(9V;MZ zRKybgVuxKo#XVM+?*yTy{W+XHaU5Jbt-UG33x{u(N-2wmw;zzPH&4DE103HV@ER86 z|FZEmQb|&1s5#`$4!Cm}&`^{(4V}OP$bk`}v6q6rm;P!H)W|2i^e{7lTk2W@jo_9q z*aw|U7#+g59Fv(5qI`#O-qPj#@_P>PC#I(GSp3DLv7x-dmYK=C7lPF8a)bxb=@)B1 zUZ`EqpXV2dR}B&r`uM}N(TS99ZT0UB%IN|0H%DcVO#T%L_chrgn#m6%x4KE*IMfjX zJ%4veCEqbXZ`H`F_+fELMC@wuy_ch%t*+Z+1I}wN#C+dRrf2X{1C8=yZ_%Pt6wL_~ zZ2NN-hXOT4P4n$QFO7yYHS-4wF1Xfr-meG9Pn;uK51?hfel`d38k{W)F*|gJLT2#T z<~>spMu4(mul-8Q3*pf=N4DcI)zzjqAgbE2eOT7~&f1W3VsdD44Ffe;3mJp-V@8UC z)|qnPc12o~$X-+U@L_lWqv-RtvB~%hLF($%Ew5w>^NR82qC_0FB z)=hP1-OEx?lLi#jnLzH}a;Nvr@JDO-zQWd}#k^an$Kwml;MrD&)sC5b`s0ZkVyPkb zt}-jOq^%_9>YZe7Y}PhW{a)c39G`kg(P4@kxjcYfgB4XOOcmezdUI7j-!gs7oAo2o zx(Ph{G+YZ`a%~kzK!HTAA5NXE-7vOFRr5oqY$rH>WI6SFvWmahFav!CfRMM3%8J&c z*p+%|-fNS_@QrFr(at!JY9jCg9F-%5{nb5Bo~z@Y9m&SHYV`49GAJjA5h~h4(G!Se zZmK{Bo7ivCfvl}@A-ptkFGcWXAzj3xfl{evi-OG(TaCn1FAHxRc{}B|x+Ua1D=I6M z!C^ZIvK6aS_c&(=OQDZfm>O`Nxsw{ta&yiYPA~@e#c%N>>#rq)k6Aru-qD4(D^v)y z*>Rs;YUbD1S8^D(ps6Jbj0K3wJw>L4m)0e(6Pee3Y?gy9i0^bZO?$*sv+xKV?WBlh zAp*;v6w!a8;A7sLB*g-^<$Z4L7|5jXxxP1}hQZ<55f9<^KJ>^mKlWSGaLcO0=$jem zWyZkRwe~u{{tU63DlCaS9$Y4CP4f?+wwa(&1ou)b>72ydrFvm`Rj-0`kBJgK@nd(*Eh!(NC{F-@=FnF&Y!q`7){YsLLHf0_B6aHc# z>WIuHTyJwIH{BJ4)2RtEauC7Yq7Cytc|S)4^*t8Va3HR zg=~sN^tp9re@w=GTx$;zOWMjcg-7X3Wk^N$n;&Kf1RgVG2}2L-(0o)54C509C&77i zrjSi{X*WV=%C17((N^6R4Ya*4#6s_L99RtQ>m(%#nQ#wrRC8Y%yxkH;d!MdY+Tw@r zjpSnK`;C-U{ATcgaxoEpP0Gf+tx);buOMlK=01D|J+ROu37qc*rD(w`#O=3*O*w9?biwNoq3WN1`&Wp8TvKj3C z3HR9ssH7a&Vr<6waJrU zdLg!ieYz%U^bmpn%;(V%%ugMk92&?_XX1K@mwnVSE6!&%P%Wdi7_h`CpScvspMx?N zQUR>oadnG17#hNc$pkTp+9lW+MBKHRZ~74XWUryd)4yd zj98$%XmIL4(9OnoeO5Fnyn&fpQ9b0h4e6EHHw*l68j;>(ya`g^S&y2{O8U>1*>4zR zq*WSI_2o$CHQ?x0!wl9bpx|Cm2+kFMR)oMud1%n2=qn5nE&t@Fgr#=Zv2?}wtEz^T z9rrj=?IH*qI5{G@Rn&}^Z{+TW}mQeb9=8b<_a`&Cm#n%n~ zU47MvCBsdXFB1+adOO)03+nczfWa#vwk#r{o{dF)QWya9v2nv43Zp3%Ps}($lA02*_g25t;|T{A5snSY?3A zrRQ~(Ygh_ebltHo1VCbJb*eOAr;4cnlXLvI>*$-#AVsGg6B1r7@;g^L zFlJ_th0vxO7;-opU@WAFe;<}?!2q?RBrFK5U{*ai@NLKZ^};Ul}beukveh?TQn;$%9=R+DX07m82gP$=}Uo_%&ngV`}Hyv8g{u z3SWzTGV|cwQuFIs7ZDOqO_fGf8Q`8MwL}eUp>q?4eqCmOTcwQuXtQckPy|4F1on8l zP*h>d+cH#XQf|+6c|S{7SF(Lg>bR~l(0uY?O{OEVlaxa5@e%T&xju=o1`=OD#qc16 zSvyH*my(dcp6~VqR;o(#@m44Lug@~_qw+HA=mS#Z^4reBy8iV?H~I;{LQWk3aKK8$bLRyt$g?- - div.hello - h1 aaa - h1 {{msg}} - h2 {{name}} - - - - - - diff --git a/web/src/components/Index.vue b/web/src/components/Index.vue deleted file mode 100644 index 5a24cc1..0000000 --- a/web/src/components/Index.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/web/src/components/LoginForm.vue b/web/src/components/LoginForm.vue new file mode 100644 index 0000000..0773553 --- /dev/null +++ b/web/src/components/LoginForm.vue @@ -0,0 +1,60 @@ + + + diff --git a/web/src/components/Main.vue b/web/src/components/Main.vue new file mode 100644 index 0000000..b95d560 --- /dev/null +++ b/web/src/components/Main.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/web/src/components/home/Index.vue b/web/src/components/home/Index.vue new file mode 100644 index 0000000..b7a9302 --- /dev/null +++ b/web/src/components/home/Index.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/index.vue b/web/src/components/index.vue deleted file mode 100644 index 5a24cc1..0000000 --- a/web/src/components/index.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/web/src/components/navBar.vue b/web/src/components/navBar.vue index 800291d..e699de8 100644 --- a/web/src/components/navBar.vue +++ b/web/src/components/navBar.vue @@ -1,37 +1,23 @@ - diff --git a/web/src/components/project/proAdd.vue b/web/src/components/project/proAdd.vue index 44edb81..d3538b1 100644 --- a/web/src/components/project/proAdd.vue +++ b/web/src/components/project/proAdd.vue @@ -59,9 +59,10 @@ diff --git a/web/src/main.ts b/web/src/main.ts index e132fe9..b3396da 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,14 +1,15 @@ // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' -import App from './App' -import router from './service/router.ts' +import Router from 'vue-router' +import App from './App.vue' +import router from './service/router' import ElementUI from 'element-ui' import 'element-ui/lib/theme-default/index.css' -Vue.config.productionTip = false -// Vue.use(iview) +Vue.config.productionTip = false Vue.use(ElementUI) +Vue.use(Router) /* eslint-disable no-new */ new Vue({ el: '#app', diff --git a/web/src/service/axiosPlugin.ts b/web/src/service/axiosPlugin.ts deleted file mode 100644 index 54909ff..0000000 --- a/web/src/service/axiosPlugin.ts +++ /dev/null @@ -1,58 +0,0 @@ -import axios from 'axios' -import qs from 'qs' -import { Message } from 'element-ui' -import router from './router.ts' - -const Axios:any = axios.create({ - baseURL: '', - timeout: 10000, - responseType: 'json', - withCredentials: true, - headers: { - "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" - } -}) -Axios.interceptors.request.use( - (config:any) => { - let m:any = config.method - if (m === 'post' || m === 'put' || m === 'delete') { - config.data = qs.stringify(config.data) - } - if (localStorage.token) { - config.headers.Authorization = localStorage.token - } - return config - },(error:any) => { - Message({type: 'error', message: error.data.error.message}) - return Promise.reject(error.data.error.message) - } -) -Axios.interceptors.response.use( - (resp:any) => { - if (resp.data && !resp.data.errCode) { - Message({type: 'error', message: resp.data.errMsg || '操作失败'}) - return Promise.reject(resp.data.errMsg) - } - return resp - }, - (error:any) => { - if (!localStorage.getItem('token')) { - router.push('/user/login') - } else { - // let lifeTime:any = JSON.parse(localStorage.getItem) - } - if (error.response.status === 403) { - Message({type: 'error', message: '当前登录信息失效, 请重新登录'}) - router.push('/user/login') - } else if (error.response.status === 404) { - router.push('/error/404') - } - return Promise.reject(error.data.errMsg) - } -) - -export default { - install: (Vue:any, Option:any) => { - Object.defineProperty(Vue.prototype, '$http', { value: Axios}) - } -} diff --git a/web/src/service/cache.ts b/web/src/service/cache.ts index 89aaf99..382ff84 100644 --- a/web/src/service/cache.ts +++ b/web/src/service/cache.ts @@ -9,5 +9,5 @@ export default { remove: (key: string) => { return localStorage.removeItem(key) }, - clear: localStorage.clear + clear: () => localStorage.clear() } diff --git a/web/src/service/http.ts b/web/src/service/http.ts index 40563e2..05287c9 100644 --- a/web/src/service/http.ts +++ b/web/src/service/http.ts @@ -14,7 +14,7 @@ let request = (method: string, url: string, data: any, config: any = {}) => new const { data, status, statusText } = e.response if (status === 401) { Cache.clear() - router.push({ path: '/user/login' }) + pleaseLogin() } else if (status === 403) { MessageBox.alert(data) } else { @@ -23,7 +23,12 @@ let request = (method: string, url: string, data: any, config: any = {}) => new } }) +let pleaseLogin = () => {} + export default { + initLogin(showLogin:any) { + pleaseLogin = showLogin + }, get: (url: string, config?: object) => request('GET', url, null, config), put: (url: string, data?: object, config?: object) => request('PUT', url, data, config), post: (url: string, data?: object, config?: object) => request('POST', url, data, config), diff --git a/web/src/service/router.ts b/web/src/service/router.ts index cdb20bb..9ca4ae5 100644 --- a/web/src/service/router.ts +++ b/web/src/service/router.ts @@ -1,160 +1,170 @@ import Vue from 'vue' import Router from 'vue-router' -import index from '../components/index' -import project from '../components/project/index' -import proAdd from '../components/project/proAdd' -import proList from '../components/project/proList' -import apiList from '../components/project/apiList' -import apiView from '../components/project/apiView' -import apiEdit from '../components/project/apiEdit' -import apiHistory from '../components/project/apiHistory' -import test from '../components/test' -import message from '../components/message' -import member from '../components/member' -import doc from '../components/doc' -import set from '../components/set' -import userSet from '../components/user/set' -import login from '../components/user/login' +import Index from '../components/home/Index.vue' +import Main from '../components/Main.vue' +import proAdd from '../components/project/proAdd.vue' +import proList from '../components/project/proList.vue' +import apiList from '../components/project/apiList.vue' +import apiView from '../components/project/apiView.vue' +import apiEdit from '../components/project/apiEdit.vue' +import apiHistory from '../components/project/apiHistory.vue' +import test from '../components/test.vue' +import message from '../components/message.vue' +import member from '../components/member.vue' +import doc from '../components/doc.vue' +import set from '../components/set.vue' +import Profile from '../components/user/Profile.vue' +import Login from '../components/user/Login.vue' -Vue.use(Router) +const projectRouter = [ + { + path: 'project/list', + name: 'projectIndex', + component: proList, + meta: { + requireLogin: true + } + }, + { + path: 'project/add', + name: 'proAdd', + component: proAdd, + meta: { + requireLogin: true + } + }, + { + path: 'project/:proId/edit', + name: 'proEdit', + component: proAdd, + meta: { + requireLogin: true + } + }, + { + path: 'project/:proId/api', + name: 'proApiList', + component: apiList, + children: [ + { + path: 'add', + name: 'apiAdd', + component: apiEdit, + meta: { + requireLogin: true + } + }, + { + path: ':apiId/detail', + name: 'apiView', + component: apiView, + meta: { + requireLogin: true + }, + }, + { + path: ':apiId/edit', + name: 'apiEdit', + component: apiEdit, + meta: { + requireLogin: true + } + }, + { + path: ':apiId/history', + name: 'apiHistory', + component: apiHistory, + meta: { + requireLogin: true + } + } + ] + } +] +const testRouter = [{ + path: '/test', + name: 'testIndex', + component: test, + meta: { + requireLogin: true + } +}] +const messageRouter = [{ + path: '/message', + name: 'messageIndex', + component: message, + meta: { + requireLogin: true + } +}] +const memberRouter = [{ + path: '/member', + name: 'memberIndex', + component: member, + meta: { + requireLogin: true + } +}] +const docRouter = [{ + path: '/doc', + name: 'docIndex', + component: doc, + meta: { + requireLogin: false + } +}] +const setRouter = [{ + path: '/set', + name: 'setIndex', + component: set, + meta: { + requireLogin: true + } +}] +const userRouter = [{ + path: '/user/profile', + name: 'userProfile', + component: Profile, + meta: { + requireLogin: true + } +}] -const router:any = new Router({ +const router: any = new Router({ mode: 'history', routes: [ { path: '/', - name: 'index', - component: index - }, - { - path: '/project', - name: 'project', - component: project, - children: [ - { - path: 'list', - name: 'proList', - component: proList, - meta: { - requireLogin: true - } - }, - { - path: 'add', - name: 'proAdd', - component: proAdd, - meta: { - requireLogin: true - } - }, - { - path: ':proId/edit', - name: 'proEdit', - component: proAdd, - meta: { - requireLogin: true - } - }, - { - path: ':proId/api', - name: 'proApiList', - component: apiList, - children: [ - { - path: 'add', - name: 'apiAdd', - component: apiEdit, - meta: { - requireLogin: true - } - }, - { - path: ':apiId/detail', - name: 'apiView', - component: apiView, - meta: { - requireLogin: true - }, - }, - { - path: ':apiId/edit', - name: 'apiEdit', - component: apiEdit, - meta: { - requireLogin: true - } - }, - { - path: ':apiId/history', - name: 'apiHistory', - component: apiHistory, - meta: { - requireLogin: true - } - } - ] - } - ] - }, - { - path: '/test', - name: 'test', - component: test, - meta: { - requireLogin: true - } - }, - { - path: '/message', - name: 'message', - component: message, - meta: { - requireLogin: true - } - }, - { - path: '/member', - name: 'member', - component: member, - meta: { - requireLogin: true - } + name: 'home', + component: Index }, { - path: '/doc', - name: 'doc', - component: doc - }, - { - path: '/set', - name: 'set', - component: set, - meta: { - requireLogin: true - } - }, - { - path: '/user/set', - name: 'userSet', - component: userSet, - meta: { - requireLogin: true - } + path: '/login', + name: 'login', + component: Login }, { - path: '/user/login', - name: 'login', - component: login + path: '', + name: 'main', + component: Main, + children: [ + ...projectRouter, + ...testRouter, + ...docRouter, + ...userRouter, + ...setRouter, + ...messageRouter + ] } ] }) -router.beforeEach((to:any, from:any, next:any) => { - if (to.matched.some((res:any) => res.meta.requireLogin)) { +router.beforeEach((to: any, from: any, next: any) => { + if (to.matched.some((res: any) => res.meta.requireLogin)) { if (localStorage.getItem('token')) { next() } else { - next('/user/login') + next({ name: 'login' }) } } else { next() diff --git a/web/src/utils/util.ts b/web/src/service/util.ts similarity index 97% rename from web/src/utils/util.ts rename to web/src/service/util.ts index bb19b29..2fef398 100644 --- a/web/src/utils/util.ts +++ b/web/src/service/util.ts @@ -1,3 +1,5 @@ +import {Tree} from '../service/interface' + let gId:Function = (size?: number) => { let t = (new Date()).getTime() let p = (Math.random().toString(16) + '00000000').substr(2,8) @@ -14,7 +16,6 @@ let gVersion:Function = () => { let s = t.getSeconds() return y + '' + (m < 10 ? '0' : '') + m + (d < 10 ? '0' : '') + d + '' + '.' + (h < 10 ? '0' : '') + h + (mi < 10 ? '0' : '') + mi + (s < 10 ? '0' : '') + s + '.' + (Math.random().toString(16) + '00000000').substr(2,4) } -import {Tree} from '../service/interface.ts' function formatApiToTree (apiList:any[]): Tree[] { let modules:any = [] let urls:any = [] diff --git a/web/src/vue-shimes.d.ts b/web/src/vue-shimes.d.ts index 1a27118..3667cb9 100644 --- a/web/src/vue-shimes.d.ts +++ b/web/src/vue-shimes.d.ts @@ -2,10 +2,11 @@ declare module '*.vue' { import Vue from 'vue' export default Vue } -declare module 'service' { - import axios from 'axios' - export default axios -} +// declare module 'service' { +// import axios from 'axios' +// export default axios +// } declare module 'element-ui' declare module 'mockjs' -declare module 'qs' +declare module 'axios' +declare module 'vue-router' \ No newline at end of file From e2980b71c044160ae66c54420cd38f4236b52624 Mon Sep 17 00:00:00 2001 From: Delong Zhu Date: Wed, 1 Nov 2017 23:17:28 +0800 Subject: [PATCH 03/42] fix navbar --- server/src/member/MemberCtrl.ts | 2 +- server/template/template.body.html | 20 +++++------ web/src/components/navBar.vue | 46 +++++++++++++++----------- web/src/components/project/proList.vue | 2 +- web/src/service/router.ts | 3 +- 5 files changed, 38 insertions(+), 35 deletions(-) diff --git a/server/src/member/MemberCtrl.ts b/server/src/member/MemberCtrl.ts index b07b989..8abc94e 100644 --- a/server/src/member/MemberCtrl.ts +++ b/server/src/member/MemberCtrl.ts @@ -12,7 +12,7 @@ export default class MemberCtrl extends BaseCtrl { let str = avatar.replace('TEXT', id.substring(0,1)).replace('COLOR', '#'+Math.random().toString(16).substring(2, 8)) let filename = path.join(staticPath, id+'.svg') fs.writeFileSync(filename, str) - return path.join('api', filename) + return path.join('/api', filename) } resetPassword(payload:any, user:any, id:string) { if(user && user.isAdmin) { diff --git a/server/template/template.body.html b/server/template/template.body.html index 09848d7..710758d 100644 --- a/server/template/template.body.html +++ b/server/template/template.body.html @@ -13,7 +13,6 @@

参数 类型 说明 - 校验 <% requestParams.forEach(it => { %> @@ -26,14 +25,13 @@

<%= it.desc %> - - <%= it.rule %> - <% }) %> -

请求示例

-
<%=requestExample%>
+
> +

请求示例

+
<%=requestExample%>
+

0 ? '' : 'style="display:none"'%> >

返回参数

@@ -42,7 +40,6 @@

参数 类型 说明 - 校验 <% responseParams.forEach(it => { %> @@ -55,14 +52,13 @@

<%=it.desc%> - - <%=it.rule%> - <% }) %> -

返回示例

-
<%=responseExample%>
+
+

返回示例

+
<%=responseExample%>
+

0 ? '' : 'style="display:none"'%> >

异常处理

diff --git a/web/src/components/navBar.vue b/web/src/components/navBar.vue index e699de8..8f56c51 100644 --- a/web/src/components/navBar.vue +++ b/web/src/components/navBar.vue @@ -2,29 +2,27 @@ div.nav-bar-wrap.p-a.t-0.r-0.l-0.h-40 div.nav-bar.ta-l.p-r div.d-ib.company - img.logo(src="") - small company name + img.logo(:src="logo") + small {{company}} router-link.menu-item(v-for="item in menu", :key="item.routeName", v-if='!item.needAdmin||user.isAdmin', :to='{name:item.routeName}', active-class='menu-active', v-text="item.name") el-popover(ref="popover1", placement="top-start", width="100", trigger='hover') - router-link.sub-menu-item(:to="{name: 'userProfile'}") 个人设置 - span.sub-menu-item(@click='logout()') 退出账号 + router-link.ta-c.sub-menu-item(:to="{name: 'userProfile'}") 个人设置 + span.ta-c.sub-menu-item(@click='logout()') 退出账号 span.d-ib.f-r.user(v-show='user.name', v-popover:popover1="") span.c-f.username {{user.name}} img.avatar(:src='user.avatar', alt='avatar', :title='user.name') - router-link(:to="{name:'messageIndex'}") - el-badge.f-r.message(v-if="user.name", :value='10') - i.fa.fa-bell-o.va-m - el-popover(ref='popover2', title='通知' placement='bottom', width='200', trigger='hover') - router-link.sub-menu-item(to='/message', active-class='') 查看全部 - el-dialog(size="tiny", :visible.sync='showLoginDialog', :show-close="false", :close-on-click-modal="false", :close-on-press-escape="false") - LoginForm(@success="hideDialog") + // router-link(:to="{name:'messageIndex'}") + // el-badge.f-r.message(v-if="user.name", :value='10') + // i.fa.fa-bell-o.va-m + // el-popover(ref='popover2', title='通知' placement='bottom', width='200', trigger='hover') + // router-link.sub-menu-item(to='/message', active-class='') 查看全部 + diff --git a/web/src/components/project/apiList.vue b/web/src/components/project/apiList.vue index 73f1275..a80c3b3 100644 --- a/web/src/components/project/apiList.vue +++ b/web/src/components/project/apiList.vue @@ -1,29 +1,31 @@ diff --git a/web/src/components/project/Api.vue b/web/src/components/project/Api.vue deleted file mode 100644 index 79fa78c..0000000 --- a/web/src/components/project/Api.vue +++ /dev/null @@ -1,63 +0,0 @@ - - - diff --git a/web/src/components/project/ApiIndex.vue b/web/src/components/project/ApiIndex.vue new file mode 100644 index 0000000..1346a69 --- /dev/null +++ b/web/src/components/project/ApiIndex.vue @@ -0,0 +1,84 @@ + + + diff --git a/web/src/components/project/apiEdit.vue b/web/src/components/project/apiEdit.vue index 1cc0c1c..a811b86 100644 --- a/web/src/components/project/apiEdit.vue +++ b/web/src/components/project/apiEdit.vue @@ -1,5 +1,5 @@ diff --git a/web/src/service/EventDelegate.ts b/web/src/service/EventDelegate.ts new file mode 100644 index 0000000..f1e9418 --- /dev/null +++ b/web/src/service/EventDelegate.ts @@ -0,0 +1,37 @@ +interface eventCallback { + eventName: string + componentId: string + callback(event?: any): void +} + +let bindList: eventCallback[] = [] +let eventList: string[] = ['mousemove', 'mouseup'] +eventList.forEach((e: string) => { + if (document.addEventListener) { + document.addEventListener(e, (event: any) => { + bindList.forEach((b: eventCallback) => { + if (e === b.eventName) b.callback(event) + }) + }) + } else { + console.error('浏览器版本过低,请升级浏览器') + } +}) +export default class EventDelegate { + static bind(eventName: string, callback: any, componentId: string) { + bindList.push({ + eventName, + componentId, + callback + }) + } + static unbind(eName: string, cId: string) { + for(let i=bindList.length-1; i>=0; i--){ + let {eventName, componentId} = bindList[i] + if(cId === componentId && eName === eventName) { + bindList.splice(i, 1) + break + } + } + } +} \ No newline at end of file diff --git a/web/src/service/http.ts b/web/src/service/http.ts index 54b48dd..4caecbc 100644 --- a/web/src/service/http.ts +++ b/web/src/service/http.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { MessageBox } from 'element-ui' +import { Message } from 'element-ui' import Cache from './cache' import router from './router' @@ -20,9 +20,9 @@ let request = (method: string, url: string, data: any, config: any = {}) => new Cache.clear() pleaseLogin() } else if (status === 403) { - MessageBox.alert(data) + Message({message: data, type: 'error'}) } else { - MessageBox.alert(status + ',' + statusText) + Message({message: status + ',' + statusText, type: 'error'}) } } }) diff --git a/web/src/service/router.ts b/web/src/service/router.ts index bd4b892..8ee6b89 100644 --- a/web/src/service/router.ts +++ b/web/src/service/router.ts @@ -5,9 +5,6 @@ import Main from '../components/Main.vue' import proAdd from '../components/project/proAdd.vue' import proList from '../components/project/proList.vue' import apiList from '../components/project/apiList.vue' -import apiView from '../components/project/apiView.vue' -import apiEdit from '../components/project/apiEdit.vue' -import apiHistory from '../components/project/apiHistory.vue' import test from '../components/test.vue' import message from '../components/message.vue' import member from '../components/member.vue' @@ -15,7 +12,7 @@ import doc from '../components/doc.vue' import set from '../components/set.vue' import Profile from '../components/user/Profile.vue' import Login from '../components/user/Login.vue' -import Api from '../components/project/Api.vue' +import ApiIndex from '../components/project/ApiIndex.vue' const projectRouter = [ { @@ -45,41 +42,7 @@ const projectRouter = [ { path: 'project/:proId/api', name: 'api', - component: Api, - children: [ - { - path: 'add', - name: 'apiAdd', - component: apiEdit, - meta: { - requireLogin: true - } - }, - { - path: ':apiId/detail', - name: 'apiView', - component: apiView, - meta: { - requireLogin: true - }, - }, - { - path: ':apiId/edit', - name: 'apiEdit', - component: apiEdit, - meta: { - requireLogin: true - } - }, - { - path: ':apiId/history', - name: 'apiHistory', - component: apiHistory, - meta: { - requireLogin: true - } - } - ] + component: ApiIndex } ] const testRouter = [{ diff --git a/web/src/vue-shimes.d.ts b/web/src/vue-shimes.d.ts index 3667cb9..5ab32dd 100644 --- a/web/src/vue-shimes.d.ts +++ b/web/src/vue-shimes.d.ts @@ -9,4 +9,5 @@ declare module '*.vue' { declare module 'element-ui' declare module 'mockjs' declare module 'axios' -declare module 'vue-router' \ No newline at end of file +declare module 'vue-router' +declare module 'hotkeys-js' \ No newline at end of file From 9f9558bb0fcd7182689fffebd4b3e5d09f539624 Mon Sep 17 00:00:00 2001 From: delong zhu Date: Mon, 6 Nov 2017 17:45:52 +0800 Subject: [PATCH 08/42] fix api bug --- web/src/common.styl | 4 + web/src/components/project/ApiIndex.vue | 26 ++++- web/src/components/project/apiEdit.vue | 3 +- web/src/components/project/apiList.vue | 44 ++++---- web/src/components/project/apiView.vue | 142 +++++++++++------------- web/src/vue-shimes.d.ts | 3 +- 6 files changed, 117 insertions(+), 105 deletions(-) diff --git a/web/src/common.styl b/web/src/common.styl index a45f287..f9929dc 100644 --- a/web/src/common.styl +++ b/web/src/common.styl @@ -7,6 +7,8 @@ $borderColorTable=#dfe6ec color #F7BA2A .c-green color #13CE66 +.bg-white + background-color #fff .bg-yellow background-color yellow .bg-green @@ -125,3 +127,5 @@ $borderColorTable=#dfe6ec user-select none .d-f display flex +.fd-c + flex-direction column diff --git a/web/src/components/project/ApiIndex.vue b/web/src/components/project/ApiIndex.vue index 1346a69..4319db3 100644 --- a/web/src/components/project/ApiIndex.vue +++ b/web/src/components/project/ApiIndex.vue @@ -1,8 +1,14 @@ diff --git a/web/src/components/project/apiEdit.vue b/web/src/components/project/apiEdit.vue index a811b86..315cc32 100644 --- a/web/src/components/project/apiEdit.vue +++ b/web/src/components/project/apiEdit.vue @@ -1,5 +1,5 @@ @@ -28,6 +28,8 @@ export default class apiList extends Vue { proId: string @Prop() apiId: string + @Prop() + clickedId: string Mock: any $refs: any $message: any @@ -36,11 +38,11 @@ export default class apiList extends Vue { proName: string apiList: any[] = [] showTree: boolean = true - visibleWidth: number = 250 + visibleWidth: number = 200 startX: number = 0 startWidth: number treeStyle: any = { - width: '250px' + width: '200px' } mousedown(e:any) { this.startX = e.clientX @@ -51,7 +53,6 @@ export default class apiList extends Vue { }, 'ApiList') } drag(e:any) { - console.log(e) let moveX:any = e.clientX - this.startX let width = '0px' if (moveX > 0) { @@ -163,6 +164,22 @@ export default class apiList extends Vue { children: 'children', label: 'name' } + get expandedKeys() { + let keys: string[] = [] + this.apiList.forEach((api:any) => { + api.children = api.children || [] + api.children.forEach((ifc:any) => { + if (ifc.id === this.clickedId) { + keys = [api.id, this.clickedId] + setTimeout(() => { + let dom: any = document.querySelector('.api-tree [title="' + ifc.name + '"]') + dom ? dom.click() : void 0 + }, 0) + } + }) + }) + return keys + } filterText: string = '' @Watch('filterText') onFilterTextChanged(val: string, oldVal: string) { @@ -176,7 +193,8 @@ export default class apiList extends Vue { if (!data.operation) { this.$emit('view', data.id, data.name, data.label) } - delete data.operation + // debounce + setTimeout(() => delete data.operation, 0) } async addModule() { let {value} = await this.$prompt('请输入模块名', '提示', {confirmButtonText: '确定', cancelButtonText: '取消'}) @@ -275,22 +293,10 @@ export default class apiList extends Vue { .api-search padding 5px .api-tree - width 250px + width 100% .el-tree border-width 0 padding-bottom 1px -.api-detail-wrap - height 100% - padding-top 40px - background-color #fff -.operation-btns - z-index 100 - top 0 - left 0 - right 0 - padding-left 40px - line-height 40px - background-color #eee .btn-add-child-param top 50% transform translateY(-50%) diff --git a/web/src/components/project/apiView.vue b/web/src/components/project/apiView.vue index c87e132..4cddbc0 100644 --- a/web/src/components/project/apiView.vue +++ b/web/src/components/project/apiView.vue @@ -1,83 +1,63 @@ diff --git a/web/src/components/project/ApiIndex.vue b/web/src/components/project/ApiIndex.vue index 8ae2f0b..4c34a9c 100644 --- a/web/src/components/project/ApiIndex.vue +++ b/web/src/components/project/ApiIndex.vue @@ -1,7 +1,7 @@