├── .eslintignore ├── .eslintrc ├── app ├── public │ ├── robots.txt │ ├── console │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── img │ │ │ ├── logo.2351b81a.png │ │ │ ├── logo-icon.cccdfcab.png │ │ │ ├── wxpay_qr_code.8e266050.png │ │ │ └── alipay_qr_code.6603dbde.png │ │ ├── fonts │ │ │ └── codicon.a609dc0f.ttf │ │ ├── css │ │ │ └── 404.54ae3b1e.css │ │ ├── js │ │ │ ├── 404.d57c49ac.js │ │ │ ├── chunk-2d0c512b.79c6b45a.js │ │ │ └── chunk-97a1a47a.e0b6e189.js │ │ └── index.html │ ├── favicon.ico │ └── images │ │ └── default_avatar.jpg ├── extend │ ├── context.js │ └── helper.js ├── controller │ ├── console │ │ ├── common.js │ │ ├── statistics.js │ │ ├── oauth.js │ │ ├── user.js │ │ ├── log.js │ │ ├── application.js │ │ └── interface.js │ ├── base.js │ └── api │ │ └── http.js ├── middleware │ ├── errorHandler.js │ ├── validateUser.js │ └── validateAppUrl.js ├── model │ ├── interface_request_log.js │ ├── interface.js │ ├── user.js │ └── application.js ├── router.js └── service │ ├── interface_request_log.js │ ├── user.js │ ├── interface.js │ └── application.js ├── jsconfig.json ├── .travis.yml ├── .gitignore ├── appveyor.yml ├── .autod.conf.js ├── test └── app │ └── controller │ ├── home.test.js │ └── user.test.js ├── config ├── plugin.js └── config.default.js ├── package.json ├── README.md └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /app/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /app/public/console/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*" 4 | ] 5 | } -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluvenr/open_virapi/HEAD/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/console/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluvenr/open_virapi/HEAD/app/public/console/favicon.ico -------------------------------------------------------------------------------- /app/public/images/default_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluvenr/open_virapi/HEAD/app/public/images/default_avatar.jpg -------------------------------------------------------------------------------- /app/public/console/img/logo.2351b81a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluvenr/open_virapi/HEAD/app/public/console/img/logo.2351b81a.png -------------------------------------------------------------------------------- /app/public/console/fonts/codicon.a609dc0f.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluvenr/open_virapi/HEAD/app/public/console/fonts/codicon.a609dc0f.ttf -------------------------------------------------------------------------------- /app/public/console/img/logo-icon.cccdfcab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluvenr/open_virapi/HEAD/app/public/console/img/logo-icon.cccdfcab.png -------------------------------------------------------------------------------- /app/public/console/img/wxpay_qr_code.8e266050.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluvenr/open_virapi/HEAD/app/public/console/img/wxpay_qr_code.8e266050.png -------------------------------------------------------------------------------- /app/public/console/img/alipay_qr_code.6603dbde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluvenr/open_virapi/HEAD/app/public/console/img/alipay_qr_code.6603dbde.png -------------------------------------------------------------------------------- /app/public/console/css/404.54ae3b1e.css: -------------------------------------------------------------------------------- 1 | .v-404-page{text-align:center;padding-top:22vh}.v-404-page h1{font-size:10em}.v-404-page h2{font-size:4em;margin-bottom:0} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | install: 6 | - npm i npminstall && npminstall 7 | script: 8 | - npm run ci 9 | after_script: 10 | - npminstall codecov && codecov 11 | -------------------------------------------------------------------------------- /app/extend/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _G = Symbol('Context#_g'); 4 | 5 | module.exports = { 6 | get _g() { 7 | return this[_G]; 8 | }, 9 | set _g(data) { 10 | this[_G] = data; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | .idea/ 9 | run/ 10 | .DS_Store 11 | *.sw* 12 | *.un~ 13 | typings/ 14 | .nyc_output/ 15 | 16 | config/*.local.* 17 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '8' 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm i npminstall && node_modules\.bin\npminstall 8 | 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | dep: [ 12 | 'egg', 13 | 'egg-scripts', 14 | ], 15 | devdep: [ 16 | 'egg-ci', 17 | 'egg-bin', 18 | 'egg-mock', 19 | 'autod', 20 | 'autod-egg', 21 | 'eslint', 22 | 'eslint-config-egg', 23 | 'webstorm-disable-index', 24 | ], 25 | exclude: [ 26 | './test/fixtures', 27 | './dist', 28 | ], 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /test/app/controller/home.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, assert } = require('egg-mock/bootstrap'); 4 | 5 | describe('test/app/controller/home.test.js', () => { 6 | it('should assert', function* () { 7 | const pkg = require('../../../package.json'); 8 | assert(app.config.keys.startsWith(pkg.name)); 9 | 10 | // const ctx = app.mockContext({}); 11 | // yield ctx.service.xx(); 12 | }); 13 | 14 | it('should GET /', () => { 15 | return app.httpRequest() 16 | .get('/') 17 | .expect('hi, egg') 18 | .expect(200); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/public/console/js/404.d57c49ac.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["404"],{"038c":function(t,n,a){"use strict";a.r(n);var e=function(){var t=this,n=t.$createElement,a=t._self._c||n;return a("div",{staticClass:"v-full-layout v-404-page"},[a("h2",[t._v("😱🛸📡")]),a("h1",[t._v("404")]),a("router-link",{staticClass:"ant-btn ant-btn-primary ant-btn-lg",attrs:{to:"/"}},[t._v("返回首页")])],1)},s=[],l=(a("e205"),a("2877")),r={},u=Object(l["a"])(r,e,s,!1,null,null,null);n["default"]=u.exports},"5d0a":function(t,n,a){},e205:function(t,n,a){"use strict";var e=a("5d0a"),s=a.n(e);s.a}}]); -------------------------------------------------------------------------------- /app/controller/console/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const BaseController = require('../base'); 5 | 6 | class CommonController extends BaseController { 7 | get user() { 8 | return { 9 | id: this.ctx._g.userInfo._id, 10 | vir_uid: this.ctx._g.userInfo.vir_uid, 11 | }; 12 | } 13 | 14 | set user(data) { 15 | this.user = data; 16 | } 17 | 18 | async index() { 19 | this.ctx.response.type = 'html'; 20 | this.ctx.body = fs.readFileSync(this.app.baseDir + '/app/public/console/index.html'); 21 | } 22 | } 23 | 24 | module.exports = CommonController; 25 | -------------------------------------------------------------------------------- /app/controller/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Controller } = require('egg'); 4 | 5 | class BaseController extends Controller { 6 | 7 | success(data = undefined, message = 'Success') { 8 | const response = { 9 | code: 200, 10 | message, 11 | data, 12 | }; 13 | 14 | this.ctx.body = response; 15 | } 16 | 17 | failed(message = 'Failed', code = 1000) { 18 | const response = { 19 | code, 20 | message, 21 | }; 22 | 23 | this.ctx.body = response; 24 | } 25 | 26 | notFound(msg) { 27 | msg = msg || 'not found'; 28 | this.ctx.throw(404, msg); 29 | } 30 | } 31 | 32 | module.exports = BaseController; 33 | -------------------------------------------------------------------------------- /app/controller/console/statistics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CommonController = require('./common'); 4 | 5 | class StatisticsController extends CommonController { 6 | /** 7 | * 获取应用相关统计数据 8 | */ 9 | async index() { 10 | const inputs = this.ctx.query; 11 | if (!inputs.app_slug) { 12 | this.failed('请指定要查找的应用'); 13 | return; 14 | } 15 | 16 | const app_info = await this.ctx.service.application.getInfoByConditions({ slug: inputs.app_slug, uid: this.user.id }, '_id'); 17 | if (!app_info) { 18 | this.failed('未找到目标应用数据'); 19 | return; 20 | } 21 | 22 | const res = await this.ctx.service.interfaceRequestLog.getStatistics(app_info._id, inputs.date_scope); 23 | 24 | this.success(res); 25 | } 26 | } 27 | 28 | module.exports = StatisticsController; 29 | -------------------------------------------------------------------------------- /config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type Egg.EggPlugin */ 4 | module.exports = { 5 | // had enabled by egg 6 | static: { 7 | enable: true, 8 | }, 9 | 10 | session: { 11 | enable: true, // enable by default 12 | package: 'egg-session', 13 | }, 14 | 15 | validate: { 16 | enable: true, 17 | package: 'egg-validate', 18 | }, 19 | 20 | mongoose: { 21 | enable: true, 22 | package: 'egg-mongoose', 23 | }, 24 | 25 | bcrypt: { 26 | enable: true, 27 | package: 'egg-bcrypt', 28 | }, 29 | 30 | moment: { 31 | enable: true, 32 | package: 'moment', 33 | }, 34 | 35 | jwt: { 36 | enable: true, 37 | package: 'egg-jwt', 38 | }, 39 | 40 | routerPlus: { 41 | enable: true, 42 | package: 'egg-router-plus', 43 | }, 44 | 45 | cors: { 46 | enable: true, 47 | package: 'egg-cors', 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /app/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (option, app) => { 4 | return async (ctx, next) => { 5 | try { 6 | await next(); 7 | } catch (err) { 8 | // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 9 | app.emit('error', err, this); 10 | 11 | const status = err.status || 500; 12 | // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 13 | const message = status === 500 && app.config.env === 'prod' ? 'Internal Server Error' : err.message; 14 | 15 | // 从 error 对象上读出各个属性,设置到响应中 16 | ctx.body = { 17 | code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 18 | message, 19 | }; 20 | 21 | if (status === 422) { // 校验参数抛出异常情况 22 | // ctx.body.detail = err.errors; 23 | ctx.body.message += ' ERR:' + err.errors.map(o => `${o.field} ${o.message}`).join(','); 24 | } 25 | ctx.status = 200; 26 | } 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /app/extend/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const moment = require('moment'); 4 | const fs = require('mz/fs'); 5 | const path = require('path'); 6 | 7 | exports.moment = () => moment(); 8 | 9 | // 格式化时间 10 | exports.formatTime = time => moment(time).format('YYYY-MM-DD HH:mm:ss'); 11 | 12 | // 同步创建多级目录 13 | function mkdirsSync(dirname) { 14 | if (fs.existsSync(dirname)) { 15 | return true; 16 | } else if (mkdirsSync(path.dirname(dirname))) { 17 | fs.mkdirSync(dirname); 18 | return true; 19 | } 20 | return false; 21 | } 22 | exports.mkdirsSync = mkdirsSync; 23 | 24 | // 处理成功响应 25 | // 使用方法:ctx.helper.success(ctx, {'xx':'xxxx'}); 26 | exports.success = (ctx, res = null, msg = 'Success') => { 27 | ctx.body = { 28 | code: 200, 29 | data: res, 30 | msg, 31 | }; 32 | 33 | ctx.status = 200; 34 | }; 35 | 36 | // 处理失败响应 37 | // 使用方法:ctx.helper.failed(ctx, error.message); 38 | exports.failed = (ctx, msg = 'Failed', code = 1000) => { 39 | ctx.body = { 40 | code, 41 | msg, 42 | }; 43 | 44 | ctx.status = 200; 45 | }; 46 | -------------------------------------------------------------------------------- /test/app/controller/user.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, assert } = require('egg-mock/bootstrap'); 4 | 5 | describe('test/app/controller/user.test.js', () => { 6 | 7 | // 注册 8 | // it('should status 200 and get the request body', () => { 9 | // app.mockCsrf(); 10 | // return app.httpRequest() 11 | // .post('/register') 12 | // .send({ name: 'john' }) 13 | // .set('Accept', 'application/json') 14 | // .expect('Content-Type', /json/) 15 | // .expect(200) 16 | // .end((err, res) => { 17 | // if (err) return done(err); 18 | // done(); 19 | // }); 20 | // }); 21 | 22 | // // 登录 23 | // it('should status 200 and get the request body', () => { 24 | // app.mockCsrf(); 25 | // return app.httpRequest() 26 | // .post('/login') 27 | // .send({ name: 'john' }) 28 | // .set('Accept', 'application/json') 29 | // .expect('Content-Type', /json/) 30 | // .expect(200) 31 | // .end((err, res) => { 32 | // if (err) return done(err); 33 | // done(); 34 | // }); 35 | // }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /app/middleware/validateUser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 验证用户是否登录及身份合法性 5 | */ 6 | module.exports = () => { 7 | function _expectsJson(headers) { 8 | return headers['x-requested-with'] && headers['x-requested-with'] === 'XMLHttpRequest' || headers['x-pjax'] || headers.accept && headers.accept.indexOf('/json') > 0; 9 | } 10 | 11 | function _responseErr(ctx, code = 401, message = '登录信息异常或失效,请重新登录') { 12 | if (code === 401) { 13 | ctx.session = null; 14 | ctx.cookies.set('Vir_SESSION', null); 15 | ctx.cookies.set('v_token', null); 16 | } 17 | 18 | if (_expectsJson(ctx.request.header)) { 19 | ctx.body = { 20 | code, 21 | message, 22 | }; 23 | } else { 24 | ctx.unsafeRedirect(`/login?err_msg=${encodeURI(message)}`); 25 | } 26 | } 27 | 28 | return async function validateUser(ctx, next) { 29 | // 验证Session 30 | if (!ctx.session.user_id || ctx.session.user_id !== ctx.cookies.get('v_token', { signed: true, encrypt: true })) { 31 | _responseErr(ctx); 32 | return; 33 | } 34 | 35 | // 检测对应用户是否存在且状态正常 36 | const userInfo = await ctx.service.user.getInfoByConditions({ _id: ctx.session.user_id }); 37 | if (!userInfo || userInfo.status !== 1) { 38 | _responseErr(ctx); 39 | return; 40 | } 41 | 42 | ctx._g = { 43 | userInfo, 44 | }; 45 | 46 | await next(); 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /app/model/interface_request_log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 应用接口请求日志模型 5 | */ 6 | const moment = require('moment'); 7 | 8 | module.exports = app => { 9 | const mongoose = app.mongoose; 10 | const Schema = mongoose.Schema; 11 | const ObjectId = mongoose.Schema.Types.ObjectId; 12 | 13 | const InterfaceRequestLogSchema = new Schema({ 14 | app_id: { 15 | type: ObjectId, 16 | required: true, 17 | }, 18 | app_slug: { 19 | type: String, 20 | required: true, 21 | }, 22 | api_id: { 23 | type: ObjectId, 24 | default: null, 25 | }, 26 | uri: { 27 | type: String, 28 | // maxlength: 100, 29 | }, 30 | method: { 31 | type: String, 32 | enum: ['GET', 'POST', 'PUT', 'DELETE'], 33 | }, 34 | params: { 35 | type: Map, 36 | }, 37 | response: { 38 | type: Map, 39 | }, 40 | result: { // 请求结果状态:1-成功、0-失败 41 | type: Number, 42 | default: 1, 43 | }, 44 | referer: { 45 | type: String, 46 | }, 47 | ip: { 48 | type: String, 49 | }, 50 | device: { 51 | type: String, 52 | }, 53 | created: { 54 | type: Date, 55 | default: Date.now, 56 | get: v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : null, 57 | }, 58 | }, { timestamps: { createdAt: 'created', updatedAt: false } }); 59 | 60 | return mongoose.model('InterfaceRequestLog', InterfaceRequestLogSchema, 'interface_request_log'); 61 | }; 62 | -------------------------------------------------------------------------------- /app/controller/api/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class HttpController extends Controller { 6 | 7 | success(data) { 8 | const { ctx } = this; 9 | 10 | const res_tpl = ctx._g.app.response_template; 11 | const res_data = {}; 12 | res_data[res_tpl.code_name] = res_tpl.succeed_code_value; 13 | if (res_tpl.message_name) { 14 | res_data[res_tpl.message_name] = res_tpl.succeed_message_value; 15 | } 16 | if (res_tpl.data_name && data) { 17 | res_data[res_tpl.data_name] = data; 18 | } 19 | ctx.body = res_data; 20 | } 21 | 22 | /** 23 | * 用户自定义GET请求统一处理入口 24 | */ 25 | async get() { 26 | const { ctx } = this; 27 | 28 | ctx.body = await ctx.service.interface.processResponse(ctx._g.app, ctx._g.api, ctx); 29 | } 30 | 31 | /** 32 | * 用户自定义POST请求统一处理入口 33 | */ 34 | async post() { 35 | const { ctx } = this; 36 | 37 | ctx.body = await ctx.service.interface.processResponse(ctx._g.app, ctx._g.api, ctx); 38 | } 39 | 40 | /** 41 | * 用户自定义PUT请求统一处理入口 42 | */ 43 | async put() { 44 | const { ctx } = this; 45 | 46 | ctx.body = await ctx.service.interface.processResponse(ctx._g.app, ctx._g.api, ctx); 47 | } 48 | 49 | /** 50 | * 用户自定义DELETE请求统一处理入口 51 | */ 52 | async delete() { 53 | const { ctx } = this; 54 | 55 | ctx.body = await ctx.service.interface.processResponse(ctx._g.app, ctx._g.api, ctx); 56 | } 57 | } 58 | 59 | module.exports = HttpController; 60 | -------------------------------------------------------------------------------- /app/public/console/index.html: -------------------------------------------------------------------------------- 1 | VirAPI -- 虚拟数据接口系统 [开源版]
2 |  __      ___             _____ _____ 
3 |  \ \    / (_)      /\   |  __ \_   _|
4 |   \ \  / / _ _ __ /  \  | |__) || |  
5 |    \ \/ / | | '__/ /\ \ |  ___/ | |  
6 |     \  /  | | | / ____ \| |    _| |_ 
7 |      \/   |_|_|/_/    \_\_|   |_____|
8 | 
9 |       
页面加载中,请稍后~~
-------------------------------------------------------------------------------- /app/controller/console/oauth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseController = require('../base'); 4 | 5 | class OauthController extends BaseController { 6 | /** 7 | * 登录 8 | */ 9 | async login() { 10 | const account = this.ctx.request.body.account; 11 | const password = this.ctx.request.body.password; 12 | if (!account || !password) { 13 | this.failed('必要参数缺失'); 14 | return; 15 | } 16 | 17 | let user_info = await this.ctx.service.user.loginByEmailAndPwd(account, password); 18 | if (!user_info) { 19 | this.failed('对应登录信息不存在,请先创建登录!'); 20 | return; 21 | } 22 | 23 | user_info = user_info.toJSON({ getters: true, virtuals: false }); 24 | 25 | // 存储用户信息到session 26 | this.ctx.session.login_time = Date.now(); 27 | this.ctx.session.user_id = user_info._id.toString(); 28 | this.ctx.cookies.set('v_token', user_info._id.toString(), { 29 | httpOnly: false, 30 | signed: true, 31 | encrypt: true, 32 | }); 33 | 34 | this.success({ 35 | nickname: user_info.nickname, 36 | avatar: user_info.avatar, 37 | vir_uid: user_info.vir_uid, 38 | other_info: { 39 | created: user_info.created, 40 | have_app_count: user_info.apps_count, 41 | max_app_count: this.ctx.service.application.maxAppCount, 42 | max_api_count: this.ctx.service.interface.maxApiCount, 43 | vir_uid_updated: user_info.vir_uid_updated, 44 | email: user_info.email, 45 | }, 46 | }); 47 | } 48 | } 49 | 50 | module.exports = OauthController; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-virapi-service", 3 | "version": "1.0.0", 4 | "description": "VirAPI在线云虚拟Api平台开源版", 5 | "homepage": "https://virapi.com/", 6 | "author": "Bluvenr ", 7 | "egg": { 8 | "declarations": true 9 | }, 10 | "dependencies": { 11 | "egg": "^2.15.1", 12 | "egg-bcrypt": "^1.1.0", 13 | "egg-cors": "^2.2.3", 14 | "egg-jwt": "^3.1.6", 15 | "egg-mongoose": "^3.1.1", 16 | "egg-router-plus": "^1.3.0", 17 | "egg-scripts": "^2.11.0", 18 | "egg-validate": "^2.0.2", 19 | "mockjs": "^1.1.0", 20 | "moment": "^2.24.0" 21 | }, 22 | "devDependencies": { 23 | "autod": "^3.0.1", 24 | "autod-egg": "^1.1.0", 25 | "egg-bin": "^4.11.0", 26 | "egg-ci": "^1.11.0", 27 | "egg-mock": "^3.21.0", 28 | "eslint": "^5.13.0", 29 | "eslint-config-egg": "^7.1.0", 30 | "webstorm-disable-index": "^1.2.0" 31 | }, 32 | "engines": { 33 | "node": ">=8.9.0" 34 | }, 35 | "scripts": { 36 | "start": "egg-scripts start --daemon --title=egg-server-virapi", 37 | "stop": "egg-scripts stop --title=egg-server-virapi", 38 | "dev": "egg-bin dev", 39 | "debug": "egg-bin debug", 40 | "test": "npm run lint -- --fix && npm run test-local", 41 | "test-local": "egg-bin test", 42 | "cov": "egg-bin cov", 43 | "lint": "eslint .", 44 | "ci": "npm run lint && npm run cov", 45 | "autod": "autod" 46 | }, 47 | "ci": { 48 | "version": "8" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "" 53 | }, 54 | "license": "Apache-2.0" 55 | } 56 | -------------------------------------------------------------------------------- /app/model/interface.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 接口模型 5 | */ 6 | const moment = require('moment'); 7 | 8 | module.exports = app => { 9 | const mongoose = app.mongoose; 10 | const Schema = mongoose.Schema; 11 | const ObjectId = mongoose.Schema.Types.ObjectId; 12 | 13 | const InterfaceSchema = new Schema({ 14 | app_id: { 15 | type: ObjectId, 16 | required: true, 17 | }, 18 | app_slug: { 19 | type: String, 20 | required: true, 21 | }, 22 | uid: { 23 | type: ObjectId, 24 | required: true, 25 | }, 26 | vir_uid: { 27 | type: String, 28 | required: true, 29 | }, 30 | name: { 31 | type: String, 32 | trim: true, 33 | minlength: 2, 34 | maxlength: 60, 35 | }, 36 | describe: { 37 | type: String, 38 | maxlength: 200, 39 | default: '', 40 | }, 41 | uri: { 42 | type: String, 43 | maxlength: 100, 44 | match: /^[\w-.]+$/, 45 | trim: true, 46 | }, 47 | method: { 48 | type: String, 49 | enum: [ 50 | 'GET', 'POST', 'PUT', 'DELETE', 51 | ], 52 | default: 'GET', 53 | }, 54 | response_rules: { 55 | type: Schema.Types.Mixed, 56 | }, 57 | creator: { 58 | type: ObjectId, 59 | }, 60 | status: { 61 | type: Number, 62 | default: 1, 63 | }, 64 | created: { 65 | type: Date, 66 | default: Date.now, 67 | get: v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : null, 68 | }, 69 | updated: { 70 | type: Date, 71 | default: Date.now, 72 | get: v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : null, 73 | }, 74 | }, { 75 | timestamps: { createdAt: 'created', updatedAt: 'updated' }, 76 | }); 77 | 78 | InterfaceSchema.pre('save', next => { 79 | const now = new Date(); 80 | this.updated = now; 81 | next(); 82 | }); 83 | 84 | return mongoose.model('Interface', InterfaceSchema, 'interface'); 85 | }; 86 | -------------------------------------------------------------------------------- /app/controller/console/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CommonController = require('./common'); 4 | 5 | class UserController extends CommonController { 6 | /** 7 | * 获取当前我的账号信息 8 | */ 9 | async my_account() { 10 | let user_info = this.ctx._g.userInfo; 11 | user_info = user_info.toJSON({ getters: true, virtuals: false }); 12 | 13 | this.success({ 14 | nickname: user_info.nickname, 15 | avatar: user_info.avatar, 16 | vir_uid: user_info.vir_uid, 17 | other_info: { 18 | created: user_info.created, 19 | have_app_count: user_info.apps_count, 20 | max_app_count: this.ctx.service.application.maxAppCount, 21 | max_api_count: this.ctx.service.interface.maxApiCount, 22 | vir_uid_updated: user_info.vir_uid_updated, 23 | email: user_info.email, 24 | }, 25 | }); 26 | } 27 | 28 | /** 29 | * 退出登录 30 | */ 31 | async logout() { 32 | this.ctx.session = null; 33 | this.ctx.cookies.set('Vir_SESSION', null); 34 | this.ctx.cookies.set('v_token', null); 35 | 36 | this.success(); 37 | } 38 | 39 | /** 40 | * 编辑个人资料 41 | */ 42 | async update() { 43 | const { ctx } = this; 44 | 45 | ctx.validate({ 46 | nickname: { type: 'string', min: 2, max: 20 }, 47 | vir_uid: { type: 'string', required: false, allowEmpty: false, format: /^[a-z][a-z0-9_\-]{3,23}$/ }, 48 | email: { type: 'email', required: false, allowEmpty: false }, 49 | avatar: { type: 'string', required: false, format: /^data:image\/\w+;base64,/ }, 50 | }, ctx.request.body); 51 | 52 | await ctx.service.user.updateByUid(this.user.id, ctx.request.body); 53 | 54 | this.success(); 55 | } 56 | 57 | /** 58 | * 重置登录密码 59 | */ 60 | async change_pwd() { 61 | const { ctx } = this; 62 | 63 | ctx.validate({ 64 | old_password: { type: 'string', min: 6, max: 20 }, 65 | password: { type: 'string', min: 6, max: 20 }, 66 | }, ctx.request.body); 67 | 68 | await ctx.service.user.updatePwdByUid(this.user.id, ctx.request.body); 69 | 70 | this.success(); 71 | } 72 | } 73 | 74 | module.exports = UserController; 75 | -------------------------------------------------------------------------------- /app/model/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 用户模型 5 | */ 6 | const moment = require('moment'); 7 | 8 | module.exports = app => { 9 | const mongoose = app.mongoose; 10 | const Schema = mongoose.Schema; 11 | const avatarBaseUri = app.config.imgUri; 12 | 13 | const UserSchema = new Schema({ 14 | vir_uid: { 15 | type: String, 16 | unique: true, 17 | match: /^[a-z][a-z0-9_\-]{3,23}$/, 18 | trim: true, 19 | }, 20 | vir_uid_updated: { 21 | type: Date, 22 | default: null, 23 | get: v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : null, 24 | }, 25 | nickname: { 26 | type: String, 27 | // lowercase: true, 28 | trim: true, 29 | minlength: 2, 30 | maxlength: 20, 31 | }, 32 | avatar: { 33 | type: String, 34 | get: v => `${avatarBaseUri}${v}`, 35 | default: '/default_avatar.jpg', 36 | }, 37 | email: { 38 | type: String, 39 | trim: true, 40 | match: /^[a-zA-Z0-9._-]+@[a-z0-9-]{2,}\.[a-z]{2,}$/, 41 | }, 42 | apps_count: { 43 | type: Number, 44 | default: 0, 45 | }, 46 | password: { 47 | type: String, 48 | minlength: 6, 49 | }, 50 | status: { 51 | type: Number, 52 | default: 1, 53 | enum: [0, 1], 54 | }, 55 | login_date: { 56 | type: Date, 57 | default: null, 58 | get: v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : null, 59 | }, 60 | created: { 61 | type: Date, 62 | default: Date.now, 63 | get: v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : null, 64 | }, 65 | updated: { 66 | type: Date, 67 | default: Date.now, 68 | get: v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : null, 69 | }, 70 | }, { timestamps: { createdAt: 'created', updatedAt: 'updated' } }); 71 | 72 | UserSchema.virtual('statusName').get(function () { 73 | switch (this.status) { 74 | case 0: return '冻结'; 75 | case 1: return '正常'; 76 | default: return '未知状态'; 77 | } 78 | }); 79 | 80 | UserSchema.pre('save', next => { 81 | const now = new Date(); 82 | this.updated = now; 83 | next(); 84 | }); 85 | 86 | return mongoose.model('User', UserSchema, 'user'); 87 | }; 88 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | /* eslint valid-jsdoc: "off" */ 2 | 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | /** 9 | * @param {Egg.EggAppInfo} appInfo app info 10 | */ 11 | module.exports = appInfo => { 12 | /** 13 | * built-in config 14 | * @type {Egg.EggAppConfig} 15 | **/ 16 | const config = { 17 | mongoose: { 18 | // url: 'mongodb://127.0.0.1:27017/open_virapi_db', 19 | options: { 20 | // useMongoClient: true, 21 | autoReconnect: true, 22 | reconnectTries: Number.MAX_VALUE, 23 | bufferMaxEntries: 0, 24 | }, 25 | }, 26 | bcrypt: { 27 | saltRounds: 10, 28 | }, 29 | security: { 30 | csrf: { 31 | enable: false, 32 | ignoreJSON: true, 33 | }, 34 | domainWhiteList: [ 35 | 'http://localhost:8080', 36 | ], 37 | }, 38 | validate: { 39 | convert: true, 40 | }, 41 | cors: { 42 | // origin: '*', 43 | allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS', 44 | }, 45 | jwt: { 46 | secret: 'virapi-202008192239', 47 | }, 48 | proxy: true, // 通过ips获取nginx代理层真实IP 49 | session: { 50 | key: 'Vir_SESSION', // 承载 Session 的 Cookie 键值对名字 51 | maxAge: 2 * 3600 * 1000, // Session 的最大有效时间 52 | httpOnly: true, 53 | encrypt: true, 54 | renew: true, // 每次访问页面都会给session会话延长时间 55 | }, 56 | static: { 57 | prefix: '/', 58 | dir: path.join(appInfo.baseDir, 'app/public'), 59 | dynamic: true, 60 | preload: false, 61 | maxAge: 0, 62 | buffer: false, 63 | }, 64 | }; 65 | 66 | // use for cookie sign key, should change to your own and keep security 67 | config.keys = appInfo.name + '_hNW87vqPkMiMpLBHEtolB3Yg6vQsk5Ip4AJzCih2QCXbZBmjh5I033ELjdwB'; 68 | 69 | // add your middleware config here 70 | config.middleware = [ 71 | 'errorHandler', 72 | ]; 73 | 74 | config.siteFile = { 75 | '/favicon.ico': fs.readFileSync(appInfo.baseDir + '/app/public/favicon.ico'), 76 | }; 77 | 78 | // add your user config here 79 | const userConfig = { 80 | // myAppName: 'egg', 81 | imgUri: '/images', 82 | imgDir: appInfo.baseDir + '/app/public/images', 83 | }; 84 | 85 | return { 86 | ...config, 87 | ...userConfig, 88 | }; 89 | }; 90 | -------------------------------------------------------------------------------- /app/controller/console/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CommonController = require('./common'); 4 | const moment = require('moment'); 5 | 6 | class LogController extends CommonController { 7 | /** 8 | * 获取应用请求日志 9 | */ 10 | async request_log() { 11 | const inputs = this.ctx.query; 12 | if (!inputs.app_slug) { 13 | this.failed('请指定要查找的应用'); 14 | return; 15 | } 16 | 17 | const app_info = await this.ctx.service.application.getInfoByConditions({ slug: inputs.app_slug, uid: this.user.id }, '_id'); 18 | if (!app_info) { 19 | this.failed('未找到目标应用数据'); 20 | return; 21 | } 22 | 23 | const conditions = { app_id: app_info._id }; 24 | switch (inputs.date_type) { 25 | case 'today': 26 | conditions.created = { $gte: new Date(moment().format('YYYY-MM-DD 00:00:00')), $lt: new Date(moment().format('YYYY-MM-DD 23:59:59')) }; 27 | break; 28 | case 'yesterday': 29 | conditions.created = { $gte: new Date(moment().subtract(1, 'days').format('YYYY-MM-DD 00:00:00')), $lt: new Date(moment().subtract(1, 'days').format('YYYY-MM-DD 23:59:59')) }; 30 | break; 31 | case '7days': 32 | conditions.created = { $gte: new Date(moment().subtract(7, 'days').format('YYYY-MM-DD 00:00:00')), $lt: new Date(moment().format('YYYY-MM-DD 23:59:59')) }; 33 | break; 34 | case '30days': 35 | conditions.created = { $gte: new Date(moment().subtract(30, 'days').format('YYYY-MM-DD 00:00:00')), $lt: new Date(moment().format('YYYY-MM-DD 23:59:59')) }; 36 | break; 37 | default: 38 | conditions.created = { $gte: new Date(moment().format('YYYY-MM-DD 00:00:00')), $lt: new Date(moment().format('YYYY-MM-DD 23:59:59')) }; 39 | break; 40 | } 41 | if (inputs.api_id) { 42 | conditions.api_id = inputs.api_id !== 'undefined' ? inputs.api_id : { $type: 10 }; 43 | } 44 | /* if (inputs.kw && inputs.kw.trim()) { 45 | conditions.$or = [{ params: { $regex: inputs.kw.trim(), $options: 'i' } }, { response: { $regex: inputs.kw.trim(), $options: 'i' } }]; 46 | } */ 47 | if (inputs.method) { 48 | conditions.method = inputs.method; 49 | } 50 | if (inputs.result !== undefined) { 51 | conditions.result = inputs.result; 52 | } 53 | 54 | let page = inputs.page || 1; 55 | let per_page = inputs.per_page || 10; 56 | if (page <= 0) page = 1; 57 | if (per_page <= 0 || per_page > 100) per_page = 100; 58 | 59 | const res = await this.ctx.service.interfaceRequestLog.getList(conditions, 'app_slug api_id uri ip method params response result created -_id', page, per_page); 60 | 61 | this.success(res); 62 | } 63 | } 64 | 65 | module.exports = LogController; 66 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {Egg.Application} app - egg application 5 | */ 6 | module.exports = app => { 7 | const { router, controller, middlewares } = app; 8 | 9 | router.get('/', controller.console.common.index); 10 | router.get('/console', controller.console.common.index); 11 | 12 | /** 13 | * Api模块路由 14 | */ 15 | const validateAppUrl = middlewares.validateAppUrl(); 16 | router.get(/^\/api\/([\w-.]+)\/([\w-.]+)\/([\w-.\/]+)$/, validateAppUrl, controller.api.http.get); 17 | router.post(/^\/api\/([\w-.]+)\/([\w-.]+)\/([\w-.\/]+)$/, validateAppUrl, controller.api.http.get); 18 | router.put(/^\/api\/([\w-.]+)\/([\w-.]+)\/([\w-.\/]+)$/, validateAppUrl, controller.api.http.get); 19 | router.delete(/^\/api\/([\w-.]+)\/([\w-.]+)\/([\w-.\/]+)$/, validateAppUrl, controller.api.http.get); 20 | 21 | 22 | /** 23 | * Console模块路由 24 | */ 25 | router.post('/ajax/login', controller.console.oauth.login); 26 | 27 | const validateUser = middlewares.validateUser(); 28 | router.get('/ajax/account', validateUser, controller.console.user.my_account); 29 | router.delete('/ajax/session', validateUser, controller.console.user.logout); 30 | router.post('/ajax/user/profile', validateUser, controller.console.user.update); 31 | router.put('/ajax/user_pwd', validateUser, controller.console.user.change_pwd); 32 | 33 | router.get('/ajax/request_log', validateUser, controller.console.log.request_log); 34 | 35 | router.get('/ajax/statistics', validateUser, controller.console.statistics.index); 36 | 37 | router.get('/ajax/application/list', validateUser, controller.console.application.list); 38 | router.post('/ajax/change_application_key', validateUser, controller.console.application.change_app_key); 39 | router.post('/ajax/application/copy', validateUser, controller.console.application.copy); 40 | router.get('/ajax/application/:slug/base_info', validateUser, controller.console.application.base_info); 41 | router.get('/ajax/application/export', validateUser, controller.console.application.export); 42 | 43 | router.post('/ajax/interface/empty', validateUser, controller.console.interface.empty); 44 | router.post('/ajax/interface/copy', validateUser, controller.console.interface.copy); 45 | router.post('/ajax/interface/move', validateUser, controller.console.interface.move); 46 | router.get('/ajax/interface/list', validateUser, controller.console.interface.list); 47 | router.get('/ajax/interface_map', validateUser, controller.console.interface.map); 48 | router.post('/ajax/interface_debug', validateUser, controller.console.interface.debug); 49 | 50 | router.resources('application', '/ajax/application', validateUser, controller.console.application); 51 | router.resources('interface', '/ajax/interface', validateUser, controller.console.interface); 52 | }; 53 | -------------------------------------------------------------------------------- /app/middleware/validateAppUrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 验证Api请求Url是否存在且有效(请求生成虚拟数据接口) 5 | */ 6 | module.exports = () => { 7 | return async function validateAppUrl(ctx, next) { 8 | const ip = ctx.ips.length > 0 ? (ctx.ips[0] !== '127.0.0.1' ? ctx.ips[0] : ctx.ips[1]) : ctx.ip; 9 | 10 | const vir_uid = ctx.params[0]; 11 | const app_slug = ctx.params[1]; 12 | const uri = ctx.params[2]; 13 | const method = ctx.request.method; 14 | 15 | // 检测对应用户是否存在且状态正常 16 | const user = await ctx.service.user.getInfoByConditions({ vir_uid }); 17 | if (!user || user.status !== 1) { 18 | ctx.body = 'APPLICATION NOT EXIST!'; 19 | ctx.status = 404; 20 | return; 21 | } 22 | 23 | // 检测对应App是否存在且状态正常 24 | const app = await ctx.service.application.getInfoByConditions({ uid: user.id, slug: app_slug }); 25 | if (!app || app.status !== 1) { 26 | ctx.body = 'The application does not exist or is invalid!'; 27 | ctx.status = 404; 28 | return; 29 | } 30 | 31 | // 检测app_key 32 | let app_token = null; 33 | if (app.verify_rule === 'header') { 34 | app_token = ctx.get('app-token'); 35 | } else if (app.verify_rule === 'param') { 36 | app_token = ctx.query._token; 37 | } else { 38 | app_token = ctx.get('app-token') || ctx.query._token; 39 | } 40 | const res_tpl = app.response_template; 41 | if (app.app_key !== app_token) { 42 | const res_data = {}; 43 | res_data[res_tpl.code_name] = res_tpl.failed_code_value; 44 | if (res_tpl.message_name) { 45 | res_data[res_tpl.message_name] = '[VirApi] token error!'; 46 | } 47 | ctx.body = res_data; 48 | ctx.status = 401; 49 | return; 50 | } 51 | 52 | // 检测对应接口是否存在且状态正常 53 | const api = await ctx.service.interface.getApiByAppIdAndMethod(app.id, method, uri); 54 | if (!api) { 55 | const res_data = {}; 56 | res_data[res_tpl.code_name] = res_tpl.failed_code_value; 57 | if (res_tpl.message_name) { 58 | res_data[res_tpl.message_name] = 'Interface error or invalid!'; 59 | } 60 | ctx.body = res_data; 61 | ctx.status = 400; 62 | return; 63 | } 64 | 65 | ctx._g = { 66 | user, 67 | app, 68 | api, 69 | }; 70 | 71 | await next(); 72 | 73 | // 添加请求日志记录 74 | ctx.service.interfaceRequestLog.insert({ 75 | app_id: app.id, 76 | app_slug: app.slug, 77 | api_id: api.id, 78 | uri: ctx.request.url.replace(/\/api/, ''), 79 | method, 80 | params: (method === 'POST' || method === 'PUT') ? ctx.request.body : ctx.query, 81 | response: ctx.body, 82 | result: (ctx.body && ctx.body[res_tpl.code_name] !== undefined && ctx.body[res_tpl.code_name] === res_tpl.succeed_code_value) ? 1 : 0, 83 | referer: ctx.get('referer'), 84 | ip, 85 | device: ctx.get('user-agent'), 86 | }); 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /app/public/console/js/chunk-2d0c512b.79c6b45a.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0c512b"],{"3e14":function(e,t,n){"use strict";n.r(t),n.d(t,"conf",(function(){return s})),n.d(t,"language",(function(){return o}));var s={comments:{blockComment:["\x3c!--","--\x3e"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">",notIn:["string"]}],surroundingPairs:[{open:"(",close:")"},{open:"[",close:"]"},{open:"`",close:"`"}],folding:{markers:{start:new RegExp("^\\s*\x3c!--\\s*#?region\\b.*--\x3e"),end:new RegExp("^\\s*\x3c!--\\s*#?endregion\\b.*--\x3e")}}},o={defaultToken:"",tokenPostfix:".md",control:/[\\`*_\[\]{}()#+\-\.!]/,noncontrol:/[^\\`*_\[\]{}()#+\-\.!]/,escapes:/\\(?:@control)/,jsescapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,empty:["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param"],tokenizer:{root:[[/^\s*\|/,"@rematch","@table_header"],[/^(\s{0,3})(#+)((?:[^\\#]|@escapes)+)((?:#+)?)/,["white","keyword","keyword","keyword"]],[/^\s*(=+|\-+)\s*$/,"keyword"],[/^\s*((\*[ ]?)+)\s*$/,"meta.separator"],[/^\s*>+/,"comment"],[/^\s*([\*\-+:]|\d+\.)\s/,"keyword"],[/^(\t|[ ]{4})[^ ].*$/,"string"],[/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/,{token:"string",next:"@codeblock"}],[/^\s*```\s*((?:\w|[\/\-#])+).*$/,{token:"string",next:"@codeblockgh",nextEmbedded:"$1"}],[/^\s*```\s*$/,{token:"string",next:"@codeblock"}],{include:"@linecontent"}],table_header:[{include:"@table_common"},[/[^\|]+/,"keyword.table.header"]],table_body:[{include:"@table_common"},{include:"@linecontent"}],table_common:[[/\s*[\-:]+\s*/,{token:"keyword",switchTo:"table_body"}],[/^\s*\|/,"keyword.table.left"],[/^\s*[^\|]/,"@rematch","@pop"],[/^\s*$/,"@rematch","@pop"],[/\|/,{cases:{"@eos":"keyword.table.right","@default":"keyword.table.middle"}}]],codeblock:[[/^\s*~~~\s*$/,{token:"string",next:"@pop"}],[/^\s*```\s*$/,{token:"string",next:"@pop"}],[/.*$/,"variable.source"]],codeblockgh:[[/```\s*$/,{token:"variable.source",next:"@pop",nextEmbedded:"@pop"}],[/[^`]+/,"variable.source"]],linecontent:[[/&\w+;/,"string.escape"],[/@escapes/,"escape"],[/\b__([^\\_]|@escapes|_(?!_))+__\b/,"strong"],[/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/,"strong"],[/\b_[^_]+_\b/,"emphasis"],[/\*([^\\*]|@escapes)+\*/,"emphasis"],[/`([^\\`]|@escapes)+`/,"variable"],[/\{+[^}]+\}+/,"string.target"],[/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/,["string.link","","string.link"]],[/(!?\[)((?:[^\]\\]|@escapes)*)(\])/,"string.link"],{include:"html"}],html:[[/<(\w+)\/>/,"tag"],[/<(\w+)/,{cases:{"@empty":{token:"tag",next:"@tag.$1"},"@default":{token:"tag",next:"@tag.$1"}}}],[/<\/(\w+)\s*>/,{token:"tag"}],[//,"comment","@pop"],[/