├── .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"],[/