├── web
├── static
│ └── .gitkeep
├── .eslintignore
├── config
│ ├── prod.env.js
│ ├── dev.env.js
│ └── index.js
├── src
│ ├── assets
│ │ └── logo.png
│ ├── event_bus.js
│ ├── constant.js
│ ├── router
│ │ └── index.js
│ ├── components
│ │ ├── layout
│ │ │ ├── Navigator.vue
│ │ │ ├── SideMenu.vue
│ │ │ └── TopMenu.vue
│ │ ├── Dashboard.vue
│ │ ├── Login.vue
│ │ └── admin
│ │ │ ├── Permission.vue
│ │ │ ├── User.vue
│ │ │ └── Role.vue
│ ├── main.js
│ └── App.vue
├── .editorconfig
├── .gitignore
├── .postcssrc.js
├── build
│ ├── dev-client.js
│ ├── vue-loader.conf.js
│ ├── build.js
│ ├── webpack.dev.conf.js
│ ├── check-versions.js
│ ├── webpack.base.conf.js
│ ├── utils.js
│ ├── dev-server.js
│ └── webpack.prod.conf.js
├── .babelrc
├── index.html
├── README.md
├── .eslintrc.js
└── package.json
├── screenshots
├── login.png
├── admin_role.png
├── admin_user.png
├── admin_permission.png
└── admin_role_delete.png
├── .sequelizerc
├── .editorconfig
├── .env.example
├── .eslintrc.json
├── config
└── database.js
├── route
├── base.js
└── admin.js
├── database
├── models
│ ├── admin
│ │ ├── role.js
│ │ ├── permission.js
│ │ └── user.js
│ └── index.js
├── migrations
│ ├── 20170711142709-admin_user.js
│ ├── 20170717022918-admin_role.js
│ └── 20170717025438-admin_permission.js
└── seeders
│ └── 20170715071127-admin.js
├── controller
├── admin
│ ├── permission.js
│ ├── role.js
│ └── user.js
├── base.js
├── rest.js
└── session.js
├── LICENSE
├── .gitignore
├── package.json
├── middleware
├── auth.js
└── base.js
├── api.rest
├── server.js
├── util.js
├── README.md
├── README_EN.md
└── test
└── test.js
/web/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/web/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"'
3 | }
4 |
--------------------------------------------------------------------------------
/screenshots/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jarontai/express-vue-admin/HEAD/screenshots/login.png
--------------------------------------------------------------------------------
/web/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jarontai/express-vue-admin/HEAD/web/src/assets/logo.png
--------------------------------------------------------------------------------
/screenshots/admin_role.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jarontai/express-vue-admin/HEAD/screenshots/admin_role.png
--------------------------------------------------------------------------------
/screenshots/admin_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jarontai/express-vue-admin/HEAD/screenshots/admin_user.png
--------------------------------------------------------------------------------
/web/src/event_bus.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | const EventBus = new Vue();
4 |
5 | export default EventBus;
6 |
--------------------------------------------------------------------------------
/screenshots/admin_permission.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jarontai/express-vue-admin/HEAD/screenshots/admin_permission.png
--------------------------------------------------------------------------------
/screenshots/admin_role_delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jarontai/express-vue-admin/HEAD/screenshots/admin_role_delete.png
--------------------------------------------------------------------------------
/web/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/web/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Editor directories and files
9 | .idea
10 | *.suo
11 | *.ntvs*
12 | *.njsproj
13 | *.sln
14 |
--------------------------------------------------------------------------------
/web/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | // to edit target browsers: use "browserlist" field in package.json
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/web/src/constant.js:
--------------------------------------------------------------------------------
1 | export default {
2 | permissionMenuMap: {
3 | 'dashboard': 'Dashboard',
4 | 'admin': '后台管理',
5 | 'admin:user': '用户管理',
6 | 'admin:role': '角色管理',
7 | 'admin:permission': '权限管理'
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/web/build/dev-client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | require('eventsource-polyfill')
3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
4 |
5 | hotClient.subscribe(function (event) {
6 | if (event.action === 'reload') {
7 | window.location.reload()
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/.sequelizerc:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = {
4 | 'config': path.resolve('config', 'database.js'),
5 | 'migrations-path': path.resolve('database', 'migrations'),
6 | 'seeders-path': path.resolve('database', 'seeders'),
7 | 'models-path': path.resolve('database', 'models')
8 | };
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | #server
2 | NODE_ENV=development
3 | SERVER_PORT=3000
4 | API_PATH=/api
5 | API_VERSION=v1
6 |
7 | #db
8 | DB_HOST=localhost
9 | DB_DATABASE=admin
10 | DB_USER=root
11 | DB_PASSWORD=password
12 |
13 | #redis
14 | REDIS_HOST=
15 | REDIS_PORT=
16 |
17 | #misc
18 | ADMIN_SEED_PASSWORD=adminpwd
19 | TEST_SEED_PASSWORD=testpwd
20 | SERVER_PORT_TEST=3001
21 |
--------------------------------------------------------------------------------
/web/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils')
2 | var config = require('../config')
3 | var isProduction = process.env.NODE_ENV === 'production'
4 |
5 | module.exports = {
6 | loaders: utils.cssLoaders({
7 | sourceMap: isProduction
8 | ? config.build.productionSourceMap
9 | : config.dev.cssSourceMap,
10 | extract: isProduction
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/web/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-runtime"],
12 | "env": {
13 | "test": {
14 | "presets": ["env", "stage-2"],
15 | "plugins": ["istanbul"]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | express-vue-admin
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "node"
4 | ],
5 | "extends": [
6 | "eslint:recommended",
7 | "plugin:node/recommended"
8 | ],
9 | "rules": {
10 | "node/exports-style": [
11 | "error",
12 | "module.exports"
13 | ],
14 | "no-console": 0,
15 | "semi": [
16 | "error",
17 | "always"
18 | ]
19 | },
20 | "env": {
21 | "mocha":true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/config/database.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('dotenv').config();
4 |
5 | const dbConfig = {
6 | "username": process.env.DB_USER,
7 | "password": process.env.DB_PASSWORD,
8 | "database": process.env.DB_DATABASE,
9 | "host": process.env.DB_HOST,
10 | "dialect": 'mysql'
11 | };
12 |
13 | const config = {
14 | "development": dbConfig,
15 | "test": dbConfig,
16 | "production": dbConfig
17 | };
18 |
19 | module.exports = config;
20 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | # express-vue-admin
2 |
3 | > express-vue-admin web app
4 |
5 | ## Build Setup
6 |
7 | ``` bash
8 | # install dependencies
9 | npm install
10 |
11 | # serve with hot reload at localhost:8080
12 | npm run dev
13 |
14 | # build for production with minification
15 | npm run build
16 |
17 | # build for production and view the bundle analyzer report
18 | npm run build --report
19 | ```
20 |
21 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
22 |
--------------------------------------------------------------------------------
/web/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Router from 'vue-router';
3 | import Dashboard from '@/components/Dashboard';
4 | import AdminUser from '@/components/admin/User';
5 | import AdminRole from '@/components/admin/Role';
6 | import AdminPermission from '@/components/admin/Permission';
7 |
8 | Vue.use(Router);
9 |
10 | export default new Router({
11 | routes: [
12 | {
13 | path: '/dashboard',
14 | component: Dashboard
15 | },
16 | {
17 | path: '/admin/user',
18 | component: AdminUser
19 | },
20 | {
21 | path: '/admin/role',
22 | component: AdminRole
23 | },
24 | {
25 | path: '/admin/permission',
26 | component: AdminPermission
27 | }
28 | ]
29 | });
30 |
--------------------------------------------------------------------------------
/route/base.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express');
4 | const router = express.Router();
5 |
6 | const util = require('../util');
7 | const sessionController = require('../controller/session');
8 | const authMiddleware = require('../middleware/auth');
9 |
10 | router.get('/', (req, res) => {
11 | res.reply('Hola!');
12 | });
13 |
14 | // 查看/创建session无需登录,登出与更新密码需要
15 | util.buildRoute([
16 | { path: '/sessions', method: 'get', target: 'index' },
17 | { path: '/sessions', method: 'post', target: 'create' },
18 | { path: '/sessions', method: 'delete', target: 'destroy', middlewares: [authMiddleware.login] },
19 | { path: '/sessions/update-password', method: 'post', target: 'updatePassword', middlewares: [authMiddleware.login] }
20 | ], router, sessionController);
21 |
22 | module.exports = router;
23 |
--------------------------------------------------------------------------------
/database/models/admin/role.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const util = require('../../../util');
4 |
5 | module.exports = (sequelize, DataTypes) => {
6 | const role = sequelize.define('AdminRole', {
7 | id: {
8 | autoIncrement: true,
9 | type: DataTypes.INTEGER.UNSIGNED,
10 | primaryKey: true
11 | },
12 | name: DataTypes.STRING,
13 | comment: DataTypes.STRING
14 | }, util.addModelCommonOptions({
15 | tableName: 'admin_role'
16 | }));
17 |
18 | role.associate = function(models) {
19 | models.AdminUser.belongsToMany(models.AdminRole, {as: 'roles', through: 'admin_user_role', foreignKey: 'uid', constraints: false});
20 | models.AdminRole.belongsToMany(models.AdminUser, {as: 'users', through: 'admin_user_role', foreignKey: 'rid', constraints: false});
21 | };
22 |
23 | return role;
24 | };
25 |
--------------------------------------------------------------------------------
/controller/admin/permission.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const joi = require('joi');
4 |
5 | const RestController = require('../rest');
6 |
7 | class PermissionController extends RestController {
8 | constructor() {
9 | super('AdminPermission');
10 |
11 | this.restRules = {
12 | create: {
13 | name: joi.string().min(3).required(),
14 | comment: joi.string().min(2).required()
15 | },
16 | update: {
17 | name: joi.string().min(3),
18 | comment: joi.string().min(2)
19 | }
20 | };
21 |
22 | this.model.count().then((result) => {
23 | if (!result || result < 5) {
24 | throw new Error('Default admin permissions count error! Should run sequelize seeder first!');
25 | }
26 | });
27 | }
28 |
29 | }
30 |
31 | module.exports = new PermissionController();
32 |
--------------------------------------------------------------------------------
/database/models/admin/permission.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const util = require('../../../util');
4 |
5 | module.exports = (sequelize, DataTypes) => {
6 | const permission = sequelize.define('AdminPermission', {
7 | id: {
8 | autoIncrement: true,
9 | type: DataTypes.INTEGER.UNSIGNED,
10 | primaryKey: true
11 | },
12 | name: DataTypes.STRING,
13 | comment: DataTypes.STRING
14 | }, util.addModelCommonOptions({
15 | tableName: 'admin_permission'
16 | }));
17 |
18 | permission.associate = function(models) {
19 | models.AdminRole.belongsToMany(models.AdminPermission, {as: 'permissions', through: 'admin_role_permission', foreignKey: 'rid', constraints: false});
20 | models.AdminPermission.belongsToMany(models.AdminRole, {as: 'roles', through: 'admin_role_permission', foreignKey: 'pid', constraints: false});
21 | };
22 |
23 | return permission;
24 | };
25 |
--------------------------------------------------------------------------------
/controller/base.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('lodash');
4 | const models = require('../database/models');
5 |
6 | /**
7 | * 控制器基类,提供默认的控制器方法
8 | */
9 | class BaseController {
10 | /**
11 | * Creates an instance of BaseController.
12 | */
13 | constructor() {
14 | this.sequelize = models.sequelize;
15 | this.models = models;
16 | }
17 |
18 | /**
19 | * 模型数组转换为JSON并过滤字段
20 | *
21 | * @param {*} modelArr
22 | * @param {*} fields
23 | */
24 | filterModels(modelArr, fields) {
25 | return _.map(modelArr, (o) => {
26 | let result;
27 | const temp = o.toJSON();
28 | if (!fields || !fields.length) {
29 | result = temp;
30 | } else {
31 | result = {};
32 | _.forEach(fields, field => {
33 | result[field] = temp[field];
34 | });
35 | }
36 | return result;
37 | });
38 | }
39 | }
40 |
41 | module.exports = BaseController;
42 |
--------------------------------------------------------------------------------
/database/migrations/20170711142709-admin_user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: function (queryInterface, Sequelize) {
5 | return queryInterface.createTable('admin_user',
6 | {
7 | id: {
8 | type: Sequelize.INTEGER,
9 | primaryKey: true,
10 | autoIncrement: true
11 | },
12 | username: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | unique: true
16 | },
17 | password: Sequelize.STRING,
18 | disabled: {
19 | type: Sequelize.BOOLEAN,
20 | allowNull: false,
21 | defaultValue: false
22 | },
23 | createdAt: {
24 | type: Sequelize.DATE
25 | },
26 | updatedAt: {
27 | type: Sequelize.DATE
28 | },
29 | deletedAt: {
30 | type: Sequelize.DATE
31 | }
32 | }
33 | );
34 | },
35 |
36 | down: function (queryInterface, Sequelize) {
37 | return queryInterface.dropTable('admin_user');
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/web/build/build.js:
--------------------------------------------------------------------------------
1 | require('./check-versions')()
2 |
3 | process.env.NODE_ENV = 'production'
4 |
5 | var ora = require('ora')
6 | var rm = require('rimraf')
7 | var path = require('path')
8 | var chalk = require('chalk')
9 | var webpack = require('webpack')
10 | var config = require('../config')
11 | var webpackConfig = require('./webpack.prod.conf')
12 |
13 | var spinner = ora('building for production...')
14 | spinner.start()
15 |
16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
17 | if (err) throw err
18 | webpack(webpackConfig, function (err, stats) {
19 | spinner.stop()
20 | if (err) throw err
21 | process.stdout.write(stats.toString({
22 | colors: true,
23 | modules: false,
24 | children: false,
25 | chunks: false,
26 | chunkModules: false
27 | }) + '\n\n')
28 |
29 | console.log(chalk.cyan(' Build complete.\n'))
30 | console.log(chalk.yellow(
31 | ' Tip: built files are meant to be served over an HTTP server.\n' +
32 | ' Opening index.html over file:// won\'t work.\n'
33 | ))
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/route/admin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express');
4 | const router = express.Router();
5 |
6 | const util = require('../util');
7 | const userController = require('../controller/admin/user');
8 | const roleController = require('../controller/admin/role');
9 | const permissionController = require('../controller/admin/permission');
10 | const authMiddleware = require('../middleware/auth');
11 |
12 | // admin角色才能访问本模块接口
13 | router.use(authMiddleware.role('admin'));
14 |
15 | util.restRoute('/users', router, userController);
16 | util.restRoute('/roles', router, roleController);
17 | util.restRoute('/permissions', router, permissionController);
18 |
19 | util.buildRoute([
20 | {path: '/users/:id/roles', method: 'get', target: 'fetchRoles'},
21 | {path: '/users/:id/roles', method: 'put', target: 'updateRoles'}
22 | ], router, userController);
23 |
24 | util.buildRoute([
25 | {path: '/roles/:id/permissions', method: 'get', target: 'fetchPermissions'},
26 | {path: '/roles/:id/permissions', method: 'put', target: 'updatePermissions'}
27 | ], router, roleController);
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jaron Tai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | .vscode
61 |
--------------------------------------------------------------------------------
/database/models/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const Sequelize = require('sequelize');
5 | const read = require('fs-readdir-recursive');
6 | const basename = path.basename(module.filename);
7 | const env = process.env.NODE_ENV || 'development';
8 | const config = require(__dirname + '/../../config/database.js')[env];
9 | const db = {};
10 |
11 | let sequelize;
12 | if (config.use_env_variable) {
13 | sequelize = new Sequelize(process.env[config.use_env_variable]);
14 | } else {
15 | sequelize = new Sequelize(config.database, config.username, config.password, config);
16 | }
17 |
18 | read(__dirname)
19 | .filter(function(file) {
20 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
21 | })
22 | .forEach(function(file) {
23 | var model = sequelize['import'](path.join(__dirname, file));
24 | db[model.name] = model;
25 | });
26 |
27 | Object.keys(db).forEach(function(modelName) {
28 | if (db[modelName].associate) {
29 | db[modelName].associate(db);
30 | }
31 | });
32 |
33 | db.sequelize = sequelize;
34 | db.Sequelize = Sequelize;
35 |
36 | module.exports = db;
37 |
--------------------------------------------------------------------------------
/web/src/components/layout/Navigator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ itemName }}
5 |
6 |
7 |
8 |
9 |
40 |
41 |
46 |
--------------------------------------------------------------------------------
/web/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils')
2 | var webpack = require('webpack')
3 | var config = require('../config')
4 | var merge = require('webpack-merge')
5 | var baseWebpackConfig = require('./webpack.base.conf')
6 | var HtmlWebpackPlugin = require('html-webpack-plugin')
7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
8 |
9 | // add hot-reload related code to entry chunks
10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
12 | })
13 |
14 | module.exports = merge(baseWebpackConfig, {
15 | module: {
16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
17 | },
18 | // cheap-module-eval-source-map is faster for development
19 | devtool: '#cheap-module-eval-source-map',
20 | plugins: [
21 | new webpack.DefinePlugin({
22 | 'process.env': config.dev.env
23 | }),
24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
25 | new webpack.HotModuleReplacementPlugin(),
26 | new webpack.NoEmitOnErrorsPlugin(),
27 | // https://github.com/ampedandwired/html-webpack-plugin
28 | new HtmlWebpackPlugin({
29 | filename: 'index.html',
30 | template: 'index.html',
31 | inject: true
32 | }),
33 | new FriendlyErrorsPlugin()
34 | ]
35 | })
36 |
--------------------------------------------------------------------------------
/web/build/check-versions.js:
--------------------------------------------------------------------------------
1 | var chalk = require('chalk')
2 | var semver = require('semver')
3 | var packageConfig = require('../package.json')
4 | var shell = require('shelljs')
5 | function exec (cmd) {
6 | return require('child_process').execSync(cmd).toString().trim()
7 | }
8 |
9 | var versionRequirements = [
10 | {
11 | name: 'node',
12 | currentVersion: semver.clean(process.version),
13 | versionRequirement: packageConfig.engines.node
14 | },
15 | ]
16 |
17 | if (shell.which('npm')) {
18 | versionRequirements.push({
19 | name: 'npm',
20 | currentVersion: exec('npm --version'),
21 | versionRequirement: packageConfig.engines.npm
22 | })
23 | }
24 |
25 | module.exports = function () {
26 | var warnings = []
27 | for (var i = 0; i < versionRequirements.length; i++) {
28 | var mod = versionRequirements[i]
29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
30 | warnings.push(mod.name + ': ' +
31 | chalk.red(mod.currentVersion) + ' should be ' +
32 | chalk.green(mod.versionRequirement)
33 | )
34 | }
35 | }
36 |
37 | if (warnings.length) {
38 | console.log('')
39 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
40 | console.log()
41 | for (var i = 0; i < warnings.length; i++) {
42 | var warning = warnings[i]
43 | console.log(' ' + warning)
44 | }
45 | console.log()
46 | process.exit(1)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/web/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parser: 'babel-eslint',
6 | parserOptions: {
7 | sourceType: 'module'
8 | },
9 | env: {
10 | browser: true,
11 | },
12 | globals: {
13 | },
14 | extends: 'airbnb-base',
15 | // required to lint *.vue files
16 | plugins: [
17 | 'html'
18 | ],
19 | // check if imports actually resolve
20 | 'settings': {
21 | 'import/resolver': {
22 | 'webpack': {
23 | 'config': 'build/webpack.base.conf.js'
24 | }
25 | }
26 | },
27 | // add your custom rules here
28 | 'rules': {
29 | 'dot-notation': ['off'],
30 | 'arrow-body-style': ['off'],
31 | 'no-param-reassign': ['off'],
32 | 'no-lonely-if': ['off'],
33 | 'consistent-return': ['off'],
34 | 'no-console': ['off'],
35 | 'func-names': ['off'],
36 | 'prefer-arrow-callback': ['off'],
37 | 'object-shorthand': ['off'],
38 | 'quote-props': ['off', 'as-needed'],
39 | 'comma-dangle': ['off'],
40 | // don't require .vue extension when importing
41 | 'import/extensions': ['error', 'always', {
42 | 'js': 'never',
43 | 'vue': 'never'
44 | }],
45 | // allow optionalDependencies
46 | 'import/no-extraneous-dependencies': ['error', {
47 | 'optionalDependencies': ['test/unit/index.js']
48 | }],
49 | // allow debugger during development
50 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/web/config/index.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | var path = require('path')
3 |
4 | module.exports = {
5 | build: {
6 | env: require('./prod.env'),
7 | index: path.resolve(__dirname, '../dist/index.html'),
8 | assetsRoot: path.resolve(__dirname, '../dist'),
9 | assetsSubDirectory: 'static',
10 | assetsPublicPath: '/',
11 | productionSourceMap: true,
12 | // Gzip off by default as many popular static hosts such as
13 | // Surge or Netlify already gzip all static assets for you.
14 | // Before setting to `true`, make sure to:
15 | // npm install --save-dev compression-webpack-plugin
16 | productionGzip: false,
17 | productionGzipExtensions: ['js', 'css'],
18 | // Run the build command with an extra argument to
19 | // View the bundle analyzer report after build finishes:
20 | // `npm run build --report`
21 | // Set to `true` or `false` to always turn it on or off
22 | bundleAnalyzerReport: process.env.npm_config_report
23 | },
24 | dev: {
25 | env: require('./dev.env'),
26 | port: 8080,
27 | autoOpenBrowser: true,
28 | assetsSubDirectory: 'static',
29 | assetsPublicPath: '/',
30 | proxyTable: {},
31 | // CSS Sourcemaps off by default because relative paths are "buggy"
32 | // with this option, according to the CSS-Loader README
33 | // (https://github.com/webpack/css-loader#sourcemaps)
34 | // In our experience, they generally work as expected,
35 | // just be aware of this issue when enabling this option.
36 | cssSourceMap: false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/web/src/components/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Welcome
6 | Welcome, user {{ userInfo.username }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Your Roles
17 | {{ role }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Your Permissions
27 | {{ permission }}
28 |
29 |
30 |
31 |
32 |
33 | Todos
34 | TODO1
35 | TODO2
36 | TODO3
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
55 |
56 |
57 |
60 |
--------------------------------------------------------------------------------
/database/migrations/20170717022918-admin_role.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: function (queryInterface, Sequelize) {
5 | return queryInterface.createTable('admin_role',
6 | {
7 | id: {
8 | type: Sequelize.INTEGER,
9 | primaryKey: true,
10 | autoIncrement: true
11 | },
12 | name: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | unique: true
16 | },
17 | comment: Sequelize.STRING,
18 | createdAt: {
19 | type: Sequelize.DATE
20 | },
21 | updatedAt: {
22 | type: Sequelize.DATE
23 | },
24 | deletedAt: {
25 | type: Sequelize.DATE
26 | }
27 | }).then(() => {
28 | return queryInterface.createTable('admin_user_role',
29 | {
30 | id: {
31 | type: Sequelize.INTEGER,
32 | primaryKey: true,
33 | autoIncrement: true
34 | },
35 | uid: {
36 | type: Sequelize.INTEGER,
37 | allowNull: false
38 | },
39 | rid: {
40 | type: Sequelize.INTEGER,
41 | allowNull: false
42 | },
43 | createdAt: {
44 | type: Sequelize.DATE
45 | },
46 | updatedAt: {
47 | type: Sequelize.DATE
48 | },
49 | deletedAt: {
50 | type: Sequelize.DATE
51 | }
52 | });
53 | });
54 | },
55 |
56 | down: function (queryInterface, Sequelize) {
57 | return queryInterface.dropTable('admin_role').then(() => {
58 | return queryInterface.dropTable('admin_user_role');
59 | });
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/database/migrations/20170717025438-admin_permission.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: function (queryInterface, Sequelize) {
5 | return queryInterface.createTable('admin_permission',
6 | {
7 | id: {
8 | type: Sequelize.INTEGER,
9 | primaryKey: true,
10 | autoIncrement: true
11 | },
12 | name: {
13 | type: Sequelize.STRING,
14 | allowNull: false,
15 | unique: true
16 | },
17 | comment: Sequelize.STRING,
18 | createdAt: {
19 | type: Sequelize.DATE
20 | },
21 | updatedAt: {
22 | type: Sequelize.DATE
23 | },
24 | deletedAt: {
25 | type: Sequelize.DATE
26 | }
27 | }).then(() => {
28 | return queryInterface.createTable('admin_role_permission',
29 | {
30 | id: {
31 | type: Sequelize.INTEGER,
32 | primaryKey: true,
33 | autoIncrement: true
34 | },
35 | rid: {
36 | type: Sequelize.INTEGER,
37 | allowNull: false
38 | },
39 | pid: {
40 | type: Sequelize.INTEGER,
41 | allowNull: false
42 | },
43 | createdAt: {
44 | type: Sequelize.DATE
45 | },
46 | updatedAt: {
47 | type: Sequelize.DATE
48 | },
49 | deletedAt: {
50 | type: Sequelize.DATE
51 | }
52 | });
53 | });
54 | },
55 |
56 | down: function (queryInterface, Sequelize) {
57 | return queryInterface.dropTable('admin_permission').then(() => {
58 | return queryInterface.dropTable('admin_role_permission');
59 | });
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-vue-admin",
3 | "version": "0.1.0",
4 | "description": "express-vue-admin",
5 | "main": "server.js",
6 | "files": [
7 | "server.js",
8 | "util.js",
9 | "config",
10 | "controller",
11 | "database",
12 | "middleware",
13 | "route"
14 | ],
15 | "scripts": {
16 | "test": "mocha",
17 | "start": "node server.js"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/jarontai/express-vue-admin.git"
22 | },
23 | "keywords": [
24 | "express",
25 | "vue",
26 | "admin"
27 | ],
28 | "author": {
29 | "name": "Jaron Tai",
30 | "email": "jaroncn@gmail.com"
31 | },
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/jarontai/express-vue-admin/issues"
35 | },
36 | "homepage": "https://github.com/jarontai/express-vue-admin#readme",
37 | "dependencies": {
38 | "bluebird": "^3.5.0",
39 | "body-parser": "^1.20.1",
40 | "connect-redis": "^3.3.0",
41 | "credential": "^2.0.0",
42 | "dotenv": "^4.0.0",
43 | "express": "^4.18.2",
44 | "express-session": "^1.15.3",
45 | "fs-readdir-recursive": "^1.0.0",
46 | "joi": "^10.6.0",
47 | "lodash": "^4.17.21",
48 | "moment": "^2.29.4",
49 | "morgan": "^1.8.2",
50 | "mysql2": "^1.3.5",
51 | "sequelize": "^5.22.5"
52 | },
53 | "devDependencies": {
54 | "chai": "^4.1.2",
55 | "chai-http": "^4.0.0",
56 | "eslint": "^8.29.0",
57 | "eslint-plugin-node": "^5.1.0",
58 | "mocha": "^10.1.0",
59 | "sequelize-cli": "^5.5.1"
60 | },
61 | "engines": {
62 | "node": ">=6.0.0"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/middleware/auth.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // auth中间件
4 |
5 | const apiPath = process.env.API_PATH + '/' + process.env.API_VERSION;
6 | const sessionPath = apiPath + '/admin/sessions';
7 | const AdminUser = require('../database/models')['AdminUser'];
8 |
9 | // 要求用户登录
10 | function login(req, res, next) {
11 | // 查询或创建session时无需检测登录
12 | const reqPath = req.baseUrl + req.path;
13 | if ((req.method === 'GET' || req.method === 'POST') && reqPath === sessionPath) {
14 | return next();
15 | }
16 |
17 | if (req.session.user && req.session.user.id) {
18 | req.user = req.session.user; // 将用户信息添加到request对象
19 | next();
20 | } else {
21 | next({ message: '请先登录!', status: 401 });
22 | }
23 | }
24 |
25 | // 要求用户具有某种角色
26 | function buildRoleAuth(roleName) {
27 | if (!roleName) {
28 | throw new Error('Missing or invalid role name for role auth middleware!');
29 | }
30 |
31 | const role = function (req, res, next) {
32 | if (!AdminUser) {
33 | console.error('AdminUser model is invalid!');
34 | return next({ message: 'Model initialize error', status: 500 });
35 | }
36 |
37 | const userId = req.session.user.id;
38 | AdminUser.findByPk(userId).then(user => {
39 | if (user) {
40 | user.hasRole(roleName).then(result => {
41 | if (result) {
42 | next();
43 | } else {
44 | next({ message: '无权访问!', status: 403 });
45 | }
46 | });
47 | } else {
48 | next({ message: '用户未找到!', status: 400 });
49 | }
50 | });
51 | };
52 |
53 | return [login, role]; // 先检测登录,再检查角色
54 | }
55 |
56 | module.exports = {
57 | login: login,
58 | role: buildRoleAuth
59 | };
60 |
--------------------------------------------------------------------------------
/web/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var utils = require('./utils')
3 | var config = require('../config')
4 | var vueLoaderConfig = require('./vue-loader.conf')
5 |
6 | function resolve (dir) {
7 | return path.join(__dirname, '..', dir)
8 | }
9 |
10 | module.exports = {
11 | entry: {
12 | app: './src/main.js'
13 | },
14 | output: {
15 | path: config.build.assetsRoot,
16 | filename: '[name].js',
17 | publicPath: process.env.NODE_ENV === 'production'
18 | ? config.build.assetsPublicPath
19 | : config.dev.assetsPublicPath
20 | },
21 | resolve: {
22 | extensions: ['.js', '.vue', '.json'],
23 | alias: {
24 | 'vue$': 'vue/dist/vue.esm.js',
25 | '@': resolve('src')
26 | }
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.(js|vue)$/,
32 | loader: 'eslint-loader',
33 | enforce: 'pre',
34 | include: [resolve('src'), resolve('test')],
35 | options: {
36 | formatter: require('eslint-friendly-formatter')
37 | }
38 | },
39 | {
40 | test: /\.vue$/,
41 | loader: 'vue-loader',
42 | options: vueLoaderConfig
43 | },
44 | {
45 | test: /\.js$/,
46 | loader: 'babel-loader',
47 | include: [resolve('src'), resolve('test')]
48 | },
49 | {
50 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
51 | loader: 'url-loader',
52 | options: {
53 | limit: 10000,
54 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
55 | }
56 | },
57 | {
58 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
59 | loader: 'url-loader',
60 | options: {
61 | limit: 10000,
62 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
63 | }
64 | }
65 | ]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/database/models/admin/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('lodash');
4 | const util = require('../../../util');
5 | const Promise = require('bluebird');
6 |
7 | module.exports = (sequelize, DataTypes) => {
8 | const user = sequelize.define('AdminUser', {
9 | id: {
10 | autoIncrement: true,
11 | type: DataTypes.INTEGER.UNSIGNED,
12 | primaryKey: true
13 | },
14 | username: DataTypes.STRING,
15 | disabled: DataTypes.BOOLEAN,
16 | password: DataTypes.STRING
17 | }, util.addModelCommonOptions({
18 | tableName: 'admin_user',
19 | defaultScope: {
20 | attributes: {
21 | exclude: ['password']
22 | }
23 | }
24 | }));
25 |
26 | user.prototype.hasRole = function (roleName) {
27 | return this.getRoles().then(roles => {
28 | let result = false;
29 | if (roleName && roles && roles.length) {
30 | _.forEach(roles, role => {
31 | if (role.get('name') === roleName) {
32 | result = true;
33 | return false;
34 | }
35 | });
36 | }
37 | return result;
38 | });
39 | };
40 |
41 | user.prototype.getRolePermissions = function () {
42 | const result = {
43 | roles: [],
44 | permissions: []
45 | };
46 | return this.getRoles().then((roles) => {
47 | roles = roles || [];
48 | const promises = _.map(roles, (role) => {
49 | result.roles.push(role.name);
50 | return role.getPermissions();
51 | });
52 | return Promise.all(promises).then((permissions) => {
53 | permissions = _.flatten(permissions || []);
54 | _.each(permissions, (permission) => {
55 | if (!_.includes(result.permissions, permission.name)) {
56 | result.permissions.push(permission.name);
57 | }
58 | });
59 | return result;
60 | });
61 | });
62 | };
63 |
64 | return user;
65 | };
66 |
--------------------------------------------------------------------------------
/web/build/utils.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var config = require('../config')
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
4 |
5 | exports.assetsPath = function (_path) {
6 | var assetsSubDirectory = process.env.NODE_ENV === 'production'
7 | ? config.build.assetsSubDirectory
8 | : config.dev.assetsSubDirectory
9 | return path.posix.join(assetsSubDirectory, _path)
10 | }
11 |
12 | exports.cssLoaders = function (options) {
13 | options = options || {}
14 |
15 | var cssLoader = {
16 | loader: 'css-loader',
17 | options: {
18 | minimize: process.env.NODE_ENV === 'production',
19 | sourceMap: options.sourceMap
20 | }
21 | }
22 |
23 | // generate loader string to be used with extract text plugin
24 | function generateLoaders (loader, loaderOptions) {
25 | var loaders = [cssLoader]
26 | if (loader) {
27 | loaders.push({
28 | loader: loader + '-loader',
29 | options: Object.assign({}, loaderOptions, {
30 | sourceMap: options.sourceMap
31 | })
32 | })
33 | }
34 |
35 | // Extract CSS when that option is specified
36 | // (which is the case during production build)
37 | if (options.extract) {
38 | return ExtractTextPlugin.extract({
39 | use: loaders,
40 | fallback: 'vue-style-loader'
41 | })
42 | } else {
43 | return ['vue-style-loader'].concat(loaders)
44 | }
45 | }
46 |
47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
48 | return {
49 | css: generateLoaders(),
50 | postcss: generateLoaders(),
51 | less: generateLoaders('less'),
52 | sass: generateLoaders('sass', { indentedSyntax: true }),
53 | scss: generateLoaders('sass'),
54 | stylus: generateLoaders('stylus'),
55 | styl: generateLoaders('stylus')
56 | }
57 | }
58 |
59 | // Generate loaders for standalone style files (outside of .vue)
60 | exports.styleLoaders = function (options) {
61 | var output = []
62 | var loaders = exports.cssLoaders(options)
63 | for (var extension in loaders) {
64 | var loader = loaders[extension]
65 | output.push({
66 | test: new RegExp('\\.' + extension + '$'),
67 | use: loader
68 | })
69 | }
70 | return output
71 | }
72 |
--------------------------------------------------------------------------------
/middleware/base.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // 基础中间件
4 |
5 | const _ = require('lodash');
6 |
7 | // 请求返回中间件,定义接口返回数据或异常
8 | // 成功:
9 | // {
10 | // code: 0,
11 | // message: 'success',
12 | // data: {}/[{}]
13 | // }
14 | // 异常:
15 | // {
16 | // code: 1,
17 | // message: 'reason'
18 | // }
19 | function reply(req, res, next) {
20 | function _reply(data) {
21 | if (data && typeof data.then === 'function') {
22 | _replyPromise(data);
23 | } else {
24 | _replyObj(data);
25 | }
26 | }
27 |
28 | function _replyPromise(promise) {
29 | promise.then((result) => {
30 | _replyObj(result);
31 | }).catch((err) => {
32 | _replyError(err);
33 | });
34 | }
35 |
36 | function _replyObj(data) {
37 | data = data || {};
38 | res.json({
39 | code: 0,
40 | message: 'success',
41 | data: data
42 | });
43 | }
44 |
45 | function _replyError(err) {
46 | err = err || {};
47 |
48 | console.error('Error', err);
49 |
50 | let message = err.message || err;
51 | let status = err.status || 400;
52 |
53 | // process joi error
54 | if (err.details && err.details.length) {
55 | message = _.reduce(err.details, (result, detail) => {
56 | if (result) {
57 | result += '; ';
58 | }
59 | return result + detail.message;
60 | }, '');
61 | } else if (err.errors && err.errors.length) {
62 | message = err.errors[0].message;
63 | }
64 |
65 | res.status(status).json({
66 | code: err.code || 1,
67 | message: message || err || 'Unknown error'
68 | });
69 | }
70 |
71 | res.reply = _reply;
72 | res.replyError = _replyError;
73 |
74 | next();
75 | }
76 |
77 | // 404
78 | function notFound(req, res) {
79 | res.status(404).end('Not found!');
80 | }
81 |
82 | // 通用错误处理
83 | function error(err, req, res, next) {
84 | console.error(err);
85 | if (process.env.NODE_ENV === 'production') {
86 | res.status(err.status || 500).json({ code: 1, message: 'Internal error!' });
87 | } else {
88 | res.status(err.status || 500);
89 | res.json({
90 | code: 1,
91 | message: err.message
92 | });
93 | }
94 | }
95 |
96 | module.exports = {
97 | reply: reply,
98 | notFound: notFound,
99 | error: error
100 | };
101 |
--------------------------------------------------------------------------------
/web/src/components/layout/SideMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
62 |
63 |
75 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-vue-admin",
3 | "version": "0.1.0",
4 | "description": "express-vue-admin web app",
5 | "author": "jaroncn@gmail.com",
6 | "private": true,
7 | "scripts": {
8 | "dev": "node build/dev-server.js",
9 | "start": "node build/dev-server.js",
10 | "build": "node build/build.js",
11 | "lint": "eslint --ext .js,.vue src"
12 | },
13 | "dependencies": {
14 | "view-design": "^4.0.0",
15 | "lodash": "^4.17.21",
16 | "moment": "^2.29.4",
17 | "vue": "^2.3.3",
18 | "vue-resource": "^1.5.3",
19 | "vue-router": "^2.3.1",
20 | "vuex": "^2.3.1"
21 | },
22 | "devDependencies": {
23 | "autoprefixer": "^10.4.13",
24 | "babel-core": "^6.22.1",
25 | "babel-eslint": "^7.1.1",
26 | "babel-loader": "^9.1.0",
27 | "babel-plugin-transform-runtime": "^6.22.0",
28 | "babel-preset-env": "^1.3.2",
29 | "babel-preset-stage-2": "^6.22.0",
30 | "babel-register": "^6.22.0",
31 | "chalk": "^1.1.3",
32 | "connect-history-api-fallback": "^1.3.0",
33 | "copy-webpack-plugin": "^4.0.1",
34 | "css-loader": "^6.7.2",
35 | "eslint": "^4.18.2",
36 | "eslint-config-airbnb-base": "^11.1.3",
37 | "eslint-friendly-formatter": "^2.0.7",
38 | "eslint-import-resolver-webpack": "^0.8.1",
39 | "eslint-loader": "^1.7.1",
40 | "eslint-plugin-html": "^3.0.0",
41 | "eslint-plugin-import": "^2.2.0",
42 | "eventsource-polyfill": "^0.9.6",
43 | "express": "^4.18.2",
44 | "extract-text-webpack-plugin": "^2.0.0",
45 | "file-loader": "^0.11.1",
46 | "friendly-errors-webpack-plugin": "^1.1.3",
47 | "html-webpack-plugin": "^5.5.0",
48 | "http-proxy-middleware": "^0.20.0",
49 | "opn": "^4.0.2",
50 | "optimize-css-assets-webpack-plugin": "^3.2.0",
51 | "ora": "^1.2.0",
52 | "rimraf": "^2.6.0",
53 | "semver": "^5.3.0",
54 | "shelljs": "^0.8.5",
55 | "url-loader": "^4.1.1",
56 | "vue-loader": "^17.0.1",
57 | "vue-style-loader": "^3.0.1",
58 | "vue-template-compiler": "^2.3.3",
59 | "webpack": "^5.75.0",
60 | "webpack-bundle-analyzer": "^4.7.0",
61 | "webpack-dev-middleware": "^1.10.0",
62 | "webpack-hot-middleware": "^2.25.3",
63 | "webpack-merge": "^4.1.0"
64 | },
65 | "engines": {
66 | "node": ">= 4.0.0",
67 | "npm": ">= 3.0.0"
68 | },
69 | "browserslist": [
70 | "> 1%",
71 | "last 2 versions",
72 | "not ie <= 8"
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/web/build/dev-server.js:
--------------------------------------------------------------------------------
1 | require('./check-versions')()
2 |
3 | var config = require('../config')
4 | if (!process.env.NODE_ENV) {
5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
6 | }
7 |
8 | var opn = require('opn')
9 | var path = require('path')
10 | var express = require('express')
11 | var webpack = require('webpack')
12 | var proxyMiddleware = require('http-proxy-middleware')
13 | var webpackConfig = require('./webpack.dev.conf')
14 |
15 | // default port where dev server listens for incoming traffic
16 | var port = process.env.PORT || config.dev.port
17 | // automatically open browser, if not set will be false
18 | var autoOpenBrowser = !!config.dev.autoOpenBrowser
19 | // Define HTTP proxies to your custom API backend
20 | // https://github.com/chimurai/http-proxy-middleware
21 | var proxyTable = config.dev.proxyTable
22 |
23 | var app = express()
24 | var compiler = webpack(webpackConfig)
25 |
26 | var devMiddleware = require('webpack-dev-middleware')(compiler, {
27 | publicPath: webpackConfig.output.publicPath,
28 | quiet: true
29 | })
30 |
31 | var hotMiddleware = require('webpack-hot-middleware')(compiler, {
32 | log: () => {}
33 | })
34 | // force page reload when html-webpack-plugin template changes
35 | compiler.plugin('compilation', function (compilation) {
36 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
37 | hotMiddleware.publish({ action: 'reload' })
38 | cb()
39 | })
40 | })
41 |
42 | // proxy api requests
43 | Object.keys(proxyTable).forEach(function (context) {
44 | var options = proxyTable[context]
45 | if (typeof options === 'string') {
46 | options = { target: options }
47 | }
48 | app.use(proxyMiddleware(options.filter || context, options))
49 | })
50 |
51 | // handle fallback for HTML5 history API
52 | app.use(require('connect-history-api-fallback')())
53 |
54 | // serve webpack bundle output
55 | app.use(devMiddleware)
56 |
57 | // enable hot-reload and state-preserving
58 | // compilation error display
59 | app.use(hotMiddleware)
60 |
61 | // serve pure static assets
62 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
63 | app.use(staticPath, express.static('./static'))
64 |
65 | var uri = 'http://localhost:' + port
66 |
67 | var _resolve
68 | var readyPromise = new Promise(resolve => {
69 | _resolve = resolve
70 | })
71 |
72 | console.log('> Starting dev server...')
73 | devMiddleware.waitUntilValid(() => {
74 | console.log('> Listening at ' + uri + '\n')
75 | // when env is testing, don't need open it
76 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
77 | opn(uri)
78 | }
79 | _resolve()
80 | })
81 |
82 | var server = app.listen(port)
83 |
84 | module.exports = {
85 | ready: readyPromise,
86 | close: () => {
87 | server.close()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/api.rest:
--------------------------------------------------------------------------------
1 | # api测试文件,适用于VSCODE的REST Client
2 |
3 | # admin/user
4 | GET http://localhost:3000/api/v1/admin/users HTTP/1.1
5 |
6 | POST http://localhost:3000/api/v1/admin/users HTTP/1.1
7 | content-type: application/json
8 |
9 | {
10 | "username": "test",
11 | "password": "testpwd",
12 | "roles": ["admin", "member"]
13 | }
14 |
15 | PUT http://localhost:3000/api/v1/admin/users/1 HTTP/1.1
16 | content-type: application/json
17 |
18 | {
19 | "username": "admin",
20 | "password": "adminpwd",
21 | "roles": ["admin"]
22 | }
23 |
24 | GET http://localhost:3000/api/v1/admin/users/1 HTTP/1.1
25 |
26 | DELETE http://localhost:3000/api/v1/admin/users/1 HTTP/1.1
27 |
28 | GET http://localhost:3000/api/v1/admin/users/1/roles HTTP/1.1
29 |
30 | PUT http://localhost:3000/api/v1/admin/users/1/roles HTTP/1.1
31 | content-type: application/json
32 |
33 | {
34 | "roles": [ "admin" ]
35 | }
36 |
37 | # admin/role
38 | GET http://localhost:3000/api/v1/admin/roles HTTP/1.1
39 |
40 | POST http://localhost:3000/api/v1/admin/roles HTTP/1.1
41 | content-type: application/json
42 |
43 | {
44 | "name": "admin"
45 | }
46 |
47 | PUT http://localhost:3000/api/v1/admin/roles/1 HTTP/1.1
48 | content-type: application/json
49 |
50 | {
51 | "name": "admin"
52 | }
53 |
54 | GET http://localhost:3000/api/v1/admin/roles/1 HTTP/1.1
55 |
56 | DELETE http://localhost:3000/api/v1/admin/roles/1 HTTP/1.1
57 |
58 | GET http://localhost:3000/api/v1/admin/roles/1/permissions HTTP/1.1
59 |
60 | PUT http://localhost:3000/api/v1/admin/roles/1/permissions HTTP/1.1
61 | content-type: application/json
62 |
63 | {
64 | "permissions": [ "dashboard" ]
65 | }
66 |
67 | # admin/permission
68 | GET http://localhost:3000/api/v1/admin/permissions HTTP/1.1
69 |
70 | POST http://localhost:3000/api/v1/admin/permissions HTTP/1.1
71 | content-type: application/json
72 |
73 | {
74 | "name": "admin"
75 | }
76 |
77 | PUT http://localhost:3000/api/v1/admin/permissions/1 HTTP/1.1
78 | content-type: application/json
79 |
80 | {
81 | "name": "admin"
82 | }
83 |
84 | GET http://localhost:3000/api/v1/admin/permissions/1 HTTP/1.1
85 |
86 | DELETE http://localhost:3000/api/v1/admin/permissions/1 HTTP/1.1
87 |
88 | # session
89 |
90 | GET http://localhost:3000/api/v1/sessions HTTP/1.1
91 |
92 | POST http://localhost:3000/api/v1/sessions HTTP/1.1
93 | content-type: application/json
94 |
95 | {
96 | "username": "admin",
97 | "password": "adminpwd"
98 | }
99 |
100 | DELETE http://localhost:3000/api/v1/sessions HTTP/1.1
101 |
102 | POST http://localhost:3000/api/v1/sessions/update-password HTTP/1.1
103 | content-type: application/json
104 |
105 | {
106 | "oldPassword": "testpwd",
107 | "newPassword": "testpwd1",
108 | "newPasswordRepeat": "testpwd1"
109 | }
110 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('dotenv').config();
4 |
5 | const express = require('express');
6 | const Sequelize = require('sequelize');
7 | const morgan = require('morgan');
8 | const bodyParser = require('body-parser');
9 | const session = require('express-session');
10 |
11 | let sessionStore;
12 | if (process.env.REDIS_HOST) {
13 | const RedisStore = require('connect-redis')(session);
14 | sessionStore = new RedisStore({
15 | host: process.env.REDIS_HOST,
16 | port: process.env.REDIS_PORT
17 | });
18 | }
19 |
20 | const app = express();
21 | const port = process.env.NODE_ENV === 'test' ? process.env.SERVER_PORT_TEST || 3001 : process.env.SERVER_PORT || 3000;
22 | const apiPath = process.env.API_PATH + '/' + process.env.API_VERSION;
23 | const util = require('./util');
24 | const expressListRoutes = util.expressListRoutes;
25 | const baseRouter = require('./route/base');
26 | const adminRouter = require('./route/admin');
27 | const baseMiddleware = require('./middleware/base');
28 |
29 | // 数据库
30 | const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USER, process.env.DB_PASSWORD, {
31 | host: process.env.DB_HOST || 'localhost',
32 | dialect: 'mysql',
33 | pool: {
34 | max: 5,
35 | min: 0,
36 | idle: 10000
37 | },
38 | timezone: '+08:00',
39 | logging: false
40 | });
41 |
42 | sequelize.authenticate()
43 | .then(() => {
44 | console.log('Database ok.');
45 | })
46 | .catch(err => {
47 | console.error('Database fail.', err);
48 | });
49 |
50 | // 中间件
51 | app.all('*', function (req, res, next) {
52 | // 设置cors
53 | res.header('Access-Control-Allow-Origin', req.headers.origin);
54 | res.header('Access-Control-Allow-Methods', "POST, GET, OPTIONS, DELETE, PUT");
55 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
56 | res.header('Access-Control-Allow-Credentials', 'true'); // 允许发送Cookie数据
57 | // intercept OPTIONS method
58 | if ('OPTIONS' == req.method) {
59 | res.send(200);
60 | } else {
61 | next();
62 | }
63 | });
64 | if (util.isNotProdEnv()) {
65 | app.use(morgan('dev'));
66 | }
67 | app.use(session({
68 | store: sessionStore,
69 | secret: 'express-vue-admin',
70 | resave: false,
71 | saveUninitialized: false
72 | }));
73 | app.use(bodyParser.json());
74 | app.use(baseMiddleware.reply);
75 |
76 | // 路由
77 | app.use(apiPath + '/', baseRouter);
78 | app.use(apiPath + '/admin', adminRouter);
79 |
80 | // 打印路由
81 | if (util.isNotProdEnv()) {
82 | expressListRoutes({}, 'ROOT:', baseRouter);
83 | expressListRoutes({ prefix: '/admin' }, 'ADMIN:', adminRouter);
84 | }
85 |
86 | // 错误处理
87 | app.use(baseMiddleware.notFound);
88 | app.use(baseMiddleware.error);
89 |
90 | // 启动
91 | app.listen(port, () => {
92 | console.log(`Server listening at - ${apiPath} : ${port}`);
93 | });
94 |
95 | module.exports = app;
96 |
--------------------------------------------------------------------------------
/database/seeders/20170715071127-admin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('dotenv').config();
4 |
5 | const Promise = require('bluebird');
6 | const credential = require('credential');
7 | const pw = credential();
8 |
9 | const models = require('../models');
10 | const AdminUser = models['AdminUser'];
11 | const AdminRole = models['AdminRole'];
12 | const AdminPermission = models['AdminPermission'];
13 |
14 | const adminPwd = process.env.ADMIN_SEED_PASSWORD || 'adminpwd';
15 | const testPwd = process.env.TEST_SEED_PASSWORD || 'testpwd';
16 |
17 | module.exports = {
18 | up: function () {
19 | let adminUser, testUser, adminRole, memberRole;
20 | return pw.hash(adminPwd).then((hash) => {
21 | return AdminUser.create({
22 | username: 'admin',
23 | password: hash,
24 | }).then((admin) => {
25 | adminUser = admin;
26 | return pw.hash(testPwd).then((hash) => {
27 | return AdminUser.create({
28 | username: 'test',
29 | password: hash,
30 | });
31 | });
32 | }).then((user) => {
33 | testUser = user;
34 | return AdminRole.create({
35 | name: 'admin',
36 | comment: '管理员'
37 | }).then((role) => {
38 | adminRole = role;
39 | return AdminRole.create({
40 | name: 'member',
41 | comment: '普通用户'
42 | });
43 | }).then((role) => {
44 | memberRole = role;
45 | return adminUser.setRoles([memberRole, adminRole]).then(() => {
46 | return testUser.setRoles([memberRole]);
47 | });
48 | });
49 | }).then(() => {
50 | return Promise.mapSeries([
51 | {
52 | name: 'dashboard',
53 | comment: 'Dashboard'
54 | },
55 | {
56 | name: 'admin',
57 | comment: '后台管理'
58 | },
59 | {
60 | name: 'admin:user',
61 | comment: '后台管理:用户'
62 | },
63 | {
64 | name: 'admin:role',
65 | comment: '后台管理:角色'
66 | },
67 | {
68 | name: 'admin:permission',
69 | comment: '后台管理:权限'
70 | }
71 | ], (data) => {
72 | return AdminPermission.create(data);
73 | }).then((permissions) => {
74 | return memberRole.setPermissions([permissions[0]]).then(() => {
75 | return adminRole.setPermissions(permissions);
76 | });
77 | });
78 | });
79 | });
80 | },
81 |
82 | down: function (queryInterface) {
83 | return queryInterface.bulkDelete('admin_user', null, {}).then(() => {
84 | return queryInterface.bulkDelete('admin_role', null, {});
85 | }).then(() => {
86 | return queryInterface.bulkDelete('admin_permission', null, {});
87 | }).then(() => {
88 | return queryInterface.bulkDelete('admin_user_role', null, {});
89 | }).then(() => {
90 | return queryInterface.bulkDelete('admin_role_permission', null, {});
91 | });
92 | }
93 | };
94 |
--------------------------------------------------------------------------------
/web/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import 'view-design/dist/styles/iview.css';
4 | import iView from 'view-design';
5 | import Vue from 'vue';
6 | import Vuex from 'vuex';
7 | import VueResource from 'vue-resource';
8 | import App from './App';
9 | import EventBus from './event_bus';
10 | import router from './router';
11 |
12 | Vue.use(Vuex);
13 | Vue.use(VueResource);
14 | Vue.use(iView);
15 |
16 | Vue.config.productionTip = false;
17 |
18 | Vue.http.options.root = 'http://localhost:3000/api/v1';
19 |
20 | Vue.http.interceptors.push(function (request, next) {
21 | request.credentials = true; // 允许发送cookie
22 |
23 | this.$Loading.start();
24 |
25 | next(function (response) {
26 | // 处理http请求异常
27 | const data = response.data;
28 | if (data && data.code > 0) {
29 | this.$Message.warning({
30 | content: data.message || '后台未知错误',
31 | duration: 5
32 | });
33 | this.$Loading.error();
34 | } else if (response && response.status !== 200) {
35 | let message = response.statusText;
36 | switch (response.status) {
37 | case 404:
38 | message = '页面或接口不存在!';
39 | break;
40 | case 500:
41 | message = '服务器内部错误!';
42 | break;
43 | default:
44 | break;
45 | }
46 | this.$Message.warning({
47 | content: message || '后台未知错误',
48 | duration: 5
49 | });
50 | this.$Loading.error();
51 | } else {
52 | this.$Loading.finish();
53 | }
54 | });
55 | });
56 |
57 | const store = new Vuex.Store({
58 | state: {
59 | user: {
60 | userInfo: null,
61 | roles: [],
62 | permissions: []
63 | }
64 | },
65 | mutations: {
66 | /* eslint-disable */
67 | clearUser(state) {
68 | state.user.userInfo = null;
69 | state.user.roles = [];
70 | state.user.permissions = [];
71 | },
72 | updateUser(state, data) {
73 | if (data.id && data.username) {
74 | state.user.userInfo = {
75 | id: data.id,
76 | username: data.username
77 | };
78 | state.user.roles = data.roles || [];
79 | state.user.permissions = data.permissions || [];
80 | }
81 | }
82 | }
83 | });
84 |
85 | router.beforeEach((to, from, next) => {
86 | const path = to.path.substr(1);
87 | const pathPermission = path.split('/').join(':');
88 | const permissions = store.state.user.permissions || [];
89 | if (permissions.indexOf(pathPermission) < 0) {
90 | // 禁止访问无权限页面
91 | next(false);
92 | } else {
93 | next();
94 | }
95 | });
96 |
97 | router.afterEach((to, from) => {
98 | EventBus.$emit('route-change', {
99 | to: to,
100 | from: from
101 | });
102 | });
103 |
104 | new Vue({
105 | el: '#app',
106 | store,
107 | router,
108 | template: '',
109 | components: { App },
110 | });
111 | /* eslint-enable */
112 |
--------------------------------------------------------------------------------
/web/src/components/layout/TopMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
86 |
87 |
90 |
--------------------------------------------------------------------------------
/controller/rest.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const joi = require('joi');
4 | const _ = require('lodash');
5 |
6 | const BaseController = require('./base');
7 |
8 | /**
9 | * REST控制器基类,提供默认的Rest请求方法,必须绑定模型
10 | */
11 | class RestController extends BaseController {
12 | /**
13 | * Creates an instance of RestController.
14 | *
15 | * @param {any} modelName
16 | *
17 | */
18 | constructor(modelName) {
19 | super();
20 |
21 | // rest操作参数校验规则
22 | this.restRules = {};
23 | // 示例
24 | // this.restRules = {
25 | // create: {
26 | // name: joi.string().min(3).required(),
27 | // comment: joi.string().min(2).required()
28 | // },
29 | // update: {
30 | // name: joi.string().min(3),
31 | // comment: joi.string().min(2)
32 | // }
33 | // };
34 |
35 | // 绑定模型
36 | if (modelName) {
37 | this.modelName = modelName;
38 | this.model = this.models[modelName];
39 | if (!this.model) {
40 | throw new Error(`The model ${modelName} for rest controller is missing or invalid!`);
41 | }
42 | } else {
43 | throw new Error('Rest controller should bind to a model!');
44 | }
45 | }
46 |
47 | /**
48 | * 分页返回所有对象
49 | */
50 | index(req, res) {
51 | const params = req.query || {};
52 | const data = {
53 | offset: +params.offset || 0,
54 | limit: +params.limit || 10
55 | };
56 | if (params.where && _.isObject(params.where)) {
57 | data.where = params.where;
58 | }
59 | res.reply(this.model.findAndCountAll(data));
60 | }
61 |
62 | /**
63 | * 创建对象
64 | */
65 | create(req, res) {
66 | let data = req.body;
67 | if (this.restRules.create) {
68 | const validate = joi.validate(req.body, this.restRules.create);
69 | if (validate.error) {
70 | return res.replyError(validate.error);
71 | }
72 | data = validate.value;
73 | }
74 | res.reply(this.model.create(data));
75 | }
76 |
77 | /**
78 | * 更新对象
79 | */
80 | update(req, res) {
81 | if (!req.params || !req.params.id) {
82 | return res.replyError('missing id parameter');
83 | }
84 |
85 | let data = req.body;
86 | if (this.restRules.update) {
87 | const validate = joi.validate(req.body, this.restRules.update);
88 | if (validate.error) {
89 | return res.replyError(validate.error);
90 | }
91 | data = validate.value;
92 | }
93 | res.reply(this.model.update(data, { where: { id: req.params.id } }));
94 | }
95 |
96 | /**
97 | * 查找单个对象
98 | */
99 | show(req, res) {
100 | if (!req.params || !req.params.id) {
101 | return res.replyError('missing id parameter');
102 | }
103 | res.reply(this.model.findByPk(req.params.id));
104 | }
105 |
106 | /**
107 | * 删除单个对象
108 | */
109 | destroy(req, res) {
110 | if (!req.params || !req.params.id) {
111 | return res.replyError('missing id parameter');
112 | }
113 |
114 | this.model.findByPk(req.params.id).then((obj) => {
115 | if (obj) {
116 | res.reply(obj.destroy());
117 | } else {
118 | res.replyError(this.modelName + ' not found');
119 | }
120 | });
121 | }
122 | }
123 |
124 | module.exports = RestController;
125 |
--------------------------------------------------------------------------------
/controller/session.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const joi = require('joi');
4 | const credential = require('credential');
5 | const pw = credential();
6 | const Promise = require('bluebird');
7 |
8 | const BaseController = require('./base');
9 |
10 | class SessionController extends BaseController {
11 | /**
12 | * check session
13 | */
14 | index(req, res) {
15 | res.reply(req.session.user ? [req.session.user] : []);
16 | }
17 |
18 | /**
19 | * create session(login)
20 | */
21 | create(req, res) {
22 | const rules = {
23 | username: joi.string().min(3).required(),
24 | password: joi.string().min(3).required()
25 | };
26 | const { error, value } = joi.validate(req.body, rules);
27 | if (error) {
28 | return res.replyError(error);
29 | }
30 |
31 | const AdminUser = this.models['AdminUser'];
32 | const result = AdminUser.findOne({ where: { username: value.username }, attributes: { include: ['id', 'password'] } }).then((user) => {
33 | if (user) {
34 | return pw.verify(user.password, value.password).then((result) => {
35 | if (result) {
36 | const userData = {
37 | id: user.id,
38 | username: user.username
39 | };
40 | req.session.user = userData;
41 |
42 | return user.getRolePermissions().then((result) => {
43 | userData.roles = result.roles || [];
44 | userData.permissions = result.permissions || [];
45 | return userData;
46 | });
47 | } else {
48 | req.session.destroy();
49 | return Promise.reject('用户名或密码错误,登录失败!');
50 | }
51 | });
52 | } else {
53 | return Promise.reject('用户不存在!');
54 | }
55 | });
56 | return res.reply(result);
57 | }
58 |
59 | /**
60 | * delete session(logout)
61 | */
62 | destroy(req, res) {
63 | req.session.destroy();
64 | return res.reply();
65 | }
66 |
67 | // 更新用户密码
68 | updatePassword(req, res) {
69 | const rules = {
70 | oldPassword: joi.string().min(6).required(),
71 | newPassword: joi.string().min(6).required(),
72 | newPasswordRepeat: joi.string().min(6).required()
73 | };
74 | const { error, value } = joi.validate(req.body, rules);
75 | if (error) {
76 | return res.replyError(error);
77 | }
78 | if (value.newPassword !== value.newPasswordRepeat) {
79 | return res.replyError('两个新密码不一致!');
80 | }
81 |
82 | const AdminUser = this.models['AdminUser'];
83 | const userId = req.user.id;
84 | const result = AdminUser.findByPk(userId, { attributes: { include: ['password'] } }).then((user) => {
85 | if (user) {
86 | return pw.verify(user.password, value.oldPassword).then((result) => {
87 | if (result) {
88 | return pw.hash(value.newPassword).then((hash) => {
89 | return user.update({
90 | password: hash
91 | }).then(() => { });
92 | });
93 | } else {
94 | return Promise.reject('旧密码错误!');
95 | }
96 | });
97 | } else {
98 | return Promise.reject('用户不存在!');
99 | }
100 | });
101 | res.reply(result);
102 | }
103 | }
104 |
105 | module.exports = new SessionController();
106 |
--------------------------------------------------------------------------------
/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('lodash');
4 |
5 | module.exports = {
6 | // 根据配置构建路由 - [{path, method, target, middlewares }]
7 | buildRoute: (routeArr, router, controller) => {
8 | if (router && routeArr && routeArr.length && controller) {
9 | _.each(routeArr, (route) => {
10 | router[route.method](route.path, route.middlewares || [], (req, res) => {
11 | return controller[route.target](req, res);
12 | });
13 | });
14 | }
15 | },
16 |
17 | // rest路由
18 | restRoute: (path, router, controller) => {
19 | path = path || '';
20 | router.get(path+'/', (req, res) => {
21 | controller.index(req, res);
22 | }).get(path+'/:id', (req, res) => {
23 | controller.show(req, res);
24 | }).post(path+'/', (req, res) => {
25 | controller.create(req, res);
26 | }).put(path+'/:id', (req, res) => {
27 | controller.update(req, res);
28 | }).delete(path+'/:id', (req, res) => {
29 | controller.destroy(req, res);
30 | });
31 | },
32 |
33 | // 是生产环境
34 | isProdEnv() {
35 | return process.env.NODE_ENV === 'production';
36 | },
37 |
38 | // 非生产环境
39 | isNotProdEnv() {
40 | return process.env.NODE_ENV !== 'production';
41 | },
42 |
43 | // 设置模型通用option
44 | addModelCommonOptions: (options) => {
45 | if (options) {
46 | options.freezeTableName = true;
47 | options.timestamps = true;
48 | options.paranoid = true;
49 |
50 | options.charset = options.charset || 'utf8';
51 | options.collate = options.collate || 'utf8_general_ci';
52 |
53 | options.defaultScope = options.defaultScope || {};
54 | options.defaultScope.attributes = options.defaultScope.attributes || {};
55 | options.defaultScope.attributes.exclude = options.defaultScope.attributes.exclude || [];
56 | options.defaultScope.attributes.exclude.push('deletedAt');
57 | }
58 | return options;
59 | },
60 |
61 | expressListRoutes: () => {
62 | const options = {
63 | prefix: '',
64 | spacer: 7
65 | };
66 |
67 | function spacer(x) {
68 | var res = '';
69 | while (x--) res += ' ';
70 | return res;
71 | }
72 |
73 | function colorMethod(method) {
74 | switch (method) {
75 | case ('POST'): return method.yellow;
76 | case ('GET'): return method.green;
77 | case ('PUT'): return method.blue;
78 | case ('DELETE'): return method.red;
79 | case ('PATCH'): return method.grey;
80 | default: return method;
81 | }
82 | }
83 |
84 | _.each(arguments, function (arg) {
85 | if (_.isString(arg)) {
86 | console.info(arg.magenta);
87 | } else if (_.isObject(arg)) {
88 | if (!arg.stack) {
89 | _.assign(options, arg);
90 | } else {
91 | _.each(arg.stack, function (stack) {
92 | if (stack.route) {
93 | var route = stack.route,
94 | methodsDone = {};
95 | _.each(route.stack, function (r) {
96 | var method = r.method ? r.method.toUpperCase() : null;
97 | if (!methodsDone[method] && method) {
98 | console.info(colorMethod(method), spacer(options.spacer - method.length), options.prefix + route.path);
99 | methodsDone[method] = true;
100 | }
101 | });
102 | }
103 | });
104 | }
105 | }
106 | });
107 | }
108 | };
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 简体中文 | [English](./README_EN.md)
2 |
3 | # express-vue-admin
4 |
5 | 使用 Node.js(Express.js), Vue2 开发的管理后台脚手架项目
6 |
7 | ## 特点
8 | * 全栈 Javascript 应用
9 | * 使用 Express.js 构建,清晰且可测试的 rest api
10 | * 最小化的用户/角色/权限管理功能
11 | * 使用 iview 框架构建的简洁后台界面
12 |
13 | ## 组件
14 |
15 | express-vue-admin 使用了很多组件(库)来构建后端接口和前端UI:
16 |
17 | ### 后端
18 | * [express](https://expressjs.com/) - 后端web框架
19 | * [sequelize](http://docs.sequelizejs.com/) - 数据库ORM
20 | * [joi](https://github.com/hapijs/joi) - 参数校验
21 | * [dotenv](https://github.com/motdotla/dotenv) - 环境配置
22 | * [mocha](https://mochajs.org/)/[chai](http://chaijs.com/)/[chai-http](https://github.com/chaijs/chai-http) - 接口测试相关组件
23 | * mysql - 数据库
24 | * redis - 缓存
25 | * ...
26 |
27 | ### 前端
28 | * [vue2](https://vuejs.org/) - 前端JS框架
29 | * [iview](https://www.iviewui.com/) - 前端UI框架
30 | * [vue-resource](https://github.com/pagekit/vue-resource)/[vue-router](https://github.com/vuejs/vue-router)/[vuex](https://github.com/vuejs/vuex) - vue 相关的路由、状态管理等组件
31 | * ...
32 |
33 | ## 文件说明
34 |
35 | .
36 | ├── .env.example 环境配置示例
37 | ├── .sequelizerc sequelize配置
38 | ├── screenshots/ 应用运行截图
39 | ├── web/ vue web应用
40 | ├── test/ 接口测试文件
41 | ├── server.js 服务器
42 | ├── middleware/ 中间件
43 | | ├── base.js 基础中间件
44 | | ├── auth.js 鉴权中间件
45 | | └── ... 其他业务中间件
46 | ├── route/ 路由
47 | | ├── base.js 基础路由
48 | | ├── admin.js admin模块路由
49 | | └── ... 其他路由
50 | ├── controller/ 控制器
51 | | ├── base.js 基础控制器
52 | | ├── rest.js rest基础控制器
53 | | ├── session.js session控制器
54 | | ├── admin/ admin模块控制器
55 | | └── ... 其他业务模块控制器
56 | ├── database/ sequelize数据库文件
57 | | ├── models/ 模型
58 | | └── migrations/ migration文件
59 | | └── seeders/ seeder文件
60 | ├── util.js 工具
61 | └── config/ 配置
62 | └── database.js sequelize-cli数据库配置
63 |
64 | ## 运行截图
65 |
66 | ### login
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | ### admin/user
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | ### admin/role
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | ### admin/role delete
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | ## 运行
100 |
101 | 1. 安装redis,用于存储session (可选)
102 |
103 | 2. 复制.env.example到.env,并对各个项目进行配置 (不配置redis,session将保存在内存中,生产环境不推荐)
104 | ```
105 | #server
106 | NODE_ENV=development 环境配置
107 | SERVER_PORT=3000 服务器端口
108 | API_PATH=/api 接口基路径
109 | API_VERSION=v1 接口版本
110 |
111 | #db
112 | DB_HOST=localhost 数据库host
113 | DB_DATABASE=admin 数据库名称
114 | DB_USER=root 数据库用户
115 | DB_PASSWORD=root 数据库密码
116 |
117 | #redis
118 | REDIS_HOST=localhost redis缓存host
119 | REDIS_PORT=6379 redis端口
120 |
121 | #misc
122 | ADMIN_SEED_PASSWORD=adminpwd admin帐号密码
123 | TEST_SEED_PASSWORD=testpwd 测试帐号密码
124 | SERVER_PORT_TEST=3001 单元测试服务器端口
125 |
126 | ```
127 |
128 | 3. 安装依赖、初始化数据库、填充seed数据:
129 | ```
130 | $ npm install // 安装依赖
131 | $ npx sequelize db:migrate // 数据库结构构建
132 | $ npx sequelize db:seed:all // 数据库数据填充
133 | ```
134 |
135 | 4. 运行server和web应用
136 | ```
137 | $ npm start // 开启后端服务
138 |
139 | $ cd ./web // 进入web文件夹
140 | $ npm install // 安装依赖
141 | $ npm run dev // 运行web应用
142 | ```
143 |
144 | ## 测试
145 |
146 | 基本的接口测试:
147 |
148 | ```
149 | $ npm run test
150 | ```
151 |
152 | ## License
153 |
154 | MIT
155 |
--------------------------------------------------------------------------------
/web/src/components/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | express-vue-admin
5 |
6 |
7 |
8 |
9 |
10 |
11 | 请登录
12 |
13 |
14 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | | admin / adminpwd |
35 |
36 |
37 | | test / testpwd |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
108 |
109 |
110 |
148 |
--------------------------------------------------------------------------------
/web/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 | {{ errorMessage }}
15 |
16 |
17 |
18 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | test
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
99 |
100 |
158 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | English | [简体中文](./README.md)
2 |
3 | # express-vue-admin
4 |
5 | Minimal admin app build with Node.js and Vue2.
6 |
7 | ## Features
8 | * Fullstack JavaScript
9 | * Lean and tested rest api powered by Express.js
10 | * Minimal but fully work user/role/permission management
11 | * Clean UI build with the iview framework
12 |
13 | ## Components
14 |
15 | express-vue-admin use lots of components to build the api server and frontend ui:
16 |
17 | ### Backend
18 | * [express](https://expressjs.com/) - backend framework
19 | * [sequelize](http://docs.sequelizejs.com/) - database ORM
20 | * [joi](https://github.com/hapijs/joi) - validation
21 | * [dotenv](https://github.com/motdotla/dotenv) - env config
22 | * [mocha](https://mochajs.org/)/[chai](http://chaijs.com/)/[chai-http](https://github.com/chaijs/chai-http) - test toolchain
23 | * mysql - database
24 | * redis - cache
25 | * ...
26 |
27 | ### Frontend
28 | * [vue2](https://vuejs.org/) - main js framework
29 | * [iview](https://www.iviewui.com/) - ui framework for vue
30 | * [vue-resource](https://github.com/pagekit/vue-resource)/[vue-router](https://github.com/vuejs/vue-router)/[vuex](https://github.com/vuejs/vuex) - vue friends
31 | * ...
32 |
33 | ## Files
34 |
35 | .
36 | ├── .env.example env example
37 | ├── .sequelizerc sequelize rc file
38 | ├── screenshots/ screenshots
39 | ├── web/ vue2 web app
40 | ├── test/ test files
41 | ├── server.js server
42 | ├── middleware/ middlewares
43 | | ├── base.js base middleware
44 | | ├── auth.js auth middleware
45 | | └── ...
46 | ├── route/ routes
47 | | ├── base.js base route
48 | | ├── admin.js admin route
49 | | └── ...
50 | ├── controller/ controllers
51 | | ├── base.js base controller
52 | | ├── rest.js rest controller
53 | | ├── session.js session controller
54 | | ├── admin/ admin controller
55 | | └── ...
56 | ├── database/ sequelize files
57 | | ├── models/ models
58 | | └── migrations/ migration files
59 | | └── seeders/ seeder files
60 | ├── util.js util
61 | └── config/ config
62 | └── database.js sequelize-cli config
63 |
64 | ## Screenshots
65 |
66 | ### login
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | ### admin/user
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | ### admin/role
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | ### admin/role delete
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | ## How to run
100 |
101 | 1. Install redis (optional)
102 |
103 | 2. copy .env.example to .env (skip redis config would make session store in memory!)
104 | ```
105 | #server
106 | NODE_ENV=development
107 | SERVER_PORT=3000
108 | API_PATH=/api
109 | API_VERSION=v1
110 |
111 | #db
112 | DB_HOST=localhost
113 | DB_DATABASE=admin
114 | DB_USER=root
115 | DB_PASSWORD=root
116 |
117 | #redis
118 | REDIS_HOST=localhost (optional)
119 | REDIS_PORT=6379 (optional)
120 |
121 | #misc
122 | ADMIN_SEED_PASSWORD=adminpwd
123 | TEST_SEED_PASSWORD=testpwd
124 | SERVER_PORT_TEST=3001
125 |
126 | ```
127 |
128 | 3. install deps, do migration:
129 | ```
130 | $ npm install
131 | $ npx sequelize db:migrate
132 | $ npx sequelize db:seed:all
133 | ```
134 |
135 | 4. run server and web app
136 | ```
137 | $ npm start
138 |
139 | $ cd ./web
140 | $ npm install
141 | $ npm run dev
142 | ```
143 |
144 | ## Test
145 |
146 | Run basic api test:
147 |
148 | ```
149 | $ npm run test
150 | ```
151 |
152 | ## License
153 |
154 | MIT
155 |
--------------------------------------------------------------------------------
/web/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var utils = require('./utils')
3 | var webpack = require('webpack')
4 | var config = require('../config')
5 | var merge = require('webpack-merge')
6 | var baseWebpackConfig = require('./webpack.base.conf')
7 | var CopyWebpackPlugin = require('copy-webpack-plugin')
8 | var HtmlWebpackPlugin = require('html-webpack-plugin')
9 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
11 |
12 | var env = config.build.env
13 |
14 | var webpackConfig = merge(baseWebpackConfig, {
15 | module: {
16 | rules: utils.styleLoaders({
17 | sourceMap: config.build.productionSourceMap,
18 | extract: true
19 | })
20 | },
21 | devtool: config.build.productionSourceMap ? '#source-map' : false,
22 | output: {
23 | path: config.build.assetsRoot,
24 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
25 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
26 | },
27 | plugins: [
28 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
29 | new webpack.DefinePlugin({
30 | 'process.env': env
31 | }),
32 | new webpack.optimize.UglifyJsPlugin({
33 | compress: {
34 | warnings: false
35 | },
36 | sourceMap: true
37 | }),
38 | // extract css into its own file
39 | new ExtractTextPlugin({
40 | filename: utils.assetsPath('css/[name].[contenthash].css')
41 | }),
42 | // Compress extracted CSS. We are using this plugin so that possible
43 | // duplicated CSS from different components can be deduped.
44 | new OptimizeCSSPlugin({
45 | cssProcessorOptions: {
46 | safe: true
47 | }
48 | }),
49 | // generate dist index.html with correct asset hash for caching.
50 | // you can customize output by editing /index.html
51 | // see https://github.com/ampedandwired/html-webpack-plugin
52 | new HtmlWebpackPlugin({
53 | filename: config.build.index,
54 | template: 'index.html',
55 | inject: true,
56 | minify: {
57 | removeComments: true,
58 | collapseWhitespace: true,
59 | removeAttributeQuotes: true
60 | // more options:
61 | // https://github.com/kangax/html-minifier#options-quick-reference
62 | },
63 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
64 | chunksSortMode: 'dependency'
65 | }),
66 | // split vendor js into its own file
67 | new webpack.optimize.CommonsChunkPlugin({
68 | name: 'vendor',
69 | minChunks: function (module, count) {
70 | // any required modules inside node_modules are extracted to vendor
71 | return (
72 | module.resource &&
73 | /\.js$/.test(module.resource) &&
74 | module.resource.indexOf(
75 | path.join(__dirname, '../node_modules')
76 | ) === 0
77 | )
78 | }
79 | }),
80 | // extract webpack runtime and module manifest to its own file in order to
81 | // prevent vendor hash from being updated whenever app bundle is updated
82 | new webpack.optimize.CommonsChunkPlugin({
83 | name: 'manifest',
84 | chunks: ['vendor']
85 | }),
86 | // copy custom static assets
87 | new CopyWebpackPlugin([
88 | {
89 | from: path.resolve(__dirname, '../static'),
90 | to: config.build.assetsSubDirectory,
91 | ignore: ['.*']
92 | }
93 | ])
94 | ]
95 | })
96 |
97 | if (config.build.productionGzip) {
98 | var CompressionWebpackPlugin = require('compression-webpack-plugin')
99 |
100 | webpackConfig.plugins.push(
101 | new CompressionWebpackPlugin({
102 | asset: '[path].gz[query]',
103 | algorithm: 'gzip',
104 | test: new RegExp(
105 | '\\.(' +
106 | config.build.productionGzipExtensions.join('|') +
107 | ')$'
108 | ),
109 | threshold: 10240,
110 | minRatio: 0.8
111 | })
112 | )
113 | }
114 |
115 | if (config.build.bundleAnalyzerReport) {
116 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
117 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
118 | }
119 |
120 | module.exports = webpackConfig
121 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('dotenv').config();
4 |
5 | process.env.NODE_ENV='test';
6 |
7 | const apiPath = process.env.API_PATH + '/' + process.env.API_VERSION;
8 |
9 | const server = require('../server');
10 | const chai = require('chai');
11 | const chaiHttp = require('chai-http');
12 | const expect = chai.expect;
13 |
14 | chai.use(chaiHttp);
15 |
16 | const agent = chai.request.agent(server);
17 |
18 | describe('index', () => {
19 | it('should return index', function(done) {
20 | agent.get(apiPath)
21 | .end((err, res) => {
22 | expect(res).to.have.status(200);
23 | done();
24 | });
25 | });
26 | });
27 |
28 | describe('sessions creation', () => {
29 | it('should get empty session', function(done) {
30 | agent.get(apiPath+'/sessions')
31 | .end((err, res) => {
32 | expect(res).to.have.status(200);
33 | expect(res.body).to.have.property('data').with.length(0);
34 | done();
35 | });
36 | });
37 |
38 | it('should create session', function(done) {
39 | this.timeout(6000); // 密码加密费时较长
40 |
41 | agent.post(apiPath+'/sessions')
42 | .send({
43 | "username": "admin",
44 | "password": "adminpwd"
45 | })
46 | .end((err, res) => {
47 | expect(res).to.have.status(200);
48 | expect(res.body).to.have.property('data').with.to.have.all.keys('id', 'username', 'roles', 'permissions');
49 | done();
50 | });
51 | });
52 | });
53 |
54 | describe('users', () => {
55 | it('should return user list', function(done) {
56 | agent.get(apiPath + '/admin/users')
57 | .end((err, res) => {
58 | expect(res).to.have.status(200);
59 | expect(res.body).to.have.property('data').with.to.have.property('count').least(1);
60 | expect(res.body).to.have.property('data').to.have.property('rows').to.have.lengthOf.least(1);
61 | expect(res.body.data.rows[0]).to.have.all.keys('id', 'username', 'roles', 'createdAt', 'updatedAt', 'disabled');
62 | done();
63 | });
64 | });
65 |
66 | it('should return admin user', function(done) {
67 | agent.get(apiPath + '/admin/users/1')
68 | .end((err, res) => {
69 | expect(res).to.have.status(200);
70 | expect(res.body).to.have.property('data').to.have.property('username').to.equal('admin');
71 | done();
72 | });
73 | });
74 |
75 | it('should update admin user', function(done) {
76 | this.timeout(6000); // 密码加密费时较长
77 |
78 | agent.put(apiPath + '/admin/users/1')
79 | .send({
80 | "username": "admin",
81 | "password": "adminpwd",
82 | "roles": ["admin"]
83 | })
84 | .end((err, res) => {
85 | expect(res).to.have.status(200);
86 | expect(res.body).to.have.property('data');
87 | done();
88 | });
89 | });
90 |
91 | it('should return user roles', function(done) {
92 | agent.get(apiPath + '/admin/users/1/roles')
93 | .end((err, res) => {
94 | expect(res).to.have.status(200);
95 | expect(res.body).to.have.property('data').to.have.lengthOf.above(0);
96 | done();
97 | });
98 | });
99 | });
100 |
101 | describe('roles', () => {
102 | it('should return role list', function(done) {
103 | agent.get(apiPath + '/admin/roles')
104 | .end((err, res) => {
105 | expect(res).to.have.status(200);
106 | expect(res.body).to.have.property('data').with.to.have.property('count').above(1);
107 | expect(res.body).to.have.property('data').to.have.property('rows').to.have.lengthOf.above(1);
108 | expect(res.body.data.rows[0]).to.have.all.keys('id', 'name', 'comment', 'permissions', 'createdAt', 'updatedAt');
109 | done();
110 | });
111 | });
112 | });
113 |
114 | describe('permissions', () => {
115 | it('should return permission list', function (done) {
116 | agent.get(apiPath + '/admin/permissions')
117 | .end((err, res) => {
118 | expect(res).to.have.status(200);
119 | expect(res.body).to.have.property('data').with.to.have.property('count').above(1);
120 | expect(res.body).to.have.property('data').to.have.property('rows').to.have.lengthOf.above(1);
121 | expect(res.body.data.rows[0]).to.have.all.keys('id', 'name', 'comment', 'createdAt', 'updatedAt');
122 | done();
123 | });
124 | });
125 | });
126 |
127 | describe('sessions delete', () => {
128 | it('should delete session', function(done) {
129 | agent.del(apiPath + '/sessions')
130 | .end((err, res) => {
131 | expect(res).to.have.status(200);
132 | done();
133 | });
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/controller/admin/role.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('lodash');
4 | const joi = require('joi');
5 |
6 | const RestController = require('../rest');
7 |
8 | class RoleController extends RestController {
9 | constructor() {
10 | super('AdminRole');
11 |
12 | this.defaultPermission = 'dashboard';
13 |
14 | const AdminPermission = this.models['AdminPermission'];
15 | AdminPermission.findOne({ where: { name: this.defaultPermission } }).then((result) => {
16 | if (!result) {
17 | throw new Error('Failed to load the default permission! Should run sequelize seeder first!');
18 | }
19 | });
20 | }
21 |
22 | /**
23 | * 分页返回所有对象
24 | */
25 | index(req, res) {
26 | const params = req.query || {};
27 | const data = {
28 | offset: +params.offset || 0,
29 | limit: +params.limit || 10
30 | };
31 | if (params.where && _.isObject(params.where)) {
32 | data.where = params.where;
33 | }
34 | const AdminPermission = this.models['AdminPermission'];
35 | data.include = [{ model: AdminPermission, as: 'permissions' }];
36 | data.distinct = true;
37 | res.reply(this.model.findAndCountAll(data));
38 | }
39 |
40 | /**
41 | * 创建对象
42 | */
43 | create(req, res) {
44 | const rules = {
45 | name: joi.string().min(3).required(),
46 | comment: joi.string().min(2).required(),
47 | permissions: joi.array().default([]),
48 | };
49 | const { error, value } = joi.validate(req.body, rules);
50 | if (error) {
51 | return res.replyError(error);
52 | }
53 |
54 | const AdminPermission = this.models['AdminPermission'];
55 | const permissions = value.permissions;
56 | permissions.push(this.defaultPermission);
57 |
58 | const result = AdminPermission.findAll({
59 | where: { name: permissions }
60 | }).then((permissions) => {
61 | delete value.permissions;
62 | return this.sequelize.transaction((t) => {
63 | return this.model.create(value, { transaction: t }).then((role) => {
64 | return role.setPermissions(permissions, { transaction: t }).then(() => { });
65 | });
66 | });
67 | });
68 | res.reply(result);
69 | }
70 |
71 | /**
72 | * 更新对象
73 | */
74 | update(req, res) {
75 | if (!req.params || !req.params.id) {
76 | return res.replyError('missing id parameter');
77 | }
78 | const rules = {
79 | name: joi.string().min(3),
80 | comment: joi.string().min(2),
81 | permissions: joi.array()
82 | };
83 | const { error, value } = joi.validate(req.body, rules);
84 | if (error) {
85 | return res.replyError(error);
86 | }
87 |
88 | let updatePermissions;
89 | const AdminPermission = this.models['AdminPermission'];
90 | const result = Promise.resolve().then(() => {
91 | if (value.permissions) {
92 | return AdminPermission.findAll({ where: { name: value.permissions } }).then((permissions) => {
93 | updatePermissions = permissions;
94 | });
95 | }
96 | }).then(() => {
97 | delete value.permissions;
98 | return this.model.findByPk(req.params.id).then((role) => {
99 | if (role && role.name === 'admin' && value.name) {
100 | // 禁止修改默认的admin权限名称
101 | console.error('Found updates to admin role name');
102 | delete value.name;
103 | console.error('Abandon updates to admin role name');
104 | }
105 | return this.sequelize.transaction((t) => {
106 | return role.update(value, { transaction: t }).then((role) => {
107 | return role.setPermissions(updatePermissions, { transaction: t }).then(() => { });
108 | });
109 | });
110 | });
111 | });
112 | res.reply(result);
113 | }
114 |
115 | // 获取角色权限列表
116 | fetchPermissions(req, res) {
117 | const AdminPermission = this.models['AdminPermission'];
118 | const promise = this.model.findByPk(req.params.id).then(role => {
119 | return AdminPermission.findAll({
120 | where: {
121 | '$roles.id$': role.get('id')
122 | },
123 | include: [{
124 | model: this.model,
125 | as: 'roles'
126 | }]
127 | });
128 | });
129 | res.reply(promise.then(results => {
130 | return this.filterModels(results, ['id', 'name']);
131 | }));
132 | }
133 |
134 | // 更新角色权限
135 | updatePermissions(req, res) {
136 | const rules = {
137 | permissions: joi.array()
138 | };
139 | const { error, value } = joi.validate(req.body, rules);
140 | if (error) {
141 | return res.replyError(error);
142 | }
143 |
144 | const AdminPermission = this.models['AdminPermission'];
145 | res.reply(this.model.findByPk(req.params.id).then(role => {
146 | return AdminPermission.findAll({
147 | where: {
148 | name: value.permissions
149 | }
150 | }).then(permissions => {
151 | return role.setPermissions(permissions);
152 | });
153 | }));
154 | }
155 | }
156 |
157 | module.exports = new RoleController();
158 |
--------------------------------------------------------------------------------
/controller/admin/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('lodash');
4 | const joi = require('joi');
5 | const credential = require('credential');
6 | const pw = credential();
7 |
8 | const RestController = require('../rest');
9 |
10 | class UserController extends RestController {
11 | constructor() {
12 | super('AdminUser');
13 |
14 | this.defaultUserRole = 'member';
15 |
16 | const AdminRole = this.models['AdminRole'];
17 | AdminRole.findOne({ where: { name: this.defaultUserRole } }).then((result) => {
18 | if (!result) {
19 | throw new Error('Failed to load the default user role! Should run sequelize seeder first!');
20 | }
21 | });
22 | }
23 |
24 | /**
25 | * 分页返回所有对象
26 | */
27 | index(req, res) {
28 | const params = req.query || {};
29 | const data = {
30 | offset: +params.offset || 0,
31 | limit: +params.limit || 10
32 | };
33 | if (params.where && _.isObject(params.where)) {
34 | data.where = params.where;
35 | }
36 | const AdminRole = this.models['AdminRole'];
37 | data.include = [{ model: AdminRole, as: 'roles' }];
38 | data.distinct = true;
39 | res.reply(this.model.findAndCountAll(data));
40 | }
41 |
42 | /**
43 | * 创建对象
44 | */
45 | create(req, res) {
46 | const rules = {
47 | username: joi.string().min(3).required(),
48 | password: joi.string().min(6).required(),
49 | disabled: joi.boolean().default(false),
50 | roles: joi.array().default([]),
51 | };
52 | const { error, value } = joi.validate(req.body, rules);
53 | if (error) {
54 | return res.replyError(error);
55 | }
56 |
57 | const AdminRole = this.models['AdminRole'];
58 | const creationRoles = value.roles;
59 | creationRoles.push(this.defaultUserRole);
60 |
61 | const result = pw.hash(value.password).then((hash) => {
62 | value.password = hash;
63 | delete value.roles;
64 | return AdminRole.findAll({ where: { name: creationRoles } });
65 | }).then((roles) => {
66 | return this.sequelize.transaction((t) => {
67 | return this.model.create(value, { transaction: t }).then((user) => {
68 | return user.setRoles(roles, { transaction: t }).then(() => { });
69 | });
70 | });
71 | });
72 | res.reply(result);
73 | }
74 |
75 | /**
76 | * 更新对象
77 | */
78 | update(req, res) {
79 | if (!req.params || !req.params.id) {
80 | return res.replyError('missing id parameter');
81 | }
82 | const rules = {
83 | username: joi.string().min(3),
84 | password: joi.string().min(6),
85 | disabled: joi.boolean(),
86 | roles: joi.array(),
87 | };
88 | const { error, value } = joi.validate(req.body, rules);
89 | if (error) {
90 | return res.replyError(error);
91 | }
92 |
93 | let updateRoles;
94 | const AdminRole = this.models['AdminRole'];
95 | const result = Promise.resolve().then(() => {
96 | if (value.password) {
97 | return pw.hash(value.password).then((hash) => {
98 | value.password = hash;
99 | });
100 | }
101 | }).then(() => {
102 | if (value.roles) {
103 | return AdminRole.findAll({ where: { name: value.roles } }).then((roles) => {
104 | updateRoles = roles;
105 | });
106 | }
107 | }).then(() => {
108 | delete value.roles;
109 | return this.model.findByPk(req.params.id).then((user) => {
110 | if (user && user.name === 'admin' && value.username) {
111 | // 禁止修改默认的admin用户名称
112 | console.error('Found updates to admin username');
113 | delete value.username;
114 | console.error('Abandon updates to admin username');
115 | }
116 | return this.sequelize.transaction((t) => {
117 | return user.update(value, { transaction: t }).then((user) => {
118 | return user.setRoles(updateRoles, { transaction: t }).then(() => { });
119 | });
120 | });
121 | });
122 | });
123 | res.reply(result);
124 | }
125 |
126 | // 获取用户角色列表
127 | fetchRoles(req, res) {
128 | const AdminRole = this.models['AdminRole'];
129 | const promise = this.model.findByPk(req.params.id).then(user => {
130 | return AdminRole.findAll({
131 | where: {
132 | '$users.id$': user.get('id')
133 | },
134 | include: [{
135 | model: this.model,
136 | as: 'users'
137 | }]
138 | });
139 | });
140 |
141 | res.reply(promise.then(results => {
142 | return this.filterModels(results, ['id', 'name']);
143 | }));
144 | }
145 |
146 | // 更新用户角色
147 | updateRoles(req, res) {
148 | const rules = {
149 | roles: joi.array()
150 | };
151 | const { error, value } = joi.validate(req.body, rules);
152 | if (error) {
153 | return res.replyError(error);
154 | }
155 |
156 | const AdminRole = this.models['AdminRole'];
157 | res.reply(this.model.findByPk(req.params.id).then(user => {
158 | return AdminRole.findAll({
159 | where: {
160 | name: value.roles
161 | }
162 | }).then(roles => {
163 | return user.setAdminRole(roles);
164 | });
165 | }));
166 | }
167 |
168 | /**
169 | * 删除单个对象
170 | */
171 | destroy(req, res) {
172 | if (!req.params || !req.params.id) {
173 | return res.replyError('missing id parameter');
174 | }
175 |
176 | this.model.findByPk(req.params.id).then((obj) => {
177 | if (obj) {
178 | if (obj.name === 'admin') {
179 | res.replyError('Admin can\'t be deleted!');
180 | } else {
181 | res.reply(obj.destroy());
182 | }
183 | } else {
184 | res.replyError(this.modelName + ' not found');
185 | }
186 | });
187 | }
188 | }
189 |
190 | module.exports = new UserController();
191 |
--------------------------------------------------------------------------------
/web/src/components/admin/Permission.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 权限列表
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
21 |
22 |
23 |
24 |
32 |
33 |
34 |
35 |
36 |
37 | 角色删除确认
38 |
39 |
40 |
角色:{{ destroyData ? destroyData.name : '-'}}
41 |
将被删除且无法恢复,是否继续?
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
219 |
220 |
221 |
228 |
--------------------------------------------------------------------------------
/web/src/components/admin/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 用户列表
7 |
8 |
9 |
10 |
11 |
12 |
14 |
19 |
20 |
21 |
22 |
38 |
39 |
40 |
41 |
42 |
43 | 用户删除确认
44 |
45 |
46 |
用户:{{ destroyData ? destroyData.username : '-'}}
47 |
将被删除且无法恢复,是否继续?
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
276 |
277 |
278 |
285 |
--------------------------------------------------------------------------------
/web/src/components/admin/Role.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 角色列表
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
21 |
22 |
23 |
24 |
32 |
33 |
34 |
35 |
36 |
43 |
44 |
45 |
46 |
47 |
48 | 角色删除确认
49 |
50 |
51 |
角色:{{ destroyData ? destroyData.name : '-'}}
52 |
将被删除且无法恢复,是否继续?
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
283 |
284 |
285 |
292 |
--------------------------------------------------------------------------------