├── public ├── favicon.ico ├── .gitignore ├── robots.txt ├── .htaccess ├── web.config └── index.php ├── frontend ├── src │ ├── assets │ │ ├── .keep │ │ └── img │ │ │ └── info.png │ ├── theme │ │ └── default.less │ ├── utils │ │ ├── Wx.js │ │ ├── Storage.js │ │ ├── Helper.js │ │ ├── Url.js │ │ └── Http.js │ ├── mobile │ │ ├── Error.vue │ │ ├── Login.vue │ │ └── Qrlogin.vue │ ├── CDesktop.vue │ ├── mobile.js │ ├── desktop.js │ ├── components │ │ ├── SpinWrapper.vue │ │ └── DataTable.vue │ ├── router │ │ ├── mobile.js │ │ └── desktop.js │ ├── desktop │ │ ├── Error.vue │ │ └── Departments.vue │ ├── CMobile.vue │ └── auth │ │ └── Auth.js ├── static │ └── .gitkeep ├── config │ ├── .gitignore │ ├── prod.env.js │ ├── test.env.js │ ├── dev.env.js │ └── index.js.example ├── .eslintignore ├── .gitignore ├── test │ └── unit │ │ ├── .eslintrc │ │ ├── specs │ │ └── Hello.spec.js │ │ ├── index.js │ │ └── karma.conf.js ├── .editorconfig ├── .postcssrc.js ├── .babelrc ├── build │ ├── dev-client.js │ ├── vue-loader.conf.js │ ├── webpack.test.conf.js │ ├── build.js │ ├── check-versions.js │ ├── webpack.dev.conf.js │ ├── webpack.base.conf.js │ ├── utils.js │ └── dev-server.js ├── index.html ├── mobile.html ├── README.md ├── .eslintrc.js └── package.json ├── database ├── .gitignore ├── seeds │ ├── DatabaseSeeder.php │ └── UsersTableSeeder.php ├── factories │ └── ModelFactory.php └── migrations │ ├── 2017_04_27_101040_create_request_logs_collection.php │ ├── 2017_03_14_204633_create_failed_jobs_table.php │ ├── 2017_03_13_152355_create_departments_table.php │ ├── 2017_04_07_145100_add_rbac_auth.php │ ├── 2017_03_12_193242_add_user_auth.php │ ├── 2017_03_13_164637_add_departments_auth.php │ ├── 2017_07_09_172152_create_department_user_table.php │ ├── 2017_03_14_203920_add_qrlogin_auth.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2017_03_10_182637_seed_users.php │ └── 2017_03_10_155503_entrust_setup_tables.php ├── bootstrap ├── cache │ └── .gitignore ├── autoload.php └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ ├── qywx │ │ └── .gitignore │ ├── upload │ │ └── .gitignore │ └── .gitignore └── framework │ ├── cache │ └── .gitignore │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── resources ├── views │ ├── vendor │ │ ├── mail │ │ │ ├── markdown │ │ │ │ ├── panel.blade.php │ │ │ │ ├── table.blade.php │ │ │ │ ├── footer.blade.php │ │ │ │ ├── promotion.blade.php │ │ │ │ ├── subcopy.blade.php │ │ │ │ ├── button.blade.php │ │ │ │ ├── header.blade.php │ │ │ │ ├── promotion │ │ │ │ │ └── button.blade.php │ │ │ │ ├── layout.blade.php │ │ │ │ └── message.blade.php │ │ │ └── html │ │ │ │ ├── table.blade.php │ │ │ │ ├── header.blade.php │ │ │ │ ├── subcopy.blade.php │ │ │ │ ├── promotion.blade.php │ │ │ │ ├── footer.blade.php │ │ │ │ ├── panel.blade.php │ │ │ │ ├── promotion │ │ │ │ └── button.blade.php │ │ │ │ ├── message.blade.php │ │ │ │ ├── button.blade.php │ │ │ │ └── layout.blade.php │ │ ├── pagination │ │ │ ├── simple-default.blade.php │ │ │ ├── simple-bootstrap-4.blade.php │ │ │ ├── default.blade.php │ │ │ └── bootstrap-4.blade.php │ │ └── notifications │ │ │ └── email.blade.php │ └── welcome.blade.php ├── assets │ ├── sass │ │ ├── app.scss │ │ └── _variables.scss │ └── js │ │ ├── app.js │ │ ├── components │ │ └── Example.vue │ │ └── bootstrap.js └── lang │ └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── .gitattributes ├── screenshots ├── user.png ├── login_1.png └── login_2.png ├── app ├── Role.php ├── Permission.php ├── RequestLog.php ├── Department.php ├── Http │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── TrimStrings.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── CanPath.php │ │ ├── RecordRequest.php │ │ └── PreventMongodbJnjection.php │ ├── Controllers │ │ ├── Controller.php │ │ ├── Auth │ │ │ └── LoginController.php │ │ └── Api │ │ │ └── V1 │ │ │ ├── DepartmentController.php │ │ │ ├── WechatController.php │ │ │ ├── UserController.php │ │ │ └── FileController.php │ └── Kernel.php ├── File.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ ├── AppServiceProvider.php │ └── RouteServiceProvider.php ├── Exceptions │ ├── NormalException.php │ └── Handler.php ├── Repositories │ ├── DepartmentRepository.php │ ├── AbstractRepository.php │ └── UserRepository.php ├── Jobs │ └── SyncFromQywx.php ├── Console │ ├── Commands │ │ └── Rbac │ │ │ ├── RemovePerm.php │ │ │ ├── RemoveRole.php │ │ │ ├── ResetPassword.php │ │ │ ├── AttachRole.php │ │ │ ├── DetachRole.php │ │ │ ├── AttachPerm.php │ │ │ ├── DetachPerm.php │ │ │ ├── AddRole.php │ │ │ ├── AddPerm.php │ │ │ ├── Perms.php │ │ │ ├── Roles.php │ │ │ └── AddUser.php │ └── Kernel.php ├── Biz │ └── UserBiz.php └── User.php ├── .editorconfig ├── artisan.bat ├── deploy.sh ├── tests ├── UnitTestCase.php ├── Unit │ └── ExampleTest.php ├── CreatesApplication.php ├── Feature │ ├── DepartmentTest.php │ ├── RbacTest.php │ ├── UserTest.php │ └── AuthTest.php └── TestCase.php ├── routes ├── web.php ├── channels.php ├── console.php └── api.php ├── config ├── qywx.php ├── view.php ├── services.php ├── broadcasting.php ├── filesystems.php ├── queue.php ├── cache.php ├── entrust.php └── auth.php ├── .gitignore ├── webpack.mix.js ├── server.php ├── LICENSE ├── .env.example ├── package.json ├── phpunit.xml.example ├── artisan ├── composer.json ├── readme.md └── install.sh /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /frontend/config/.gitignore: -------------------------------------------------------------------------------- 1 | index.js 2 | -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- 1 | static 2 | *.html 3 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/qywx/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/upload/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/panel.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/table.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-vendored 2 | *.scss linguist-vendored 3 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/promotion.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/button.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }}: {{ $url }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/header.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !upload/ 3 | !public/ 4 | !qywx/ 5 | !.gitignore 6 | -------------------------------------------------------------------------------- /frontend/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/promotion/button.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /screenshots/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purocean/laravel-template/HEAD/screenshots/user.png -------------------------------------------------------------------------------- /screenshots/login_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purocean/laravel-template/HEAD/screenshots/login_1.png -------------------------------------------------------------------------------- /screenshots/login_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purocean/laravel-template/HEAD/screenshots/login_2.png -------------------------------------------------------------------------------- /frontend/src/theme/default.less: -------------------------------------------------------------------------------- 1 | @import '~iview/src/styles/index.less'; 2 | 3 | // @primary-color: #3399ff; 4 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | test/unit/coverage 7 | -------------------------------------------------------------------------------- /frontend/src/assets/img/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purocean/laravel-template/HEAD/frontend/src/assets/img/info.png -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/table.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 |
4 | -------------------------------------------------------------------------------- /app/Role.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ $slot }} 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /app/RequestLog.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ Illuminate\Mail\Markdown::parse($slot) }} 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/Department.php: -------------------------------------------------------------------------------- 1 | belongsToMany(User::class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /resources/assets/sass/app.scss: -------------------------------------------------------------------------------- 1 | 2 | // Fonts 3 | @import url(https://fonts.googleapis.com/css?family=Raleway:300,400,600); 4 | 5 | // Variables 6 | @import "variables"; 7 | 8 | // Bootstrap 9 | @import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap"; 10 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/promotion.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 |
4 | {{ Illuminate\Mail\Markdown::parse($slot) }} 5 |
8 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd `dirname $0` 4 | php artisan down 5 | git pull && composer install --optimize-autoloader --no-dev \ 6 | && php artisan config:cache --no-interaction \ 7 | && php artisan migrate --no-interaction --force \ 8 | && php artisan api:cache --no-interaction 2>&1 9 | php artisan up 10 | -------------------------------------------------------------------------------- /frontend/src/utils/Wx.js: -------------------------------------------------------------------------------- 1 | import Url from './Url'; 2 | 3 | export default { 4 | previewImage(current, urls) { 5 | current = Url.fixFull(current); 6 | urls = urls.map(src => Url.fixFull(src)); 7 | 8 | window.wx.previewImage({current, urls}); 9 | }, 10 | 11 | hideOptionMenu() { 12 | window.wx.hideOptionMenu(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Laravel template 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |

错误 {{ $route.query.code }}

4 |

{{ $route.query.message }}

5 |
6 | 7 | 8 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /frontend/test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from '@/components/Hello' 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(Hello) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .to.equal('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | { 2 | if (!data || data.length === 0) { 3 | return '' 4 | } 5 | 6 | return data.map(item => { 7 | let padding = new Array(level + 1).join(' ') + 8 | (item.children && item.children.length > 0 ? '+ ' : '- ') 9 | return `${padding}${item.title}\n` + textTree(item.children, level + 1) 10 | }).join('') 11 | } 12 | 13 | export default { textTree } 14 | -------------------------------------------------------------------------------- /tests/UnitTestCase.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 18 | 19 | Model::reguard(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/File.php: -------------------------------------------------------------------------------- 1 | path; 14 | if ($thumb) { 15 | $path = dirname($this->path) . '/thumb_' . basename($this->path); 16 | } 17 | 18 | return storage_path('app/' . $path); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/CDesktop.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 26 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 |
7 | {{ Illuminate\Mail\Markdown::parse($slot) }} 8 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/mobile.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 Vue from 'vue' 4 | import Router from 'vue-router' 5 | import Mobile from './CMobile' 6 | import router from './router/mobile' 7 | 8 | Vue.use(Router) 9 | Vue.config.productionTip = false 10 | 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | el: '#app', 14 | router, 15 | template: '', 16 | components: { Mobile } 17 | }) 18 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/promotion/button.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 |
4 | 5 | 6 | 9 | 10 |
7 | {{ $slot }} 8 |
11 |
14 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'file_prefix' => 'contacts_', 6 | 'rootid' => 1, 7 | 'corpid' => env('QYWX_CORPID'), 8 | 'secret' => env('QYWX_CONTACTS_SECRET'), 9 | 'dataPath' => storage_path('app/qywx'), 10 | ], 11 | 'app' => [ 12 | 'file_prefix' => 'app_', 13 | 'corpid' => env('QYWX_CORPID'), 14 | 'appid' => env('QYWX_APPID'), 15 | 'secret' => env('QYWX_SECRET'), 16 | 'dataPath' => storage_path('app/qywx'), 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /database/seeds/UsersTableSeeder.php: -------------------------------------------------------------------------------- 1 | 'useradmin', 'password' => 'password', 'rolename' => 'admin']); 16 | Artisan::call('rbac:adduser', ['username' => 'usertest', 'password' => 'password', 'rolename' => 'user']); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/src/utils/Url.js: -------------------------------------------------------------------------------- 1 | const serialize = function (data) { 2 | return Object.keys(data).map(keyName => { 3 | return encodeURIComponent(keyName) + '=' + encodeURIComponent(data[keyName]) 4 | }).join('&') 5 | } 6 | 7 | const fixFull = url => { 8 | if (/^\//.test(url)) { 9 | url = location.protocol + '//' + location.host + url 10 | } else if (!/^http/.test(url)) { 11 | url = location.protocol + '//' + location.host + location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/' + url 12 | } 13 | 14 | return url 15 | } 16 | 17 | export default { serialize, fixFull } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/storage 3 | /public/hot 4 | /storage/*.key 5 | /vendor 6 | /.idea 7 | Homestead.json 8 | Homestead.yaml 9 | .env 10 | phpunit.xml 11 | 12 | ###SublimeText### 13 | 14 | # cache files for sublime text 15 | *.tmlanguage.cache 16 | *.tmPreferences.cache 17 | *.stTheme.cache 18 | 19 | # workspace files are user-specific 20 | *.sublime-workspace 21 | 22 | # project files should be checked into the repository, unless a significant 23 | # proportion of contributors will probably not be using SublimeText 24 | *.sublime-project 25 | 26 | # sftp configuration file 27 | sftp-config.json 28 | -------------------------------------------------------------------------------- /frontend/src/desktop.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 Vue from 'vue' 4 | import iView from 'iview' 5 | import Router from 'vue-router' 6 | import Desktop from './CDesktop' 7 | import router from './router/desktop' 8 | import '@/theme/default.less' 9 | 10 | Vue.config.productionTip = false 11 | 12 | Vue.use(Router) 13 | Vue.use(iView) 14 | 15 | /* eslint-disable no-new */ 16 | new Vue({ 17 | el: '#app', 18 | router, 19 | template: '', 20 | components: { Desktop } 21 | }) 22 | -------------------------------------------------------------------------------- /frontend/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Laravel template 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const { mix } = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the Sass 10 | | file for the application as well as bundling up all the JS files. 11 | | 12 | */ 13 | 14 | mix.js('resources/assets/js/app.js', 'public/js') 15 | .sass('resources/assets/sass/app.scss', 'public/css'); 16 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 18 | })->describe('Display an inspiring quote'); 19 | -------------------------------------------------------------------------------- /bootstrap/autoload.php: -------------------------------------------------------------------------------- 1 | A Vue.js project 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 | # run unit tests 21 | npm run unit 22 | 23 | # run all tests 24 | npm test 25 | ``` 26 | 27 | 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). 28 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Redirect Trailing Slashes If Not A Folder... 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | RewriteRule ^(.*)/$ /$1 [L,R=301] 11 | 12 | # Handle Front Controller... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteRule ^ index.php [L] 16 | 17 | # Handle Authorization Header 18 | RewriteCond %{HTTP:Authorization} . 19 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 20 | 21 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 21 | return redirect('/home'); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /resources/assets/js/app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * First we will load all of this project's JavaScript dependencies which 4 | * includes Vue and other libraries. It is a great starting point when 5 | * building robust, powerful web applications using Vue and Laravel. 6 | */ 7 | 8 | require('./bootstrap'); 9 | 10 | /** 11 | * Next, we will create a fresh Vue application instance and attach it to 12 | * the page. Then, you may begin adding components to this application 13 | * or customize the JavaScript scaffolding to fit your unique needs. 14 | */ 15 | 16 | Vue.component('example', require('./components/Example.vue')); 17 | 18 | const app = new Vue({ 19 | el: '#app' 20 | }); 21 | -------------------------------------------------------------------------------- /frontend/src/components/SpinWrapper.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.config.productionTip = false 3 | 4 | // Polyfill fn.bind() for PhantomJS 5 | /* eslint-disable no-extend-native */ 6 | Function.prototype.bind = require('function-bind') 7 | 8 | // require all test files (files that ends with .spec.js) 9 | const testsContext = require.context('./specs', true, /\.spec$/) 10 | testsContext.keys().forEach(testsContext) 11 | 12 | // require all src files except main.js for coverage. 13 | // you can also change this to match only the subset of files that 14 | // you want coverage for. 15 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 16 | srcContext.keys().forEach(srcContext) 17 | -------------------------------------------------------------------------------- /resources/assets/js/components/Example.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /frontend/build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | plugins: [ 15 | new webpack.DefinePlugin({ 16 | 'process.env': require('../config/test.env') 17 | }) 18 | ] 19 | }) 20 | 21 | // no need for app entry during tests 22 | delete webpackConfig.entry 23 | 24 | module.exports = webpackConfig 25 | -------------------------------------------------------------------------------- /frontend/src/router/mobile.js: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | import Auth from '@/auth/Auth' 3 | import Err from '@/mobile/Error' 4 | import Login from '@/mobile/Login.vue' 5 | import Qrlogin from '@/mobile/Qrlogin.vue' 6 | 7 | const allowList = ['/', '/error/*', '/login'] 8 | 9 | const routes = [ 10 | {path: '/', name: '/', redirect: '/'}, 11 | {path: '/login', name: 'login', component: Login}, 12 | {path: '/qrlogin', name: 'qrlogin', component: Qrlogin}, 13 | {path: '/error', name: 'error', component: Err}, 14 | ] 15 | 16 | const router = new Router({ routes }) 17 | 18 | router.beforeEach((to, from, next) => { 19 | Auth.requireAuth(allowList, to, from, next) 20 | }) 21 | 22 | export default router 23 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | 28 | // 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Middleware/CanPath.php: -------------------------------------------------------------------------------- 1 | is('api' . $permission) 21 | || $request->is('api' . rtrim($permission, '/*')); 22 | 23 | if ($matched and Entrust::can($permission)) { 24 | return $next($request); 25 | } 26 | 27 | abort(403, '无权访问资源'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/views/vendor/pagination/simple-default.blade.php: -------------------------------------------------------------------------------- 1 | @if ($paginator->hasPages()) 2 | 17 | @endif 18 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'App\Listeners\EventListener', 18 | ], 19 | ]; 20 | 21 | /** 22 | * Register any events for your application. 23 | * 24 | * @return void 25 | */ 26 | public function boot() 27 | { 28 | parent::boot(); 29 | 30 | // 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/markdown/message.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::layout') 2 | {{-- Header --}} 3 | @slot('header') 4 | @component('mail::header', ['url' => config('app.url')]) 5 | {{ config('app.name') }} 6 | @endcomponent 7 | @endslot 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @if (isset($subcopy)) 14 | @slot('subcopy') 15 | @component('mail::subcopy') 16 | {{ $subcopy }} 17 | @endcomponent 18 | @endslot 19 | @endif 20 | 21 | {{-- Footer --}} 22 | @slot('footer') 23 | @component('mail::footer') 24 | © {{ date('Y') }} {{ config('app.name') }}. All rights reserved. 25 | @endcomponent 26 | @endslot 27 | @endcomponent 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/message.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::layout') 2 | {{-- Header --}} 3 | @slot('header') 4 | @component('mail::header', ['url' => config('app.url')]) 5 | {{ config('app.name') }} 6 | @endcomponent 7 | @endslot 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @if (isset($subcopy)) 14 | @slot('subcopy') 15 | @component('mail::subcopy') 16 | {{ $subcopy }} 17 | @endcomponent 18 | @endslot 19 | @endif 20 | 21 | {{-- Footer --}} 22 | @slot('footer') 23 | @component('mail::footer') 24 | © {{ date('Y') }} {{ config('app.name') }}. All rights reserved. 25 | @endcomponent 26 | @endslot 27 | @endcomponent 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | response->array([ 18 | 'status' => $status, 19 | 'message' => $message, 20 | 'data' => $data, 21 | 'errors' => $errors, 22 | 'code' => $code, 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/.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 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | "comma-dangle": 0, 21 | // allow paren-less arrow functions 22 | 'arrow-parens': 0, 23 | // allow async-await 24 | 'generator-star-spacing': 0, 25 | // allow debugger during development 26 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 27 | "yoda": [2, "never", { "exceptRange": true }] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/desktop/Error.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | 26 | 41 | -------------------------------------------------------------------------------- /frontend/src/router/desktop.js: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | import Auth from '@/auth/Auth' 3 | import Err from '@/desktop/Error' 4 | import Login from '@/desktop/Login.vue' 5 | import Users from '@/desktop/Users.vue' 6 | import Departments from '@/desktop/Departments.vue' 7 | 8 | const allowList = ['/', '/error', '/login'] 9 | 10 | const routes = [ 11 | {path: '/', name: '/', redirect: '/users'}, 12 | {path: '/users/:rolename?', name: 'users', component: Users}, 13 | {path: '/departments', name: 'departments', component: Departments}, 14 | {path: '/login', name: 'login', component: Login}, 15 | {path: '/error', name: 'error', component: Err}, 16 | ] 17 | 18 | const router = new Router({ routes }) 19 | 20 | router.beforeEach((to, from, next) => { 21 | Auth.requireAuth(allowList, to, from, next) 22 | }) 23 | 24 | export default router 25 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 |
4 | 5 | 6 | 15 | 16 |
7 | 8 | 9 | 12 | 13 |
10 | {{ $slot }} 11 |
14 |
17 |
20 | -------------------------------------------------------------------------------- /app/Exceptions/NormalException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 16 | $this->status = $status; 17 | $this->data = $data; 18 | 19 | parent::__construct($message, $code); 20 | } 21 | 22 | public function toArray() 23 | { 24 | return [ 25 | 'status' => $this->status, 26 | 'message' => $this->getMessage(), 27 | 'data' => $this->data, 28 | 'errors' => $this->errors, 29 | 'code' => $this->getCode(), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | define(App\User::class, function (Faker\Generator $faker) { 16 | static $password; 17 | 18 | return [ 19 | 'name' => $faker->name, 20 | 'email' => $faker->unique()->safeEmail, 21 | 'password' => $password ?: $password = bcrypt('secret'), 22 | 'remember_token' => str_random(10), 23 | ]; 24 | }); 25 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Passwords must be at least six characters and match the confirmation.', 17 | 'reset' => 'Your password has been reset!', 18 | 'sent' => 'We have e-mailed your password reset link!', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that e-mail address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/views/vendor/pagination/simple-bootstrap-4.blade.php: -------------------------------------------------------------------------------- 1 | @if ($paginator->hasPages()) 2 | 17 | @endif 18 | -------------------------------------------------------------------------------- /app/Repositories/DepartmentRepository.php: -------------------------------------------------------------------------------- 1 | getDepartments('qywx.contacts.rootid')) { 19 | Department::where('id', '>', '0')->delete(); 20 | 21 | return Department::insert(array_map(function ($row) { 22 | return array_merge($row, [ 23 | 'created_at' => date("Y-m-d H:i:s"), 24 | 'updated_at' => date("Y-m-d H:i:s") 25 | ]); 26 | }, $departments)); 27 | } 28 | 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2017_04_27_101040_create_request_logs_collection.php: -------------------------------------------------------------------------------- 1 | createCollection('request_logs', [ 17 | 'capped' => true, 18 | 'size' => 52428800, 19 | 'max' => 50000 20 | ]); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | if (Schema::connection('mongodb')->hasCollection('request_logs')) { 31 | Schema::connection('mongodb')->drop('request_logs'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make('api.exception')->register(function (AuthorizationException $e) { 29 | abort(403, '无权操作'); 30 | }); 31 | 32 | $this->app->make('api.exception')->register(function (NormalException $e) { 33 | return response()->json($e->toArray(), 200); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2017_03_14_204633_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->text('connection'); 19 | $table->text('queue'); 20 | $table->longText('payload'); 21 | $table->longText('exception'); 22 | $table->timestamp('failed_at')->useCurrent(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('failed_jobs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/Jobs/SyncFromQywx.php: -------------------------------------------------------------------------------- 1 | userRepo = $userRepo; 26 | $this->departmentRepo = $departmentRepo; 27 | } 28 | 29 | /** 30 | * Execute the job. 31 | * 32 | * @return void 33 | */ 34 | public function handle() 35 | { 36 | $this->departmentRepo->sync(); 37 | $this->userRepo->sync(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2017_03_13_152355_create_departments_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name')->comment('名称'); 19 | $table->integer('parentid')->default(0)->comment('父级ID'); 20 | $table->integer('order')->default(0)->comment('排序'); 21 | $table->smallInteger('status')->default(0)->comment('状态'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('departments'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/assets/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | // Body 3 | $body-bg: #f5f8fa; 4 | 5 | // Borders 6 | $laravel-border-color: darken($body-bg, 10%); 7 | $list-group-border: $laravel-border-color; 8 | $navbar-default-border: $laravel-border-color; 9 | $panel-default-border: $laravel-border-color; 10 | $panel-inner-border: $laravel-border-color; 11 | 12 | // Brands 13 | $brand-primary: #3097D1; 14 | $brand-info: #8eb4cb; 15 | $brand-success: #2ab27b; 16 | $brand-warning: #cbb956; 17 | $brand-danger: #bf5329; 18 | 19 | // Typography 20 | $icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"; 21 | $font-family-sans-serif: "Raleway", sans-serif; 22 | $font-size-base: 14px; 23 | $line-height-base: 1.6; 24 | $text-color: #636b6f; 25 | 26 | // Navbar 27 | $navbar-default-bg: #fff; 28 | 29 | // Buttons 30 | $btn-default-color: $text-color; 31 | 32 | // Inputs 33 | $input-border: lighten($text-color, 40%); 34 | $input-border-focus: lighten($brand-primary, 25%); 35 | $input-color-placeholder: lighten($text-color, 30%); 36 | 37 | // Panels 38 | $panel-default-heading-bg: #fff; 39 | -------------------------------------------------------------------------------- /database/migrations/2017_04_07_145100_add_rbac_auth.php: -------------------------------------------------------------------------------- 1 | '/rbac/*']); 17 | 18 | Artisan::call('rbac:attachperm', ['rolename' => 'suadmin', 'permname' => '/rbac/*']); 19 | Artisan::call('rbac:attachperm', ['rolename' => 'admin', 'permname' => '/rbac/*']); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Artisan::call('rbac:detachperm', ['rolename' => 'admin', 'permname' => '/rbac/*']); 30 | Artisan::call('rbac:detachperm', ['rolename' => 'suadmin', 'permname' => '/rbac/*']); 31 | 32 | Artisan::call('rbac:removeperm', ['name' => '/rbac/*']); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2017_03_12_193242_add_user_auth.php: -------------------------------------------------------------------------------- 1 | '/users/*']); 18 | 19 | Artisan::call('rbac:attachperm', ['rolename' => 'suadmin', 'permname' => '/users/*']); 20 | Artisan::call('rbac:attachperm', ['rolename' => 'admin', 'permname' => '/users/*']); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Artisan::call('rbac:detachperm', ['rolename' => 'admin', 'permname' => '/users/*']); 31 | Artisan::call('rbac:detachperm', ['rolename' => 'suadmin', 'permname' => '/users/*']); 32 | 33 | Artisan::call('rbac:removeperm', ['name' => '/users/*']); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 洋子 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 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => realpath(storage_path('framework/views')), 32 | 33 | ]; 34 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/RemovePerm.php: -------------------------------------------------------------------------------- 1 | argument('name'); 42 | 43 | $permission = Permission::where(['name' => $name])->firstOrFail(); 44 | 45 | $permission->forceDelete(); 46 | 47 | $this->info("removed permission {$name}"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/LoginController.php: -------------------------------------------------------------------------------- 1 | middleware('guest', ['except' => 'logout']); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | ], 21 | 22 | 'ses' => [ 23 | 'key' => env('SES_KEY'), 24 | 'secret' => env('SES_SECRET'), 25 | 'region' => 'us-east-1', 26 | ], 27 | 28 | 'sparkpost' => [ 29 | 'secret' => env('SPARKPOST_SECRET'), 30 | ], 31 | 32 | 'stripe' => [ 33 | 'model' => App\User::class, 34 | 'key' => env('STRIPE_KEY'), 35 | 'secret' => env('STRIPE_SECRET'), 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | APP_KEY= 3 | APP_DEBUG=true 4 | APP_LOG_LEVEL=debug 5 | APP_URL=http://localhost 6 | 7 | TIMEZONE=PRC 8 | LOCALE=zh_cn 9 | 10 | DB_CONNECTION=mysql 11 | DB_HOST=127.0.0.1 12 | DB_PORT=3306 13 | DB_DATABASE=homestead 14 | DB_USERNAME=homestead 15 | DB_PASSWORD=secret 16 | 17 | MONGODB_HOST=localhost 18 | MONGODB_PORT=27017 19 | MONGODB_DATABASE=laravel_template 20 | MONGODB_USERNAME= 21 | MONGODB_PASSWORD= 22 | 23 | BROADCAST_DRIVER=log 24 | CACHE_DRIVER=redis 25 | CACHE_PREFIX=laravel_template 26 | SESSION_DRIVER=file 27 | QUEUE_DRIVER=sync 28 | QUEUE_DEFAULT=default 29 | 30 | REDIS_HOST=127.0.0.1 31 | REDIS_PASSWORD=null 32 | REDIS_PORT=6379 33 | 34 | MAIL_DRIVER=smtp 35 | MAIL_HOST=mailtrap.io 36 | MAIL_PORT=2525 37 | MAIL_USERNAME=null 38 | MAIL_PASSWORD=null 39 | MAIL_ENCRYPTION=null 40 | 41 | PUSHER_APP_ID= 42 | PUSHER_APP_KEY= 43 | PUSHER_APP_SECRET= 44 | 45 | API_SUBTYPE=laravel_template 46 | API_PREFIX=api 47 | API_NAME="Laravel template" 48 | API_STRICT=false 49 | API_DEBUG=true 50 | 51 | JWT_SECRET= 52 | JWT_BLACKLIST_GRACE_PERIOD=3600 53 | 54 | QYWX_ROOTID=1 55 | QYWX_CONTACTS_SECRET= 56 | QYWX_CORPID= 57 | QYWX_SECRET= 58 | QYWX_APPID=18 59 | -------------------------------------------------------------------------------- /database/migrations/2017_03_13_164637_add_departments_auth.php: -------------------------------------------------------------------------------- 1 | '/departments/*']); 18 | 19 | Artisan::call('rbac:attachperm', ['rolename' => 'suadmin', 'permname' => '/departments/*']); 20 | Artisan::call('rbac:attachperm', ['rolename' => 'admin', 'permname' => '/departments/*']); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Artisan::call('rbac:detachperm', ['rolename' => 'admin', 'permname' => '/departments/*']); 31 | Artisan::call('rbac:detachperm', ['rolename' => 'suadmin', 'permname' => '/departments/*']); 32 | 33 | Artisan::call('rbac:removeperm', ['name' => '/departments/*']); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/RemoveRole.php: -------------------------------------------------------------------------------- 1 | argument('name'); 42 | 43 | $role = Role::where(['name' => $name])->firstOrFail(); 44 | 45 | $role->users()->sync([]); 46 | $role->perms()->sync([]); 47 | $role->forceDelete(); 48 | 49 | $this->info("removed role {$name}"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Middleware/RecordRequest.php: -------------------------------------------------------------------------------- 1 | except as $path) { 30 | if ($request->is($path)) { 31 | $flag = false; 32 | break; 33 | } 34 | } 35 | 36 | if ($flag) { 37 | $model = new RequestLog; 38 | $model->url = $request->fullUrl(); 39 | $model->data = $request->all(); 40 | $model->user = $request->user() ? $request->user()->attributesToArray() : null; 41 | $model->save(); 42 | } 43 | 44 | return $response; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /database/migrations/2017_07_09_172152_create_department_user_table.php: -------------------------------------------------------------------------------- 1 | integer('user_id')->unsigned(); 18 | $table->integer('department_id')->unsigned(); 19 | 20 | $table->foreign('user_id')->references('id')->on('users') 21 | ->onUpdate('cascade')->onDelete('cascade'); 22 | $table->foreign('department_id')->references('id')->on('departments') 23 | ->onUpdate('cascade')->onDelete('cascade'); 24 | 25 | $table->primary(['user_id', 'department_id']); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('department_user'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 5 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch-poll": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --watch-poll --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 8 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 9 | }, 10 | "devDependencies": { 11 | "axios": "^0.15.3", 12 | "bootstrap-sass": "^3.3.7", 13 | "cross-env": "^3.2.3", 14 | "jquery": "^3.1.1", 15 | "laravel-mix": "^0.8.1", 16 | "lodash": "^4.17.4", 17 | "vue": "^2.1.10" 18 | }, 19 | "dependencies": {} 20 | } 21 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/ResetPassword.php: -------------------------------------------------------------------------------- 1 | argument('username'); 42 | $password = $this->argument('password'); 43 | 44 | $user = User::where(['username' => $username])->firstOrFail(); 45 | $user->password = bcrypt($password); 46 | $user->saveOrFail(); 47 | 48 | $this->info("$username new password is $password"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Biz/UserBiz.php: -------------------------------------------------------------------------------- 1 | userRepo = $userRepo; 16 | } 17 | 18 | public function sendWxMsg($username, $title, $message, $url = '') 19 | { 20 | $qywx = new Qywx(config('qywx.app')); 21 | 22 | $articles = [ 23 | $qywx->buildNewsItem($title, $message, $url, ''), 24 | ]; 25 | 26 | if (!is_array($username)) { 27 | $username = [$username]; 28 | } 29 | 30 | $username = array_filter($username, function ($name) { 31 | return !in_array($name, ['suadmin', 'admin', 'demo']); 32 | }); 33 | 34 | $result = $qywx->sendNewsMsg( 35 | $articles, 36 | ['touser' => $username], 37 | config('qywx.app.appid') 38 | ); 39 | 40 | if (($result['invaliduser'] ?? false) or 41 | ($result['invalidparty'] ?? false) or 42 | ($result['invalidtag'] ?? false)) { 43 | throw new NormalException('部分发送失败'); 44 | } 45 | 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/AttachRole.php: -------------------------------------------------------------------------------- 1 | argument('username'); 43 | $rolename = $this->argument('rolename'); 44 | 45 | $user = User::where(['username' => $username])->firstOrFail(); 46 | $role = Role::where(['name' => $rolename])->firstOrFail(); 47 | 48 | $user->attachRole($role); 49 | 50 | $this->info("attach role $rolename for $username"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/DetachRole.php: -------------------------------------------------------------------------------- 1 | argument('username'); 43 | $rolename = $this->argument('rolename'); 44 | 45 | $user = User::where(['username' => $username])->firstOrFail(); 46 | $role = Role::where(['name' => $rolename])->firstOrFail(); 47 | 48 | $user->detachRole($role); 49 | 50 | $this->info("detach role $rolename from $username"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/AttachPerm.php: -------------------------------------------------------------------------------- 1 | argument('rolename'); 43 | $permname = $this->argument('permname'); 44 | 45 | $role = Role::where(['name' => $rolename])->firstOrFail(); 46 | $perm = Permission::where(['name' => $permname])->firstOrFail(); 47 | 48 | $role->attachPermission($perm); 49 | 50 | $this->info("attach permission $permname for $rolename"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/mobile/Login.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 55 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/DetachPerm.php: -------------------------------------------------------------------------------- 1 | argument('rolename'); 43 | $permname = $this->argument('permname'); 44 | 45 | $role = Role::where(['name' => $rolename])->firstOrFail(); 46 | $perm = Permission::where(['name' => $permname])->firstOrFail(); 47 | 48 | $role->detachPermission($perm); 49 | 50 | $this->info("detach permission $permname from $rolename"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventMongodbJnjection.php: -------------------------------------------------------------------------------- 1 | except as $path) { 28 | if ($request->is($path)) { 29 | return $next($request); 30 | } 31 | } 32 | 33 | $transform = function ($value) use (&$transform) { 34 | if (is_array($value)) { 35 | foreach ($value as $k => $v) { 36 | if (is_string($k)) { 37 | unset($value[$k]); 38 | $value[str_replace(['$', chr(0)], '', $k)] = $transform($v); 39 | } 40 | } 41 | } 42 | 43 | return $value; 44 | }; 45 | 46 | $request->replace($transform($request->all())); 47 | 48 | return $next($request); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /database/migrations/2017_03_14_203920_add_qrlogin_auth.php: -------------------------------------------------------------------------------- 1 | '/qrlogin/*']); 18 | 19 | Artisan::call('rbac:attachperm', ['rolename' => 'suadmin', 'permname' => '/qrlogin/*']); 20 | Artisan::call('rbac:attachperm', ['rolename' => 'admin', 'permname' => '/qrlogin/*']); 21 | Artisan::call('rbac:attachperm', ['rolename' => 'user', 'permname' => '/qrlogin/*']); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Artisan::call('rbac:detachperm', ['rolename' => 'user', 'permname' => '/qrlogin/*']); 32 | Artisan::call('rbac:detachperm', ['rolename' => 'admin', 'permname' => '/qrlogin/*']); 33 | Artisan::call('rbac:detachperm', ['rolename' => 'suadmin', 'permname' => '/qrlogin/*']); 34 | 35 | Artisan::call('rbac:removeperm', ['name' => '/qrlogin/*']); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Feature/DepartmentTest.php: -------------------------------------------------------------------------------- 1 | getJson('/api/departments')->assertStatus(401); 17 | 18 | $this->iam('usertest')->getJson('/api/departments')->assertStatus(403); 19 | 20 | $this->iam('useradmin')->getJson('/api/departments') 21 | ->assertStatus(200) 22 | ->assertJson(['status' => 'ok']); 23 | } 24 | 25 | public function testSync() 26 | { 27 | $this->postJson('/api/departments/sync')->assertStatus(401); 28 | 29 | $this->iam('usertest')->postJson('/api/departments/sync')->assertStatus(403); 30 | 31 | $this->iam('suadmin')->getJson('/api/departments/sync')->assertStatus(405); 32 | 33 | $this->iam('suadmin', function () { 34 | Queue::fake(); 35 | })->postJson('/api/departments/sync') 36 | ->assertStatus(200) 37 | ->assertJson(['status' => 'ok']); 38 | 39 | Queue::assertPushed(SyncFromQywx::class); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('username')->unique()->comment('用户名'); 19 | $table->string('name')->default('')->comment('名字'); 20 | $table->string('email')->unique()->nullable()->comment('邮箱'); 21 | $table->string('mobile')->default('')->comment('电话号码'); 22 | $table->string('avatar')->default('')->comment('头像'); 23 | $table->string('password')->comment('密码'); 24 | $table->json('info')->comment('个人信息'); 25 | $table->smallInteger('status')->default(0)->comment('状态'); 26 | $table->rememberToken(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('users'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/AddRole.php: -------------------------------------------------------------------------------- 1 | argument('name'); 44 | $displayName = $this->argument('displayName') ?: $name; 45 | $description = $this->argument('description') ?: $name; 46 | 47 | $role->name = $name; 48 | $role->display_name = $displayName; 49 | $role->description = $description; 50 | 51 | $role->saveOrFail(); 52 | 53 | $this->info("new role {$name}"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /resources/views/vendor/notifications/email.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | {{-- Greeting --}} 3 | @if (! empty($greeting)) 4 | # {{ $greeting }} 5 | @else 6 | @if ($level == 'error') 7 | # Whoops! 8 | @else 9 | # Hello! 10 | @endif 11 | @endif 12 | 13 | {{-- Intro Lines --}} 14 | @foreach ($introLines as $line) 15 | {{ $line }} 16 | 17 | @endforeach 18 | 19 | {{-- Action Button --}} 20 | @if (isset($actionText)) 21 | 33 | @component('mail::button', ['url' => $actionUrl, 'color' => $color]) 34 | {{ $actionText }} 35 | @endcomponent 36 | @endif 37 | 38 | {{-- Outro Lines --}} 39 | @foreach ($outroLines as $line) 40 | {{ $line }} 41 | 42 | @endforeach 43 | 44 | 45 | @if (! empty($salutation)) 46 | {{ $salutation }} 47 | @else 48 | Regards,
{{ config('app.name') }} 49 | @endif 50 | 51 | 52 | @if (isset($actionText)) 53 | @component('mail::subcopy') 54 | If you’re having trouble clicking the "{{ $actionText }}" button, copy and paste the URL below 55 | into your web browser: [{{ $actionUrl }}]({{ $actionUrl }}) 56 | @endcomponent 57 | @endif 58 | @endcomponent 59 | -------------------------------------------------------------------------------- /frontend/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | 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 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Repositories/AbstractRepository.php: -------------------------------------------------------------------------------- 1 | find($id)) { 13 | throw new NormalException($message ?: "未找到资源 [{$id}]"); 14 | } 15 | 16 | return $model; 17 | } 18 | 19 | public function find($id) 20 | { 21 | $modelName = $this->modelName(); 22 | 23 | return $modelName::find($id); 24 | } 25 | 26 | public function list($orderBy = 'id', $direction = 'desc', $perPage = 15) 27 | { 28 | $modelName = $this->modelName(); 29 | 30 | return $modelName::orderBy($orderBy, $direction)->paginate($perPage); 31 | } 32 | 33 | public function updateOrCreate($id, $data) 34 | { 35 | $modelName = $this->modelName(); 36 | 37 | $model = $id ? $this->findOrFail($id) : new $modelName; 38 | 39 | if ($model->fill($data)->save()) { 40 | return $model; 41 | } 42 | 43 | return null; 44 | } 45 | 46 | public function delete($id) 47 | { 48 | if ($model = $this->find($id)) { 49 | return $model->delete(); 50 | } 51 | 52 | return true; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/AddPerm.php: -------------------------------------------------------------------------------- 1 | argument('name'); 44 | $displayName = $this->argument('displayName') ?: $name; 45 | $description = $this->argument('description') ?: $name; 46 | 47 | $permission->name = $name; 48 | $permission->display_name = $displayName; 49 | $permission->description = $description; 50 | 51 | $permission->saveOrFail(); 52 | 53 | $this->info("new permission {$name}"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/Perms.php: -------------------------------------------------------------------------------- 1 | argument('rolename'); 43 | $headers = ['name', 'display_name', 'description']; 44 | 45 | if (empty($rolename)) { 46 | $this->table($headers, Permission::all($headers)); 47 | } else { 48 | $role = Role::where(['name' => $rolename])->firstOrFail(); 49 | $this->table($headers, $role->perms->map(function ($row) { 50 | return $row->makeHidden(['id', 'pivot', 'created_at', 'updated_at'])->toArray(); 51 | })); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /resources/views/vendor/pagination/default.blade.php: -------------------------------------------------------------------------------- 1 | @if ($paginator->hasPages()) 2 | 36 | @endif 37 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/Roles.php: -------------------------------------------------------------------------------- 1 | argument('username'); 44 | $headers = ['name', 'display_name', 'description']; 45 | 46 | if (empty($username)) { 47 | $this->table($headers, Role::all($headers)); 48 | } else { 49 | $user = User::where(['username' => $username])->firstOrFail(); 50 | $this->table($headers, $user->roles->map(function ($row) { 51 | return $row->makeHidden(['id', 'pivot', 'created_at', 'updated_at'])->toArray(); 52 | })); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/User.php: -------------------------------------------------------------------------------- 1 | getKey(); // Eloquent model method 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function getJWTCustomClaims() 45 | { 46 | return [ 47 | 'user' => [ 48 | 'id' => $this->id, 49 | ] 50 | ]; 51 | } 52 | 53 | public static function findByUsername($username) 54 | { 55 | return self::where(['username' => $username])->firstOrFail(); 56 | } 57 | 58 | public function departments() 59 | { 60 | return $this->belongsToMany(Department::class); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire') 39 | // ->hourly(); 40 | } 41 | 42 | /** 43 | * Register the Closure based commands for the application. 44 | * 45 | * @return void 46 | */ 47 | protected function commands() 48 | { 49 | require base_path('routes/console.php'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /phpunit.xml.example: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | 17 | ./tests/Unit 18 | 19 | 20 | 21 | 22 | ./app 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /resources/assets/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | 2 | window._ = require('lodash'); 3 | 4 | /** 5 | * We'll load jQuery and the Bootstrap jQuery plugin which provides support 6 | * for JavaScript based Bootstrap features such as modals and tabs. This 7 | * code may be modified to fit the specific needs of your application. 8 | */ 9 | 10 | window.$ = window.jQuery = require('jquery'); 11 | 12 | require('bootstrap-sass'); 13 | 14 | /** 15 | * Vue is a modern JavaScript library for building interactive web interfaces 16 | * using reactive data binding and reusable components. Vue's API is clean 17 | * and simple, leaving you to focus on building your next great project. 18 | */ 19 | 20 | window.Vue = require('vue'); 21 | 22 | /** 23 | * We'll load the axios HTTP library which allows us to easily issue requests 24 | * to our Laravel back-end. This library automatically handles sending the 25 | * CSRF token as a header based on the value of the "XSRF" token cookie. 26 | */ 27 | 28 | window.axios = require('axios'); 29 | 30 | window.axios.defaults.headers.common = { 31 | 'X-CSRF-TOKEN': window.Laravel.csrfToken, 32 | 'X-Requested-With': 'XMLHttpRequest' 33 | }; 34 | 35 | /** 36 | * Echo exposes an expressive API for subscribing to channels and listening 37 | * for events that are broadcast by Laravel. Echo and event broadcasting 38 | * allows your team to easily build robust real-time web applications. 39 | */ 40 | 41 | // import Echo from "laravel-echo" 42 | 43 | // window.Echo = new Echo({ 44 | // broadcaster: 'pusher', 45 | // key: 'your-pusher-key' 46 | // }); 47 | -------------------------------------------------------------------------------- /frontend/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 | chunks: ['desktop'], 32 | inject: true 33 | }), 34 | new HtmlWebpackPlugin({ 35 | filename: 'mobile.html', 36 | template: 'mobile.html', 37 | chunks: ['mobile'], 38 | inject: true 39 | }), 40 | new FriendlyErrorsPlugin() 41 | ] 42 | }) 43 | -------------------------------------------------------------------------------- /frontend/src/CMobile.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | 67 | -------------------------------------------------------------------------------- /resources/views/vendor/pagination/bootstrap-4.blade.php: -------------------------------------------------------------------------------- 1 | @if ($paginator->hasPages()) 2 | 36 | @endif 37 | -------------------------------------------------------------------------------- /frontend/src/desktop/Departments.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 65 | 66 | 69 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | // 40 | ], 41 | ], 42 | 43 | 'redis' => [ 44 | 'driver' => 'redis', 45 | 'connection' => 'default', 46 | ], 47 | 48 | 'log' => [ 49 | 'driver' => 'log', 50 | ], 51 | 52 | 'null' => [ 53 | 'driver' => 'null', 54 | ], 55 | 56 | ], 57 | 58 | ]; 59 | -------------------------------------------------------------------------------- /app/Console/Commands/Rbac/AddUser.php: -------------------------------------------------------------------------------- 1 | argument('username'); 45 | $password = $this->argument('password'); 46 | $email = $this->argument('email'); 47 | $name = $this->argument('name') ?: 'new_user_'.str_random(4); 48 | $rolename = $this->argument('rolename'); 49 | 50 | $user->username = $username; 51 | $user->password = bcrypt($password); 52 | $user->name = $name; 53 | $user->email = $email; 54 | $user->info = '{}'; 55 | 56 | $user->saveOrFail(); 57 | 58 | $this->info("new user {$username}, password is {$password}"); 59 | 60 | if ($rolename) { 61 | $role = Role::where(['name' => $rolename])->firstOrFail(); 62 | 63 | $user->attachRole($role); 64 | 65 | $this->info("attach role $rolename for $username"); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /tests/Feature/RbacTest.php: -------------------------------------------------------------------------------- 1 | getJson('/api/rbac/roles')->assertStatus(401); 15 | 16 | $this->iam('usertest')->getJson('/api/rbac/roles')->assertStatus(403); 17 | 18 | $this->iam('useradmin') 19 | ->getJson('/api/rbac/roles') 20 | ->assertStatus(200) 21 | ->assertJson(['status' => 'ok']); 22 | } 23 | 24 | public function testRoles() 25 | { 26 | $this->getJson('/api/rbac/roles/admin')->assertStatus(401); 27 | 28 | $this->iam('usertest')->getJson('/api/rbac/roles/admin')->assertStatus(403); 29 | 30 | $this->iam('useradmin') 31 | ->getJson('/api/rbac/roles/admin') 32 | ->assertStatus(200) 33 | ->assertJson(['status' => 'ok']); 34 | } 35 | 36 | public function testAttachRoles() 37 | { 38 | $this->postJson('/api/rbac/roles/attach')->assertStatus(401); 39 | 40 | $this->iam('usertest')->postJson('/api/rbac/roles/attach')->assertStatus(403); 41 | 42 | $this->iam('useradmin') 43 | ->postJson('/api/rbac/roles/attach', ['username' => 'usertest', 'rolenames' => ['admin', 'xxxx']]) 44 | ->assertStatus(200) 45 | ->assertJson(['status' => 'error']); 46 | 47 | $this->iam('useradmin') 48 | ->postJson('/api/rbac/roles/attach', ['username' => 'usertest', 'rolenames' => ['admin', 'user']]) 49 | ->assertStatus(200) 50 | ->assertJson(['status' => 'ok']); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 32 | 33 | $status = $kernel->handle( 34 | $input = new Symfony\Component\Console\Input\ArgvInput, 35 | new Symfony\Component\Console\Output\ConsoleOutput 36 | ); 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Shutdown The Application 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Once Artisan has finished running. We will fire off the shutdown events 44 | | so that any final work may be done by the application before we shut 45 | | down the process. This is the last thing to happen to the request. 46 | | 47 | */ 48 | 49 | $kernel->terminate($input, $status); 50 | 51 | exit($status); 52 | -------------------------------------------------------------------------------- /frontend/config/index.js.example: -------------------------------------------------------------------------------- 1 | // 修改下面两行开发 2 | const devRemoteUrl = 'http://192.168.1.108:8000' 3 | const devPort = 8077 4 | 5 | // see http://vuejs-templates.github.io/webpack for documentation. 6 | var path = require('path') 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | index: path.resolve(__dirname, '../dist/index.html'), 12 | mobile: path.resolve(__dirname, '../dist/mobile.html'), 13 | assetsRoot: path.resolve(__dirname, '../dist'), 14 | assetsSubDirectory: 'static', 15 | assetsPublicPath: '/', 16 | productionSourceMap: true, 17 | // Gzip off by default as many popular static hosts such as 18 | // Surge or Netlify already gzip all static assets for you. 19 | // Before setting to `true`, make sure to: 20 | // npm install --save-dev compression-webpack-plugin 21 | productionGzip: false, 22 | productionGzipExtensions: ['js', 'css'], 23 | // Run the build command with an extra argument to 24 | // View the bundle analyzer report after build finishes: 25 | // `npm run build --report` 26 | // Set to `true` or `false` to always turn it on or off 27 | bundleAnalyzerReport: process.env.npm_config_report 28 | }, 29 | dev: { 30 | env: require('./dev.env'), 31 | port: devPort, 32 | autoOpenBrowser: true, 33 | assetsSubDirectory: 'static', 34 | assetsPublicPath: '/', 35 | proxyTable: { 36 | '/api': { 37 | target: devRemoteUrl, 38 | }, 39 | }, 40 | // CSS Sourcemaps off by default because relative paths are "buggy" 41 | // with this option, according to the CSS-Loader README 42 | // (https://github.com/webpack/css-loader#sourcemaps) 43 | // In our experience, they generally work as expected, 44 | // just be aware of this issue when enabling this option. 45 | cssSourceMap: false 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/migrations/2017_03_10_182637_seed_users.php: -------------------------------------------------------------------------------- 1 | 'suadmin', 'displayName' => '超级管理员']); 18 | Artisan::call('rbac:addrole', ['name' => 'admin', 'displayName' => '管理员']); 19 | Artisan::call('rbac:addrole', ['name' => 'user', 'displayName' => '普通用户']); 20 | 21 | Artisan::call('rbac:adduser', ['username' => 'suadmin', 'password' => str_random(10), 'rolename' => 'suadmin', 'name' => '超级管理员']); 22 | Artisan::call('rbac:adduser', ['username' => 'admin', 'password' => str_random(10), 'rolename' => 'admin', 'name' => '管理员']); 23 | Artisan::call('rbac:adduser', ['username' => 'demo', 'password' => str_random(10), 'rolename' => 'user', 'name' => '示例用户']); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Artisan::call('rbac:detachrole', ['username' => 'demo', 'rolename' => 'user']); 34 | Artisan::call('rbac:detachrole', ['username' => 'admin', 'rolename' => 'admin']); 35 | Artisan::call('rbac:detachrole', ['username' => 'suadmin', 'rolename' => 'suadmin']); 36 | 37 | Artisan::call('rbac:removerole', ['name' => 'user']); 38 | Artisan::call('rbac:removerole', ['name' => 'admin']); 39 | Artisan::call('rbac:removerole', ['name' => 'suadmin']); 40 | 41 | DB::table('users')->where(['username' => ['suadmin', 'admin', 'demo']])->delete(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapApiRoutes(); 39 | 40 | $this->mapWebRoutes(); 41 | 42 | // 43 | } 44 | 45 | /** 46 | * Define the "web" routes for the application. 47 | * 48 | * These routes all receive session state, CSRF protection, etc. 49 | * 50 | * @return void 51 | */ 52 | protected function mapWebRoutes() 53 | { 54 | Route::middleware('web') 55 | ->namespace($this->namespace) 56 | ->group(base_path('routes/web.php')); 57 | } 58 | 59 | /** 60 | * Define the "api" routes for the application. 61 | * 62 | * These routes are typically stateless. 63 | * 64 | * @return void 65 | */ 66 | protected function mapApiRoutes() 67 | { 68 | Route::prefix('api') 69 | ->middleware('api') 70 | ->namespace($this->namespace) 71 | ->group(base_path('routes/api.php')); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/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 | desktop: './src/desktop.js', 13 | mobile: './src/mobile.js' 14 | }, 15 | output: { 16 | path: config.build.assetsRoot, 17 | filename: '[name].js', 18 | publicPath: process.env.NODE_ENV === 'production' 19 | ? config.build.assetsPublicPath 20 | : config.dev.assetsPublicPath 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.vue', '.json'], 24 | alias: { 25 | 'vue$': 'vue/dist/vue.esm.js', 26 | '@': resolve('src'), 27 | } 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.(js|vue)$/, 33 | loader: 'eslint-loader', 34 | enforce: "pre", 35 | include: [resolve('src'), resolve('test')], 36 | options: { 37 | formatter: require('eslint-friendly-formatter') 38 | } 39 | }, 40 | { 41 | test: /\.vue$/, 42 | loader: 'vue-loader', 43 | options: vueLoaderConfig 44 | }, 45 | { 46 | test: /\.js$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src'), resolve('test')] 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 52 | loader: 'url-loader', 53 | query: { 54 | limit: 10000, 55 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 56 | } 57 | }, 58 | { 59 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 60 | loader: 'url-loader', 61 | query: { 62 | limit: 10000, 63 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 64 | } 65 | } 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Feature/UserTest.php: -------------------------------------------------------------------------------- 1 | getJson('/api/users')->assertStatus(401); 17 | 18 | $this->iam('usertest')->getJson('/api/users')->assertStatus(403); 19 | 20 | $this->iam('useradmin')->getJson('/api/users') 21 | ->assertStatus(200) 22 | ->assertJson(['status' => 'ok']); 23 | } 24 | 25 | public function testSync() 26 | { 27 | $this->postJson('/api/users/sync')->assertStatus(401); 28 | 29 | $this->iam('usertest')->postJson('/api/users/sync')->assertStatus(403); 30 | 31 | $this->iam('useradmin')->getJson('/api/users/sync')->assertStatus(405); 32 | 33 | $this->iam('useradmin', function () { 34 | Queue::fake(); 35 | })->postJson('/api/users/sync') 36 | ->assertStatus(200) 37 | ->assertJson(['status' => 'ok']); 38 | 39 | Queue::assertPushed(SyncFromQywx::class); 40 | } 41 | 42 | public function testSendMessage() 43 | { 44 | $username = 'cscs'; // 在这里改成你的微信 userid 测试 45 | 46 | $this->postJson('/api/users/sendmessage/xxx')->assertStatus(401); 47 | 48 | $this->iam('usertest')->postJson('/api/users/sendmessage/xx')->assertStatus(403); 49 | 50 | $this->iam('useradmin')->getJson('/api/users/sendmessage/xxx')->assertStatus(405); 51 | 52 | $this->iam('useradmin') 53 | ->postJson('/api/users/sendmessage/' . $username, ['message' => '测试消息']) 54 | ->assertStatus(200) 55 | ->assertJson(['status' => 'ok']); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/DepartmentController.php: -------------------------------------------------------------------------------- 1 | departmentRepo = $departmentRepo; 22 | } 23 | 24 | /** 25 | * 从企业号同步部门 26 | * 27 | * @Post("/sync") 28 | * @Response(200, body={"status": "ok|error", "message": "..."}) 29 | */ 30 | public function sync() 31 | { 32 | dispatch(app(SyncFromQywx::class)); 33 | 34 | return $this->ajax('ok', "已经开始同步,请稍后刷新页面查看同步结果"); 35 | } 36 | 37 | /** 38 | * 列出部门列表 39 | * 40 | * @Get("{?page}") 41 | * @Response(200, body={ 42 | * "status": "ok|error", 43 | * "message": "...", 44 | * "data": { 45 | * "total": 150, 46 | * "per_page": 15, 47 | * "current_page": 1, 48 | * "last_page": 10, 49 | * "next_page_url": "http:\/\/...", 50 | * "prev_page_url": null, 51 | * "from": 1, 52 | * "to": 15, 53 | * "data": { 54 | * {"created_at": "2017-03-14 20:42:26", "departments": "{}", "email": null, "id": 1, "info": "{}", "mobile": "", "name": "超级管理员", "status": 0, "updated_at": "2017-03-14 20:42:49", "username": "suadmin"}, 55 | * } 56 | * }, 57 | * "errors":null, 58 | * "code":0 59 | * }) 60 | */ 61 | public function list() 62 | { 63 | return $this->ajax('ok', '获取成功', $this->departmentRepo->list()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | initApplication(); 20 | 21 | $this->initDatabase(); 22 | } 23 | 24 | public function initApplication() 25 | { 26 | // 配置应用相关 27 | } 28 | 29 | protected function initDatabase() 30 | { 31 | $this->artisan('migrate:refresh'); 32 | $this->artisan('db:seed'); 33 | } 34 | 35 | protected function resetDatabase() 36 | { 37 | $this->artisan('migrate:reset'); 38 | } 39 | 40 | protected function iam($username = null, $refresh = true) 41 | { 42 | if ($refresh !== false) { 43 | // 下面这三句必须加,不然在一个测试里面不能多次请求 44 | $this->refreshApplication(); // 刷新一个新的 app 实例 45 | $this->initApplication(); // 配置 app 参数 46 | $this->artisan('queue:flush'); // 刷去任务队列的任务,以免影响下一次执行 47 | 48 | if ($refresh instanceof Closure) { 49 | $refresh(); 50 | } 51 | } 52 | 53 | if ($username) { 54 | $user = $this->getUser($username); 55 | $token = JWTAuth::fromUser($user); 56 | 57 | return $this->withServerVariables( 58 | $this->transformHeadersToServerVars(['Authorization' => 'Bearer ' . $token]) 59 | ); 60 | } else { 61 | return $this->withServerVariables([]); 62 | } 63 | } 64 | 65 | protected function getUser($username) 66 | { 67 | return User::where(['username' => $username])->firstOrFail(); 68 | } 69 | 70 | public function tearDown() 71 | { 72 | // $this->resetDatabase(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Feature/AuthTest.php: -------------------------------------------------------------------------------- 1 | postJson('/api/login', ['username' => 'useradmin', 'password' => 'xxxxxxxxx']) 17 | ->assertStatus(200) 18 | ->assertJson(['status' => 'error']); 19 | 20 | // 正确密码登录 21 | $this->postJson('/api/login', ['username' => 'useradmin', 'password' => 'password']) 22 | ->assertStatus(200) 23 | ->assertJson(['status' => 'ok']); 24 | } 25 | 26 | public function testLimits() 27 | { 28 | $this->getJson('/api/limits')->assertStatus(401); 29 | 30 | $this->iam('useradmin')->getJson('/api/limits') 31 | ->assertStatus(200) 32 | ->assertJsonStructure(['data' => ['roles', 'perms']]); 33 | } 34 | 35 | public function testQrlogin() 36 | { 37 | $nonce = $this->getJson('/api/qrcode') 38 | ->assertStatus(200) 39 | ->assertJsonStructure(['data' => ['nonce', 'url', 'expires']]) 40 | ->json()['data']['nonce']; 41 | 42 | $this->postJson('/api/qrlogin', ['nonce' => $nonce])->assertStatus(200)->assertJson(['status' => 'error', 'message' => '请扫码']); 43 | 44 | $this->iam('suadmin', false)->postJson('/api/confirmqrlogin', ['nonce' => $nonce, 'login' => false]); 45 | 46 | $this->postJson('/api/qrlogin', ['nonce' => $nonce])->assertStatus(200)->assertJson(['status' => 'error', 'message' => '请确认登录']); 47 | 48 | $this->iam('suadmin', false)->postJson('/api/confirmqrlogin', ['nonce' => $nonce, 'login' => true]); 49 | 50 | $this->postJson('/api/qrlogin', ['nonce' => $nonce])->assertStatus(200)->assertJson(['status' => 'ok']); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Register The Auto Loader 13 | |-------------------------------------------------------------------------- 14 | | 15 | | Composer provides a convenient, automatically generated class loader for 16 | | our application. We just need to utilize it! We'll simply require it 17 | | into the script here so that we don't have to worry about manual 18 | | loading any of our classes later on. It feels nice to relax. 19 | | 20 | */ 21 | 22 | require __DIR__.'/../bootstrap/autoload.php'; 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Turn On The Lights 27 | |-------------------------------------------------------------------------- 28 | | 29 | | We need to illuminate PHP development, so let us turn on the lights. 30 | | This bootstraps the framework and gets it ready for use, then it 31 | | will load up this application so that we can run it and send 32 | | the responses back to the browser and delight our users. 33 | | 34 | */ 35 | 36 | $app = require_once __DIR__.'/../bootstrap/app.php'; 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Run The Application 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Once we have the application, we can handle the incoming request 44 | | through the kernel, and send the associated response back to 45 | | the client's browser allowing them to enjoy the creative 46 | | and wonderful application we have prepared for them. 47 | | 48 | */ 49 | 50 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 51 | 52 | $response = $kernel->handle( 53 | $request = Illuminate\Http\Request::capture() 54 | ); 55 | 56 | $response->send(); 57 | 58 | $kernel->terminate($request, $response); 59 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | 27 | 28 | 51 | 52 |
29 | 30 | {{ $header or '' }} 31 | 32 | 33 | 34 | 46 | 47 | 48 | {{ $footer or '' }} 49 |
35 | 36 | 37 | 38 | 43 | 44 |
39 | {{ Illuminate\Mail\Markdown::parse($slot) }} 40 | 41 | {{ $subcopy or '' }} 42 |
45 |
50 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purocean/laravel-template", 3 | "description": "The Laravel Project Template.", 4 | "keywords": ["framework", "laravel", "template"], 5 | "homepage": "https://github.com/purocean/laravel-template", 6 | "type": "project", 7 | "license": "MIT", 8 | "support": { 9 | "issues": "https://github.com/purocean/laravel-template/issues?state=open" 10 | }, 11 | "require": { 12 | "php": ">=7.0.0", 13 | "dingo/api": "2.*.*@alpha", 14 | "intervention/image": "^2.3", 15 | "jenssegers/mongodb": "^3.3@alpha", 16 | "laravel/framework": "5.5.*", 17 | "laravel/tinker": "~1.0", 18 | "predis/predis": "^1.1", 19 | "purocean/php-wechat-sdk": "^0.3.0", 20 | "tymon/jwt-auth": "^1.0.0@beta", 21 | "zizaco/entrust": "^1.7" 22 | }, 23 | "require-dev": { 24 | "fzaninotto/faker": "~1.4", 25 | "mockery/mockery": "0.9.*", 26 | "phpunit/phpunit": "~5.7" 27 | }, 28 | "autoload": { 29 | "classmap": [ 30 | "database" 31 | ], 32 | "psr-4": { 33 | "App\\": "app/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Tests\\": "tests/" 39 | } 40 | }, 41 | "scripts": { 42 | "post-root-package-install": [ 43 | "php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 44 | ], 45 | "post-create-project-cmd": [ 46 | "php artisan key:generate", 47 | "php artisan jwt:secret" 48 | ], 49 | "post-install-cmd": [ 50 | "Illuminate\\Foundation\\ComposerScripts::postInstall", 51 | "php artisan optimize" 52 | ], 53 | "post-update-cmd": [ 54 | "Illuminate\\Foundation\\ComposerScripts::postUpdate", 55 | "php artisan optimize" 56 | ], 57 | "make-api-doc": [ 58 | "php artisan api:docs --output-file api.md" 59 | ] 60 | }, 61 | "config": { 62 | "preferred-install": "dist", 63 | "sort-packages": true 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 60 | return response()->json(['error' => 'Unauthenticated.'], 401); 61 | } 62 | 63 | return redirect()->guest(route('login')); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/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 | // http://vuejs.github.io/vue-loader/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 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | 'local', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Cloud Filesystem Disk 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Many applications store files both locally and in the cloud. For this 24 | | reason, you may specify a default "cloud" driver here. This driver 25 | | will be bound as the Cloud disk implementation in the container. 26 | | 27 | */ 28 | 29 | 'cloud' => 's3', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Filesystem Disks 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you may configure as many filesystem "disks" as you wish, and you 37 | | may even configure multiple disks of the same driver. Defaults have 38 | | been setup for each driver as an example of the required options. 39 | | 40 | | Supported Drivers: "local", "ftp", "s3", "rackspace" 41 | | 42 | */ 43 | 44 | 'disks' => [ 45 | 46 | 'local' => [ 47 | 'driver' => 'local', 48 | 'root' => storage_path('app'), 49 | ], 50 | 51 | 'public' => [ 52 | 'driver' => 'local', 53 | 'root' => storage_path('app/public'), 54 | 'url' => env('APP_URL').'/storage', 55 | 'visibility' => 'public', 56 | ], 57 | 58 | 's3' => [ 59 | 'driver' => 's3', 60 | 'key' => env('AWS_KEY'), 61 | 'secret' => env('AWS_SECRET'), 62 | 'region' => env('AWS_REGION'), 63 | 'bucket' => env('AWS_BUCKET'), 64 | ], 65 | 66 | ], 67 | 68 | ]; 69 | -------------------------------------------------------------------------------- /app/Repositories/UserRepository.php: -------------------------------------------------------------------------------- 1 | orWhere('username', 'like', "%{$search}%") 20 | ->orWhere('mobile', 'like', "%{$search}%") 21 | ->orWhere('email', 'like', "%{$search}%") 22 | ->paginate(15); 23 | } 24 | 25 | public function getByUsername($username) 26 | { 27 | return User::where('username', $username) 28 | ->first(); 29 | } 30 | 31 | public function getMe() 32 | { 33 | return auth()->user(); 34 | } 35 | 36 | public function sync() 37 | { 38 | $qywx = new Qywx(config('qywx.contacts')); 39 | 40 | $members = $qywx->getDepartmentMembers( 41 | config('qywx.contacts.rootid'), 42 | true, 43 | true); 44 | 45 | $count = 0; 46 | foreach ((array) $members as $member) { 47 | $isNew = false; 48 | if (! $user = $this->getByUsername($member['userid'])) { 49 | $user = new User; 50 | $isNew = true; 51 | } 52 | 53 | $user->name = $member['name']; 54 | $user->email = isset($member['email']) ? ($member['email'] ? $member['email'] : null) : null; 55 | $user->mobile = $member['mobile'] ?? ''; 56 | $user->avatar = $member['avatar'] ?? ''; 57 | $user->info = json_encode($user); 58 | 59 | if ($isNew) { 60 | $user->username = $member['userid']; 61 | $user->password = bcrypt(str_random(8)); 62 | 63 | $user->save() and ++$count; 64 | 65 | // 是新用户就分配默认角色 66 | $role = Role::where('name', 'user')->firstOrFail(); 67 | $user->attachRole($role); 68 | } else { 69 | $user->save() and ++$count; 70 | } 71 | 72 | // 同步组织架构 73 | $user->departments()->sync($member['department']); 74 | } 75 | 76 | return $count; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | version('v1', ['namespace' => 'App\Http\Controllers\Api\V1'], function ($api) { 18 | // 无需登录即可操作 19 | $api->get('qrcode', 'AuthController@qrcode'); 20 | $api->get('wxcode', 'WechatController@code'); 21 | $api->get('wxjs', 'WechatController@wxjs'); 22 | $api->post('login', 'AuthController@login'); 23 | $api->post('qrlogin', 'AuthController@qrlogin'); 24 | $api->post('codelogin', 'AuthController@codelogin'); 25 | // 需要登录才能操作 26 | $api->group(['middleware' => ['api.auth', 'jwt.refresh']], function ($api) { 27 | // 无需特殊权限 28 | $api->post('confirmqrlogin', 'AuthController@confirmqrlogin'); 29 | $api->get('limits', 'AuthController@limits'); 30 | $api->get('file/{id?}', 'FileController@download'); 31 | $api->post('file/upload', 'FileController@upload'); 32 | 33 | // 用户 34 | $api->group(['middleware' => ['can.path:/users/*']], function ($api) { 35 | $api->get('users', 'UserController@list'); 36 | $api->post('users/sync', 'UserController@sync'); 37 | $api->post('users/sendmessage/{username}', 'UserController@sendMessage'); 38 | }); 39 | 40 | // RBAC 41 | $api->group(['middleware' => ['can.path:/rbac/*']], function ($api) { 42 | $api->get('rbac/roles', 'RbacController@allroles'); 43 | $api->get('rbac/roles/users/{rolename}', 'RbacController@usersOfRole'); 44 | $api->get('rbac/roles/{username}', 'RbacController@rolesOfUser'); 45 | $api->post('rbac/roles/attach', 'RbacController@attachRoles'); 46 | }); 47 | 48 | // 部门 49 | $api->group(['middleware' => ['can.path:/departments/*']], function ($api) { 50 | $api->post('/departments/sync', 'DepartmentController@sync'); 51 | $api->get('/departments', 'DepartmentController@list'); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/WechatController.php: -------------------------------------------------------------------------------- 1 | input('state') === 'WechatOAuth') { // 验证回调 14 | $next = $request->input('next'); 15 | $code = $request->input('code'); 16 | 17 | return redirect("/mobile.html#/login/?code={$code}&next=" . urlencode($next)); 18 | } else { 19 | $qywx = new Qywx(config('qywx.app')); 20 | return redirect($qywx->getJumpOAuthUrl(url()->full())); 21 | } 22 | } 23 | 24 | public function wxjs() 25 | { 26 | $qywx = new Qywx(config('qywx.app')); 27 | $jsApiPackage = $qywx->getJsApiPackage(url('mobile.html')); 28 | 29 | $content = <<< JS 30 | wx.config({ 31 | debug: false, 32 | appId: '{$jsApiPackage["corpid"]}', 33 | timestamp: {$jsApiPackage["timestamp"]}, 34 | nonceStr: '{$jsApiPackage["nonceStr"]}', 35 | signature: '{$jsApiPackage["signature"]}', 36 | jsApiList: [ 37 | 'onMenuShareTimeline', 38 | 'onMenuShareAppMessage', 39 | 'onMenuShareQQ', 40 | 'onMenuShareWeibo', 41 | 'onMenuShareQZone', 42 | 'startRecord', 43 | 'stopRecord', 44 | 'onVoiceRecordEnd', 45 | 'playVoice', 46 | 'pauseVoice', 47 | 'stopVoice', 48 | 'onVoicePlayEnd', 49 | 'uploadVoice', 50 | 'downloadVoice', 51 | 'chooseImage', 52 | 'previewImage', 53 | 'uploadImage', 54 | 'downloadImage', 55 | 'translateVoice', 56 | 'getNetworkType', 57 | 'openLocation', 58 | 'getLocation', 59 | 'hideOptionMenu', 60 | 'showOptionMenu', 61 | 'hideMenuItems', 62 | 'showMenuItems', 63 | 'hideAllNonBaseMenuItem', 64 | 'showAllNonBaseMenuItem', 65 | 'closeWindow', 66 | 'scanQRCode', 67 | 'chooseWXPay', 68 | 'openProductSpecificView', 69 | 'addCard', 70 | 'chooseCard', 71 | 'openCard', 72 | ] 73 | }); 74 | 75 | wx.ready(function() { 76 | wx.hideOptionMenu(); 77 | }); 78 | 79 | JS; 80 | 81 | return response($content)->header('Content-Type', 'application/javascript'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /frontend/src/mobile/Qrlogin.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | 57 | 105 | -------------------------------------------------------------------------------- /frontend/src/components/DataTable.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 87 | 88 | 91 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 32 | \App\Http\Middleware\EncryptCookies::class, 33 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 34 | \Illuminate\Session\Middleware\StartSession::class, 35 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 36 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 37 | \App\Http\Middleware\VerifyCsrfToken::class, 38 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 39 | ], 40 | 41 | 'api' => [ 42 | 'throttle:60,1', 43 | 'bindings', 44 | ], 45 | ]; 46 | 47 | /** 48 | * The application's route middleware. 49 | * 50 | * These middleware may be assigned to groups or used individually. 51 | * 52 | * @var array 53 | */ 54 | protected $routeMiddleware = [ 55 | 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 56 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 57 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 58 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 59 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 60 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 61 | 'role' => \Zizaco\Entrust\Middleware\EntrustRole::class, 62 | 'permission' => \Zizaco\Entrust\Middleware\EntrustPermission::class, 63 | 'ability' => \Zizaco\Entrust\Middleware\EntrustAbility::class, 64 | 'can.path' => \App\Http\Middleware\CanPath::class, 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /database/migrations/2017_03_10_155503_entrust_setup_tables.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->string('name')->unique(); 18 | $table->string('display_name')->nullable(); 19 | $table->string('description')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | 23 | // Create table for associating roles to users (Many-to-Many) 24 | Schema::create('role_user', function (Blueprint $table) { 25 | $table->integer('user_id')->unsigned(); 26 | $table->integer('role_id')->unsigned(); 27 | 28 | $table->foreign('user_id')->references('id')->on('users') 29 | ->onUpdate('cascade')->onDelete('cascade'); 30 | $table->foreign('role_id')->references('id')->on('roles') 31 | ->onUpdate('cascade')->onDelete('cascade'); 32 | 33 | $table->primary(['user_id', 'role_id']); 34 | }); 35 | 36 | // Create table for storing permissions 37 | Schema::create('permissions', function (Blueprint $table) { 38 | $table->increments('id'); 39 | $table->string('name')->unique(); 40 | $table->string('display_name')->nullable(); 41 | $table->string('description')->nullable(); 42 | $table->timestamps(); 43 | }); 44 | 45 | // Create table for associating permissions to roles (Many-to-Many) 46 | Schema::create('permission_role', function (Blueprint $table) { 47 | $table->integer('permission_id')->unsigned(); 48 | $table->integer('role_id')->unsigned(); 49 | 50 | $table->foreign('permission_id')->references('id')->on('permissions') 51 | ->onUpdate('cascade')->onDelete('cascade'); 52 | $table->foreign('role_id')->references('id')->on('roles') 53 | ->onUpdate('cascade')->onDelete('cascade'); 54 | 55 | $table->primary(['permission_id', 'role_id']); 56 | }); 57 | } 58 | 59 | /** 60 | * Reverse the migrations. 61 | * 62 | * @return void 63 | */ 64 | public function down() 65 | { 66 | Schema::drop('permission_role'); 67 | Schema::drop('permissions'); 68 | Schema::drop('role_user'); 69 | Schema::drop('roles'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /frontend/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 = process.env.NODE_ENV === 'testing' 14 | ? require('./webpack.prod.conf') 15 | : require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | var port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | var proxyTable = config.dev.proxyTable 24 | 25 | var app = express() 26 | var compiler = webpack(webpackConfig) 27 | 28 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: () => {} 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | compiler.plugin('compilation', function (compilation) { 38 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 39 | hotMiddleware.publish({ action: 'reload' }) 40 | cb() 41 | }) 42 | }) 43 | 44 | // proxy api requests 45 | Object.keys(proxyTable).forEach(function (context) { 46 | var options = proxyTable[context] 47 | if (typeof options === 'string') { 48 | options = { target: options } 49 | } 50 | app.use(proxyMiddleware(options.filter || context, options)) 51 | }) 52 | 53 | // handle fallback for HTML5 history API 54 | app.use(require('connect-history-api-fallback')()) 55 | 56 | // serve webpack bundle output 57 | app.use(devMiddleware) 58 | 59 | // enable hot-reload and state-preserving 60 | // compilation error display 61 | app.use(hotMiddleware) 62 | 63 | // serve pure static assets 64 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 65 | app.use(staticPath, express.static('./static')) 66 | 67 | var uri = 'http://localhost:' + port 68 | 69 | devMiddleware.waitUntilValid(function () { 70 | console.log('> Listening at ' + uri + '\n') 71 | }) 72 | 73 | module.exports = app.listen(port, function (err) { 74 | if (err) { 75 | console.log(err) 76 | return 77 | } 78 | 79 | // when env is testing, don't need open it 80 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 81 | opn(uri) 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Laravel 5.4 RESTful 应用模板,包含企业微信同步联系人,扫码登录,使用 Vue2 iView 做前端 2 | =============================== 3 | [![composer.lock](https://poser.pugx.org/purocean/laravel-template/composerlock)](https://packagist.org/packages/purocean/laravel-template) 4 | [![Latest Stable Version](https://poser.pugx.org/purocean/laravel-template/v/stable)](https://packagist.org/packages/purocean/laravel-template) 5 | [![Total Downloads](https://poser.pugx.org/purocean/laravel-template/downloads)](https://packagist.org/packages/purocean/laravel-template) 6 | [![License](https://poser.pugx.org/purocean/laravel-template/license)](https://packagist.org/packages/purocean/laravel-template) 7 | 8 | > 此项目不再维护,仅作学习参考用,勿用作生产。 9 | 10 | 特性 11 | ------------------- 12 | + [x] 微信企业微信同步联系人 13 | + [x] 扫码登录 14 | + [x] Vue2 iView 前端界面 15 | + [x] 文件上传处理 16 | + [x] 微信 jssdk 17 | + [x] RESTful 18 | 19 | 建议环境 20 | ------------------- 21 | + [x] PHP 7.0+ 22 | + [x] Composer 23 | + [x] Redis 24 | + [x] Mysql 5.7 25 | + [x] MongoDB 3.2+ 26 | 27 | 安装 28 | ------------------- 29 | ```bash 30 | composer install 31 | cp .env.example .env 32 | vim ./.env # 配置数据库,企业微信,缓存等信息 33 | php ./artisan key:generate 34 | php ./artisan jwt:secret 35 | php ./artisan migrate # 迁移表结构 36 | php ./artisan rbac:resetpwd suadmin # 更改超级管理员密码 37 | php ./artisan serve --host=192.168.1.108 # 运行开发服务器,IP 为本机局域网 IP,以便手机访问(扫码) 38 | php ./artisan queue:work # 开启任务队列进程 39 | composer run-script make-api-doc # 生成接口文档 40 | 41 | cd frontend 42 | npm install # 安装 nodejs 依赖 43 | cp ./config/index.js.example ./config/index.js 44 | vim ./config/index.js # 修改本机后台服务器 IP 端口 45 | npm run dev # 运行开发服务器 46 | npm run build # 前端打包 47 | npm run dist # 把打包的文件复制到 public 目录 48 | # npm run dist-win # 把打包的文件复制到 public 目录,windows 平台使用 49 | ``` 50 | 51 | 测试 52 | ------------------- 53 | ```bash 54 | # 创建测试用 MySQL 数据库 laravel_template_test 55 | cp ./phpunit.xml.example ./phpunit.xml # 编辑配置测试相关值 56 | php ./artisan config:clear # 清除配置缓存 57 | composer exec phpunit # 开始测试 58 | ``` 59 | 60 | 注意事项 61 | ------------------- 62 | + 若微信调试不通过,可在 /storage/app/qywx/qywx.log 查看日志,删除缓存文件 63 | + 请使用PHP7 以及开启 OPcache 提高性能 64 | + storage 及其目录需要有写入权限 65 | + 任务队列默认是 sync 方式,可在 .env 文件中修改为 redis 方式 66 | + 如非必要,队列不要用 root 权限执行 67 | + 线上修改了配置文件请需重新运行 *php ./arartisan* 更新配置缓存 68 | + 多项目使用同一个 redis 做任务队列支撑,需要配置好默认的任务队列 69 | 70 | 链接 71 | ------------------- 72 | + [Laravel - The PHP Framework For Web Artisans](https://laravel.com/) 73 | + [Laravel 5.4 中文文档](http://d.laravel-china.org/docs/5.4) 74 | + [vue.js](https://cn.vuejs.org/) 75 | + [iView - 一套高质量的UI组件库](https://www.iviewui.com/) 76 | + [dingoapi](https://github.com/dingo/api) 77 | + [jwt-auth](https://github.com/tymondesigns/jwt-auth) 78 | + [ENTRUST](https://github.com/Zizaco/entrust) 79 | 80 | 截图 81 | ------------------- 82 | ![login_1](./screenshots/login_1.png "登录") 83 | ![login_2](./screenshots/login_2.png "登录") 84 | ![user](./screenshots/user.png "用户管理") 85 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_DRIVER', 'sync'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Queue Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may configure the connection information for each server that 26 | | is used by your application. A default configuration has been added 27 | | for each back-end shipped with Laravel. You are free to add more. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => env('QUEUE_DEFAULT', 'default'), 41 | 'retry_after' => 90, 42 | ], 43 | 44 | 'beanstalkd' => [ 45 | 'driver' => 'beanstalkd', 46 | 'host' => 'localhost', 47 | 'queue' => env('QUEUE_DEFAULT', 'default'), 48 | 'retry_after' => 90, 49 | ], 50 | 51 | 'sqs' => [ 52 | 'driver' => 'sqs', 53 | 'key' => 'your-public-key', 54 | 'secret' => 'your-secret-key', 55 | 'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id', 56 | 'queue' => 'your-queue-name', 57 | 'region' => 'us-east-1', 58 | ], 59 | 60 | 'redis' => [ 61 | 'driver' => 'redis', 62 | 'connection' => 'default', 63 | 'queue' => env('QUEUE_DEFAULT', 'default'), 64 | 'retry_after' => 90, 65 | ], 66 | 67 | ], 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Failed Queue Jobs 72 | |-------------------------------------------------------------------------- 73 | | 74 | | These options configure the behavior of failed queue job logging so you 75 | | can control which database and table are used to store the jobs that 76 | | have failed. You may change them to any database / table you wish. 77 | | 78 | */ 79 | 80 | 'failed' => [ 81 | 'database' => env('DB_CONNECTION', 'mysql'), 82 | 'table' => 'failed_jobs', 83 | ], 84 | 85 | ]; 86 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | */ 30 | 31 | 'stores' => [ 32 | 33 | 'apc' => [ 34 | 'driver' => 'apc', 35 | ], 36 | 37 | 'array' => [ 38 | 'driver' => 'array', 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'table' => 'cache', 44 | 'connection' => null, 45 | ], 46 | 47 | 'file' => [ 48 | 'driver' => 'file', 49 | 'path' => storage_path('framework/cache/data'), 50 | ], 51 | 52 | 'memcached' => [ 53 | 'driver' => 'memcached', 54 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 55 | 'sasl' => [ 56 | env('MEMCACHED_USERNAME'), 57 | env('MEMCACHED_PASSWORD'), 58 | ], 59 | 'options' => [ 60 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 61 | ], 62 | 'servers' => [ 63 | [ 64 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 65 | 'port' => env('MEMCACHED_PORT', 11211), 66 | 'weight' => 100, 67 | ], 68 | ], 69 | ], 70 | 71 | 'redis' => [ 72 | 'driver' => 'redis', 73 | 'connection' => 'default', 74 | ], 75 | 76 | ], 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Cache Key Prefix 81 | |-------------------------------------------------------------------------- 82 | | 83 | | When utilizing a RAM based store such as APC or Memcached, there might 84 | | be other applications utilizing the same cache. So, we'll specify a 85 | | value to get prefixed to all our keys so we can avoid collisions. 86 | | 87 | */ 88 | 89 | 'prefix' => env('CACHE_PREFIX', 'laravel'), 90 | 91 | ]; 92 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Laravel 9 | 10 | 11 | 12 | 13 | 14 | 66 | 67 | 68 |
69 | @if (Route::has('login')) 70 | 78 | @endif 79 | 80 |
81 |
82 | Laravel 83 |
84 | 85 | 92 |
93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /frontend/src/auth/Auth.js: -------------------------------------------------------------------------------- 1 | import Storage from '@/utils/Storage' 2 | import Http from '@/utils/Http' 3 | 4 | const setUser = function (user) { 5 | return Storage.set('auth_user', user) 6 | } 7 | 8 | const getUser = function () { 9 | return Storage.get('auth_user', {}) 10 | } 11 | 12 | const setAccessToken = function (token) { 13 | return Storage.set('auth_token', token) 14 | } 15 | 16 | const getAccessToken = function () { 17 | return Storage.get('auth_token', '') 18 | } 19 | 20 | const isLogin = function () { 21 | return !!getAccessToken() 22 | } 23 | 24 | const getPermissions = function () { 25 | return Storage.get('auth_permissions', {}) 26 | } 27 | 28 | const setPermissions = function (permissions) { 29 | return Storage.set('auth_permissions', permissions) 30 | } 31 | 32 | const getRoles = function () { 33 | return Storage.get('auth_roles', {}) 34 | } 35 | 36 | const setRoles = function (roles) { 37 | return Storage.set('auth_roles', roles) 38 | } 39 | 40 | const checkRole = function (role) { 41 | let roles = getRoles() 42 | return Object.keys(roles).indexOf(role) > -1 43 | } 44 | 45 | const checkPermission = function (permissions, permission) { 46 | if (!Array.isArray(permissions)) { 47 | permissions = Object.keys(permissions) 48 | } 49 | 50 | if (permissions.indexOf(permission) > -1 || permissions.indexOf(permission + '/*') > -1) { 51 | return true 52 | } 53 | 54 | let pos = permission.lastIndexOf('/') 55 | while (pos > -1) { 56 | pos = permission.lastIndexOf('/') 57 | permission = permission.substring(0, pos) 58 | if (permissions.indexOf(permission + '/*') > -1) { 59 | return true 60 | } 61 | } 62 | 63 | return false 64 | } 65 | 66 | /** 67 | * Check from server if callback provided. 68 | */ 69 | const can = function (item, callback) { 70 | if (callback) { 71 | Http.fetch('/api/limits', {}, result => { 72 | setRoles(result.data.roles) 73 | setPermissions(result.data.perms) 74 | callback(checkRole(item) || checkPermission(getPermissions(), item)) 75 | }, error => { 76 | callback(error.status) 77 | }) 78 | } else { 79 | return checkRole(item) || checkPermission(getPermissions(), item) 80 | } 81 | } 82 | 83 | const requireAuth = function (allowList, to, from, next) { 84 | if (checkPermission(allowList, to.path)) { 85 | next() 86 | return 87 | } 88 | 89 | if (isLogin()) { 90 | if (can(to.path)) { 91 | next() 92 | } else { 93 | can(to.path, allow => { 94 | if (allow) { 95 | next() 96 | } else { 97 | next({name: 'error', query: {code: 403, message: '无权访问资源', from: from.fullPath}}) 98 | } 99 | }) 100 | } 101 | } else { 102 | next({name: 'login', query: {next: to.fullPath}}) 103 | } 104 | } 105 | 106 | export default { 107 | requireAuth, 108 | setUser, 109 | getUser, 110 | setAccessToken, 111 | getAccessToken, 112 | getPermissions, 113 | setPermissions, 114 | getRoles, 115 | setRoles, 116 | checkPermission, 117 | checkRole, 118 | isLogin, 119 | can 120 | } 121 | -------------------------------------------------------------------------------- /frontend/src/utils/Http.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch' 2 | import NProgress from 'nprogress' 3 | import Auth from '../auth/Auth' 4 | import Router from 'vue-router' 5 | 6 | const fetchWithAuth = (url, params = {}, cbSuccess = (() => {}), cbError = null, ...other) => { 7 | if (params.showLoading !== false) NProgress.start() 8 | 9 | params = Object.assign({ 10 | headers: Object.assign({ 11 | 'Accept': 'application/json', 12 | 'Content-Type': 'application/json', 13 | 'Authorization': 'Bearer ' + Auth.getAccessToken() 14 | }, params.headers), 15 | }, params, { 16 | body: typeof params.body === 'object' ? JSON.stringify(params.body) : params.body, 17 | }) 18 | 19 | return fetch(url, params, ...other).then(response => { 20 | NProgress.done() 21 | 22 | let token = response.headers.get('Authorization') || '' 23 | if (token) { 24 | Auth.setAccessToken(token.replace(/^Bearer\s+?/i, '')) 25 | } 26 | 27 | if (response.ok) { 28 | if (response.headers.get('Content-Type') === 'application/json') { 29 | response.json().then(data => cbSuccess(data, response)) 30 | } else { 31 | cbSuccess(response.body, response) 32 | } 33 | } else if (!cbError) { 34 | let router = new Router() 35 | switch (response.status) { 36 | case 401: 37 | Auth.setAccessToken('') 38 | Auth.setPermissions({}) 39 | Auth.setRoles({}) 40 | Auth.setUser('') 41 | window.location.href = `${window.location.href}__refresh__` + (new Date().getTime()) 42 | break 43 | case 403: 44 | Auth.setPermissions({}) 45 | Auth.setRoles({}) 46 | router.replace(`/error?code=${response.status}&message=无权访问该资源`) 47 | break 48 | case 404: 49 | router.replace(`/error?code=${response.status}&message=页面找不到`) 50 | break 51 | case 503: 52 | router.replace(`/error?code=${response.status}&message=服务器正在维护,请稍后再来吧`) 53 | break 54 | default: 55 | router.replace(`/error?code=${response.status}&message=出现了一点错误`) 56 | } 57 | } else { 58 | cbError(response) 59 | } 60 | }).catch(error => { 61 | if (cbError) cbError(error) 62 | console.log('There has been a problem with your fetch operation: ' + error.message) 63 | }) 64 | } 65 | 66 | const download = (url, params = {}, callback = (() => {})) => { 67 | fetchWithAuth(url, params, (body, response) => { 68 | response.blob().then(data => { 69 | let fileName = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/ 70 | .exec(response.headers.get('Content-Disposition'))[1] || 'download' 71 | 72 | fileName = fileName.replace(/['"]/g, '') 73 | 74 | if (window.navigator.msSaveOrOpenBlob) { 75 | window.navigator.msSaveBlob(data, fileName) 76 | } else { 77 | let link = document.createElement('a') 78 | link.href = window.URL.createObjectURL(data, {type: response.headers.get('Content-Type')}) 79 | link.download = fileName 80 | link.click() 81 | window.URL.revokeObjectURL(link.href) 82 | } 83 | 84 | callback(response) 85 | }) 86 | }) 87 | } 88 | 89 | export default { fetch: fetchWithAuth, download } 90 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "Yang ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "build": "node build/build.js", 10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 11 | "test": "npm run unit", 12 | "lint": "eslint --ext .js,.vue src test/unit/specs", 13 | "dist": "npm run build && cp -r dist/* ../public/", 14 | "dist-win": "npm run build && xcopy dist\\* ..\\public\\ /s /e /y" 15 | }, 16 | "dependencies": { 17 | "Particleground.js": "^1.1.0", 18 | "iview": "^2.0.0-rc.9", 19 | "jr-qrcode": "^1.0.5", 20 | "nprogress": "^0.2.0", 21 | "vue": "^2.2.2", 22 | "vue-router": "^2.2.0", 23 | "whatwg-fetch": "^2.0.3" 24 | }, 25 | "devDependencies": { 26 | "autoprefixer": "^6.7.2", 27 | "babel-core": "^6.22.1", 28 | "babel-eslint": "^7.1.1", 29 | "babel-loader": "^6.2.10", 30 | "babel-plugin-istanbul": "^3.1.2", 31 | "babel-plugin-transform-runtime": "^6.22.0", 32 | "babel-preset-env": "^1.2.1", 33 | "babel-preset-stage-2": "^6.22.0", 34 | "babel-register": "^6.22.0", 35 | "babel-runtime": "^6.23.0", 36 | "chai": "^3.5.0", 37 | "chalk": "^1.1.3", 38 | "connect-history-api-fallback": "^1.3.0", 39 | "copy-webpack-plugin": "^4.0.1", 40 | "cross-env": "^3.1.4", 41 | "css-loader": "^0.26.1", 42 | "eslint": "^3.14.1", 43 | "eslint-config-standard": "^6.2.1", 44 | "eslint-friendly-formatter": "^2.0.7", 45 | "eslint-loader": "^1.6.1", 46 | "eslint-plugin-html": "^2.0.0", 47 | "eslint-plugin-promise": "^3.4.0", 48 | "eslint-plugin-standard": "^2.0.1", 49 | "eventsource-polyfill": "^0.9.6", 50 | "express": "^4.14.1", 51 | "extract-text-webpack-plugin": "^2.0.0", 52 | "file-loader": "^0.10.0", 53 | "friendly-errors-webpack-plugin": "^1.1.3", 54 | "function-bind": "^1.1.0", 55 | "html-webpack-plugin": "^2.28.0", 56 | "http-proxy-middleware": "^0.17.3", 57 | "inject-loader": "^2.0.1", 58 | "karma": "^1.4.1", 59 | "karma-coverage": "^1.1.1", 60 | "karma-mocha": "^1.3.0", 61 | "karma-phantomjs-launcher": "^1.0.2", 62 | "karma-sinon-chai": "^1.2.4", 63 | "karma-sourcemap-loader": "^0.3.7", 64 | "karma-spec-reporter": "0.0.26", 65 | "karma-webpack": "^2.0.2", 66 | "less": "^2.7.2", 67 | "less-loader": "^3.0.0", 68 | "lolex": "^1.5.2", 69 | "mocha": "^3.2.0", 70 | "opn": "^4.0.2", 71 | "optimize-css-assets-webpack-plugin": "^1.3.0", 72 | "ora": "^1.1.0", 73 | "phantomjs-prebuilt": "^2.1.14", 74 | "rimraf": "^2.6.0", 75 | "semver": "^5.3.0", 76 | "sinon": "^1.17.7", 77 | "sinon-chai": "^2.8.0", 78 | "url-loader": "^0.5.7", 79 | "vue-loader": "^11.1.4", 80 | "vue-style-loader": "^2.0.0", 81 | "vue-template-compiler": "^2.2.1", 82 | "webpack": "^2.2.1", 83 | "webpack-bundle-analyzer": "^2.2.1", 84 | "webpack-dev-middleware": "^1.10.0", 85 | "webpack-hot-middleware": "^2.16.1", 86 | "webpack-merge": "^2.6.1" 87 | }, 88 | "engines": { 89 | "node": ">= 4.0.0", 90 | "npm": ">= 3.0.0" 91 | }, 92 | "browserslist": [ 93 | "> 1%", 94 | "last 2 versions", 95 | "not ie <= 8" 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/UserController.php: -------------------------------------------------------------------------------- 1 | userRepo = $userRepo; 24 | $this->userBiz = $userBiz; 25 | } 26 | 27 | /** 28 | * 从企业号同步用户 29 | * 30 | * @Post("sync") 31 | * @Response(200, body={ 32 | * "status": "ok|error", 33 | * "message": "...", 34 | * "data": null, 35 | * "errors":null, 36 | * "code":0 37 | * }) 38 | */ 39 | public function sync() 40 | { 41 | dispatch(app(SyncFromQywx::class)); 42 | 43 | return $this->ajax('ok', "已经开始同步,请稍后刷新页面查看同步结果"); 44 | } 45 | 46 | /** 47 | * 获取用户列表 48 | * search 参数可以搜索 name,username,mobile,email 49 | * 50 | * @Get("{?page=1&search=管理员}") 51 | * @Response(200, body={ 52 | * "status": "ok|error", 53 | * "message": "...", 54 | * "data": { 55 | * "total": 150, 56 | * "per_page": 15, 57 | * "current_page": 1, 58 | * "last_page": 10, 59 | * "next_page_url": "http:\/\/...", 60 | * "prev_page_url": null, 61 | * "from": 1, 62 | * "to": 15, 63 | * "data": { 64 | * { 65 | * "created_at": "2017-03-14 20:42:26", 66 | * "departments": "{}", 67 | * "email": null, 68 | * "id": 1, 69 | * "info": "{}", 70 | * "mobile": "", 71 | * "name": "超级管理员", 72 | * "status": 0, 73 | * "updated_at": "2017-03-14 20:42:49", 74 | * "username": "suadmin", 75 | }, 76 | * } 77 | * }, 78 | * "errors":null, 79 | * "code":0 80 | * }) 81 | */ 82 | public function list(Request $request) 83 | { 84 | $search = $request->input('search'); 85 | 86 | $data = $this->userRepo->search($search); 87 | 88 | return $this->ajax('ok', '获取成功', $data); 89 | } 90 | 91 | /** 92 | * 向某个用户发送微信消息 93 | * 94 | * @Post("sendmessage/testuser") 95 | * @Request({"message": "测试消息"}) 96 | * @Response(200, body={ 97 | * "status": "ok|error", 98 | * "message": "...", 99 | * "data": null, 100 | * "errors":null, 101 | * "code":0 102 | * }) 103 | */ 104 | public function sendMessage(Request $request, $username) 105 | { 106 | $message = $request->json('message'); 107 | 108 | if ($this->userBiz->sendWxMsg($username, '管理员消息', $message)) { 109 | return $this->ajax('ok', '发送消息成功'); 110 | } 111 | 112 | return $this->ajax('error', '发送消息失败'); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/FileController.php: -------------------------------------------------------------------------------- 1 | input('id'); 26 | $thumb = $request->input('thumb') == 1; 27 | 28 | $file = File::findOrFail($id); 29 | 30 | return response()->download( 31 | $file->realPath($thumb), 32 | ($thumb ? 'thumb_' : '') . $file->name 33 | ); 34 | } 35 | 36 | /** 37 | * 上传文件 38 | * 39 | * @Post("upload") 40 | * @Request({ 41 | * "file": "file....", 42 | * "tag": "tag", 43 | * "title": "title", 44 | * "comment": "comment", 45 | * }) 46 | * @Response(200, body={ 47 | * "status": "ok|error", 48 | * "message": "...", 49 | * "data": { 50 | * "id": "58d1dea704f80a5250007ab3", 51 | * "tag": "hhhtag", 52 | * "title": "kkktitle", 53 | * "comment": "kkkcomment", 54 | * "name": "TIM截图20170317154115.png", 55 | * "mime": "image/png", 56 | * "size": 35640 57 | * }, 58 | * "errors":null, 59 | * "code":0 60 | * }) 61 | */ 62 | public function upload(Request $request) 63 | { 64 | $tag = $request->input('tag'); 65 | $title = $request->input('title'); 66 | $comment = $request->input('comment'); 67 | 68 | if (! $uploadFile = $request->file('file')) { 69 | return $this->ajax('error', '上传文件失败'); 70 | } 71 | 72 | if (! $path = $uploadFile->store('upload/'.date('Y_m_d'))) { 73 | return $this->ajax('error', '储存文件失败'); 74 | } 75 | 76 | $file = new File; 77 | 78 | $file->tag = $tag; 79 | $file->title = $title; 80 | $file->comment = $comment; 81 | $file->name = $uploadFile->getClientOriginalName(); 82 | $file->path = $path; 83 | $file->mime = $uploadFile->getClientMimeType(); 84 | $file->size = $uploadFile->getClientSize(); 85 | 86 | if (! $file->save()) { 87 | return $this->ajax('error', '保存文件信息失败'); 88 | } 89 | 90 | // 为图片生成缩略图 91 | if (stripos($file->mime, 'image/') === 0) { 92 | $img = Image::make($file->realPath()) 93 | ->resize(400, null, function ($constraint) { 94 | $constraint->aspectRatio(); 95 | }); 96 | 97 | $img->save($file->realPath(true), 90); 98 | } 99 | 100 | return $this->ajax('ok', '上传成功', [ 101 | 'id' => $file->id, 102 | 'tag' => $file->tag, 103 | 'title' => $file->title, 104 | 'comment' => $file->comment, 105 | 'name' => $file->name, 106 | 'mime' => $file->mime, 107 | 'size' => $file->size, 108 | ]); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /config/entrust.php: -------------------------------------------------------------------------------- 1 | 'App\Role', 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Entrust Roles Table 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This is the roles table used by Entrust to save roles to the database. 30 | | 31 | */ 32 | 'roles_table' => 'roles', 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Entrust Permission Model 37 | |-------------------------------------------------------------------------- 38 | | 39 | | This is the Permission model used by Entrust to create correct relations. 40 | | Update the permission if it is in a different namespace. 41 | | 42 | */ 43 | 'permission' => 'App\Permission', 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Entrust Permissions Table 48 | |-------------------------------------------------------------------------- 49 | | 50 | | This is the permissions table used by Entrust to save permissions to the 51 | | database. 52 | | 53 | */ 54 | 'permissions_table' => 'permissions', 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Entrust permission_role Table 59 | |-------------------------------------------------------------------------- 60 | | 61 | | This is the permission_role table used by Entrust to save relationship 62 | | between permissions and roles to the database. 63 | | 64 | */ 65 | 'permission_role_table' => 'permission_role', 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Entrust role_user Table 70 | |-------------------------------------------------------------------------- 71 | | 72 | | This is the role_user table used by Entrust to save assigned roles to the 73 | | database. 74 | | 75 | */ 76 | 'role_user_table' => 'role_user', 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | User Foreign key on Entrust's role_user Table (Pivot) 81 | |-------------------------------------------------------------------------- 82 | */ 83 | 'user_foreign_key' => 'user_id', 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Role Foreign key on Entrust's role_user and permission_role Tables (Pivot) 88 | |-------------------------------------------------------------------------- 89 | */ 90 | 'role_foreign_key' => 'role_id', 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Permission Foreign key on Entrust's permission_role Table (Pivot) 95 | |-------------------------------------------------------------------------- 96 | */ 97 | 'permission_foreign_key' => 'permission_id', 98 | ]; 99 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | composer install --no-dev --ignore-platform-reqs 4 | 5 | cp .env.example .env 6 | 7 | read -p "APP_ENV (local|production, default: proportion): " ans 8 | if [ -z "$ans" ] 9 | then 10 | ans='proportion' 11 | fi 12 | sed -i "s/^APP_ENV=.*$/APP_ENV=$ans/" .env 13 | 14 | read -p "APP_DEBUG (true|false, default: false): " ans 15 | if [ -z "$ans" ] 16 | then 17 | ans='false' 18 | fi 19 | sed -i "s/^APP_DEBUG=.*$/APP_DEBUG=$ans/" .env 20 | 21 | read -p "APP_URL (default: http://localhost): " ans 22 | if [ -z "$ans" ] 23 | then 24 | ans='http://localhost' 25 | fi 26 | sed -i "s~^APP_URL=.*$~APP_URL=${ans}~" .env 27 | 28 | read -p "DB_HOST (default: 127.0.0.1): " ans 29 | if [ -z "$ans" ] 30 | then 31 | ans='127.0.0.1' 32 | fi 33 | sed -i "s/^DB_HOST=.*$/DB_HOST=$ans/" .env 34 | 35 | read -p "DB_PORT (default: 3306): " ans 36 | if [ -z "$ans" ] 37 | then 38 | ans='3306' 39 | fi 40 | sed -i "s/^DB_PORT=.*$/DB_PORT=$ans/" .env 41 | 42 | read -p "DB_DATABASE (default: homestead): " ans 43 | if [ -z "$ans" ] 44 | then 45 | ans='homestead' 46 | fi 47 | sed -i "s/^DB_DATABASE=.*$/DB_DATABASE=$ans/" .env 48 | 49 | read -p "DB_USERNAME (default: homestead): " ans 50 | if [ -z "$ans" ] 51 | then 52 | ans='homestead' 53 | fi 54 | sed -i "s/^DB_USERNAME=.*$/DB_USERNAME=$ans/" .env 55 | 56 | read -sp "DB_PASSWORD (default: secret): " ans 57 | echo 58 | if [ -z "$ans" ] 59 | then 60 | ans='secret' 61 | fi 62 | sed -i "s/^DB_PASSWORD=.*$/DB_PASSWORD=$ans/" .env 63 | 64 | read -p "MONGODB_HOST (default: localhost): " ans 65 | if [ -z "$ans" ] 66 | then 67 | ans='localhost' 68 | fi 69 | sed -i "s/^MONGODB_HOST=.*$/MONGODB_HOST=$ans/" .env 70 | 71 | read -p "MONGODB_PORT (default: 27017): " ans 72 | if [ -z "$ans" ] 73 | then 74 | ans='27017' 75 | fi 76 | sed -i "s/^MONGODB_PORT=.*$/MONGODB_PORT=$ans/" .env 77 | 78 | read -p "MONGODB_DATABASE (default: laravel_template): " ans 79 | if [ -z "$ans" ] 80 | then 81 | ans='laravel_template' 82 | fi 83 | sed -i "s/^MONGODB_DATABASE=.*$/MONGODB_DATABASE=$ans/" .env 84 | 85 | read -p "CACHE_DRIVER (array|file|..., default: redis): " ans 86 | if [ -z "$ans" ] 87 | then 88 | ans='redis' 89 | fi 90 | sed -i "s/^CACHE_DRIVER=.*$/CACHE_DRIVER=$ans/" .env 91 | 92 | read -p "CACHE_PREFIX (default: laravel_template): " ans 93 | if [ -z "$ans" ] 94 | then 95 | ans='laravel_template' 96 | fi 97 | sed -i "s/^CACHE_PREFIX=.*$/CACHE_PREFIX=$ans/" .env 98 | 99 | read -p "QUEUE_DRIVER (sync|redis|..., default: redis): " ans 100 | if [ -z "$ans" ] 101 | then 102 | ans='redis' 103 | fi 104 | sed -i "s/^QUEUE_DRIVER=.*$/QUEUE_DRIVER=$ans/" .env 105 | 106 | read -p "API_DEBUG (true|false, default: false): " ans 107 | if [ -z "$ans" ] 108 | then 109 | ans='false' 110 | fi 111 | sed -i "s/^API_DEBUG=.*$/API_DEBUG=$ans/" .env 112 | 113 | read -p "QYWX_ROOTID (default: 1): " ans 114 | if [ -z "$ans" ] 115 | then 116 | ans='1' 117 | fi 118 | sed -i "s/^QYWX_ROOTID=.*$/QYWX_ROOTID=$ans/" .env 119 | 120 | read -p "QYWX_CORPID: " ans 121 | sed -i "s/^QYWX_CORPID=.*$/QYWX_CORPID=$ans/" .env 122 | 123 | read -p "QYWX_CONTACTS_SECRET: " ans 124 | sed -i "s/^QYWX_CONTACTS_SECRET=.*$/QYWX_CONTACTS_SECRET=$ans/" .env 125 | 126 | read -p "QYWX_SECRET: " ans 127 | sed -i "s/^QYWX_SECRET=.*$/QYWX_SECRET=$ans/" .env 128 | 129 | read -p "QYWX_APPID: " ans 130 | sed -i "s/^QYWX_APPID=.*$/QYWX_APPID=$ans/" .env 131 | 132 | php ./artisan key:generate --force 133 | php ./artisan jwt:secret --force 134 | php ./artisan migrate --force 135 | 136 | read -p "suadmin password:" ans 137 | if [ ! -z "$ans" ] 138 | then 139 | php ./artisan rbac:resetpwd suadmin "$ans" 140 | fi 141 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => 'web', 18 | 'passwords' => 'users', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | here which uses session storage and the Eloquent user provider. 29 | | 30 | | All authentication drivers have a user provider. This defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | mechanisms used by this application to persist your user's data. 33 | | 34 | | Supported: "session", "token" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | 44 | 'api' => [ 45 | 'driver' => 'token', 46 | 'provider' => 'users', 47 | ], 48 | ], 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | User Providers 53 | |-------------------------------------------------------------------------- 54 | | 55 | | All authentication drivers have a user provider. This defines how the 56 | | users are actually retrieved out of your database or other storage 57 | | mechanisms used by this application to persist your user's data. 58 | | 59 | | If you have multiple user tables or models you may configure multiple 60 | | sources which represent each model / table. These sources may then 61 | | be assigned to any extra authentication guards you have defined. 62 | | 63 | | Supported: "database", "eloquent" 64 | | 65 | */ 66 | 67 | 'providers' => [ 68 | 'users' => [ 69 | 'driver' => 'eloquent', 70 | 'model' => App\User::class, 71 | 'table' => 'users', 72 | ], 73 | 74 | // 'users' => [ 75 | // 'driver' => 'database', 76 | // 'table' => 'users', 77 | // ], 78 | ], 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Resetting Passwords 83 | |-------------------------------------------------------------------------- 84 | | 85 | | You may specify multiple password reset configurations if you have more 86 | | than one user table or model in the application and you want to have 87 | | separate password reset settings based on the specific user types. 88 | | 89 | | The expire time is the number of minutes that the reset token should be 90 | | considered valid. This security feature keeps tokens short-lived so 91 | | they have less time to be guessed. You may change this as needed. 92 | | 93 | */ 94 | 95 | 'passwords' => [ 96 | 'users' => [ 97 | 'provider' => 'users', 98 | 'table' => 'password_resets', 99 | 'expire' => 60, 100 | ], 101 | ], 102 | 103 | ]; 104 | --------------------------------------------------------------------------------