├── public ├── favicon.ico ├── robots.txt ├── .htaccess └── index.php ├── resources ├── css │ └── app.css ├── js │ ├── src │ │ ├── components │ │ │ ├── table │ │ │ │ ├── Tr.vue │ │ │ │ ├── TBody.vue │ │ │ │ ├── THead.vue │ │ │ │ ├── Td.vue │ │ │ │ ├── Th.vue │ │ │ │ └── Table.vue │ │ │ ├── ui │ │ │ │ ├── SaveButton.vue │ │ │ │ ├── EditButton.vue │ │ │ │ ├── DeleteButton.vue │ │ │ │ ├── CreateButton.vue │ │ │ │ ├── 401.vue │ │ │ │ ├── FormLabelError.vue │ │ │ │ ├── Spinner.vue │ │ │ │ ├── Button.vue │ │ │ │ ├── FormInput.vue │ │ │ │ └── Slider.vue │ │ │ └── page │ │ │ │ ├── AuthorizationFallback.vue │ │ │ │ ├── PermissionSlider.vue │ │ │ │ └── ConfirmModal.vue │ │ ├── utils │ │ │ └── axios.js │ │ ├── layouts │ │ │ ├── suspense-fallback │ │ │ │ └── Default.vue │ │ │ ├── SuspenseFallback.vue │ │ │ ├── LayoutRenderer.vue │ │ │ ├── LayoutFull.vue │ │ │ ├── LayoutDashboard.vue │ │ │ └── PageLoader.vue │ │ ├── composables │ │ │ ├── useAuth.js │ │ │ ├── useModalToast.js │ │ │ ├── useAppRouter.js │ │ │ ├── useUtils.js │ │ │ ├── useSlider.js │ │ │ ├── useTheme.js │ │ │ ├── useValidation.js │ │ │ └── useHttpRequest.js │ │ ├── store │ │ │ ├── useRoleStore.js │ │ │ ├── usePermissionStore.js │ │ │ └── useUserStore.js │ │ ├── assets │ │ │ └── css │ │ │ │ └── vue-select-override.css │ │ ├── router │ │ │ ├── index.js │ │ │ └── routes.js │ │ ├── directives │ │ │ └── focus.js │ │ └── pages │ │ │ ├── Permissions.vue │ │ │ ├── Roles.vue │ │ │ ├── Login.vue │ │ │ └── Users.vue │ ├── App.vue │ ├── app.js │ └── app.css └── views │ └── app.blade.php ├── database ├── .gitignore ├── migrations │ ├── 2023_11_25_192216_create_roles_table.php │ ├── 2023_11_25_192233_create_permissions_table.php │ ├── 2014_10_12_100000_create_password_reset_tokens_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2023_11_25_192311_create_role_user_table.php │ ├── 2023_11_25_192402_create_permission_role_table.php │ └── 2019_12_14_000001_create_personal_access_tokens_table.php ├── seeders │ ├── PermissionRoleTableSeeder.php │ ├── DatabaseSeeder.php │ ├── RoleTableSeeder.php │ ├── RoleUserTableSeeder.php │ ├── PermissionTableSeeder.php │ └── UserTableSeeder.php └── factories │ └── UserFactory.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── postcss.config.js ├── routes ├── web.php ├── channels.php ├── console.php └── api.php ├── tests ├── TestCase.php ├── Unit │ └── ExampleTest.php ├── Feature │ └── ExampleTest.php └── CreatesApplication.php ├── .gitattributes ├── .prettierrc.json ├── .editorconfig ├── .gitignore ├── app ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ ├── AuthController.php │ │ ├── RoleController.php │ │ ├── PermissionController.php │ │ └── UserController.php │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── Authenticate.php │ │ ├── ValidateSignature.php │ │ ├── TrustProxies.php │ │ ├── RedirectIfAuthenticated.php │ │ └── EnsureUserHasPermission.php │ └── Kernel.php ├── Models │ ├── Permission.php │ ├── Role.php │ └── User.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php ├── Traits │ ├── Helpers.php │ └── Error.php ├── Console │ └── Kernel.php └── Exceptions │ └── Handler.php ├── config ├── cors.php ├── services.php ├── view.php ├── hashing.php ├── broadcasting.php ├── filesystems.php ├── sanctum.php ├── cache.php ├── queue.php ├── mail.php ├── auth.php ├── logging.php ├── database.php └── app.php ├── package.json ├── phpunit.xml ├── .env.example ├── artisan ├── vite.config.js ├── composer.json ├── tailwind.config.js └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /resources/js/src/components/table/Tr.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/src/components/table/TBody.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/src/utils/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | axios.defaults.baseURL = `/api`; 4 | axios.defaults.withCredentials = true; 5 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | view('app') 8 | )->where('any', '.*'); 9 | -------------------------------------------------------------------------------- /resources/js/src/components/table/THead.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/src/components/table/Td.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/src/components/table/Th.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@prettier/plugin-php"], 3 | "tabWidth": 4, 4 | "trailingComma": "all", 5 | "arrowParens": "always", 6 | "printWidth": 80, 7 | "semi": true, 8 | "singleQuote": true, 9 | "singleAttributePerLine": true, 10 | "vueIndentScriptAndStyle": false 11 | } 12 | -------------------------------------------------------------------------------- /resources/js/src/layouts/suspense-fallback/Default.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/js/src/components/ui/SaveButton.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/app 7 | /storage/*.key 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpunit.result.cache 13 | Homestead.json 14 | Homestead.yaml 15 | auth.json 16 | npm-debug.log 17 | yarn-error.log 18 | /.fleet 19 | /.idea 20 | /.vscode 21 | -------------------------------------------------------------------------------- /resources/js/src/components/ui/EditButton.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/js/src/components/ui/DeleteButton.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /resources/js/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Models/Permission.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Permission::class, 'permission_role', 'permission_id', 'role_id'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts(): array 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | expectsJson() ? null : route('login'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /resources/js/src/layouts/SuspenseFallback.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /app/Traits/Helpers.php: -------------------------------------------------------------------------------- 1 | roles as $role) { 14 | $permissions = $permissions->merge($role->permissions); 15 | } 16 | 17 | $permissions = $permissions 18 | ->flatten() 19 | ->unique('id') 20 | ->values(); 21 | 22 | $user->permissions = $permissions; 23 | 24 | return $user; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /resources/js/src/composables/useAuth.js: -------------------------------------------------------------------------------- 1 | import useHttpRequest from './useHttpRequest'; 2 | import useUserStore from '../store/useUserStore'; 3 | 4 | const useAuth = () => { 5 | const { index: verify } = useHttpRequest('/auth/verify'); 6 | const userStore = useUserStore(); 7 | 8 | const isUserAuthenticated = async () => { 9 | const user = await verify(); 10 | if (!Array.isArray(user) && user?.id) { 11 | userStore.setUser(user); 12 | return true; 13 | } 14 | return false; 15 | }; 16 | 17 | return { 18 | isUserAuthenticated, 19 | }; 20 | }; 21 | 22 | export default useAuth; 23 | -------------------------------------------------------------------------------- /resources/js/src/layouts/LayoutRenderer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | */ 22 | public function boot(): void 23 | { 24 | // 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 16 | } 17 | 18 | /** 19 | * Register the commands for the application. 20 | */ 21 | protected function commands(): void 22 | { 23 | $this->load(__DIR__.'/Commands'); 24 | 25 | require base_path('routes/console.php'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2023_11_25_192216_create_roles_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('roles'); 27 | } 28 | }; -------------------------------------------------------------------------------- /database/migrations/2023_11_25_192233_create_permissions_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('permissions'); 27 | } 28 | }; -------------------------------------------------------------------------------- /resources/js/src/store/useRoleStore.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | import { defineStore } from 'pinia'; 3 | 4 | import useHttpRequest from '../composables/useHttpRequest'; 5 | 6 | const useRoleStore = defineStore('roles', () => { 7 | const { 8 | index: getRoles, 9 | loading: rolesLoading, 10 | initialLoading: rolesFirstTimeLoading, 11 | } = useHttpRequest('/roles'); 12 | 13 | const roles = ref([]); 14 | const loadRoles = async () => { 15 | const res = await getRoles(); 16 | roles.value = res; 17 | }; 18 | 19 | return { 20 | roles, 21 | loadRoles, 22 | rolesLoading, 23 | rolesFirstTimeLoading, 24 | }; 25 | }); 26 | 27 | export default useRoleStore; 28 | -------------------------------------------------------------------------------- /app/Models/Role.php: -------------------------------------------------------------------------------- 1 | belongsToMany( 17 | Role::class, 18 | 'role_user', 19 | 'role_id', 20 | 'user_id' 21 | ); 22 | } 23 | 24 | public function permissions() 25 | { 26 | return $this->belongsToMany( 27 | Permission::class, 28 | 'permission_role', 29 | 'role_id', 30 | 'permission_id' 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/js/src/assets/css/vue-select-override.css: -------------------------------------------------------------------------------- 1 | .v-select .vs__dropdown-toggle { 2 | height: 40px; 3 | background: theme('colors.gray.50'); 4 | } 5 | 6 | .vs__dropdown-option--highlight { 7 | background: theme('colors.active.DEFAULT'); 8 | } 9 | 10 | .dark .v-select .vs__dropdown-toggle { 11 | background: theme('colors.gray.700'); 12 | color: theme('colors.dark-color'); 13 | } 14 | 15 | .dark .v-select .vs__selected { 16 | color: theme('colors.dark-color'); 17 | } 18 | 19 | .dark .v-select .vs__dropdown-menu { 20 | background: theme('colors.cc-10'); 21 | } 22 | 23 | .dark .v-select .vs__clear svg { 24 | stroke: #aaa; 25 | fill: #aaa; 26 | } 27 | .dark .v-select .vs__open-indicator { 28 | stroke: #aaa; 29 | fill: #aaa; 30 | } 31 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $dontFlash = [ 16 | 'current_password', 17 | 'password', 18 | 'password_confirmation', 19 | ]; 20 | 21 | /** 22 | * Register the exception handling callbacks for the application. 23 | */ 24 | public function register(): void 25 | { 26 | $this->reportable(function (Throwable $e) { 27 | // 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /database/seeders/PermissionRoleTableSeeder.php: -------------------------------------------------------------------------------- 1 | 1, 21 | 'permission_id' => $i, 22 | 'created_at' => now(), 23 | ]; 24 | 25 | $permissionRole[] = $item; 26 | } 27 | 28 | DB::table('permission_role')->insert($permissionRole); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Laravel vue role permission management 11 | 12 | @vite('resources/js/app.js') 13 | 14 | 15 | 16 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php: -------------------------------------------------------------------------------- 1 | string('email')->primary(); 16 | $table->string('token'); 17 | $table->timestamp('created_at')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('password_reset_tokens'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import './app.css'; 4 | import 'vue-select/dist/vue-select.css'; 5 | import './src/assets/css/vue-select-override.css'; 6 | import './src/utils/axios'; 7 | 8 | import router from './src/router'; 9 | import { createPinia } from 'pinia'; 10 | 11 | import Toast from 'vue-toastification'; 12 | import 'vue-toastification/dist/index.css'; 13 | 14 | import focus from './src/directives/focus'; 15 | 16 | const app = createApp(App); 17 | const pinia = createPinia(); 18 | 19 | app.use(pinia) 20 | .use(router) 21 | .use(Toast, { 22 | pauseOnFocusLoss: false, 23 | hideProgressBar: true, 24 | timeout: 10000, 25 | }) 26 | .directive('focus', focus) 27 | .mount('#app'); 28 | 29 | app.config.warnHandler = () => null; 30 | -------------------------------------------------------------------------------- /resources/js/src/store/usePermissionStore.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | import { defineStore } from 'pinia'; 3 | 4 | import useHttpRequest from '../composables/useHttpRequest'; 5 | 6 | const usePermissionsStore = defineStore('permissions', () => { 7 | const { 8 | index: getPermissions, 9 | loading: permissionsLoading, 10 | initialLoading: permissionsFirstTimeLoading, 11 | } = useHttpRequest('/permissions'); 12 | 13 | const permissions = ref([]); 14 | const loadPermissions = async () => { 15 | const res = await getPermissions(); 16 | permissions.value = res; 17 | }; 18 | 19 | return { 20 | permissions, 21 | loadPermissions, 22 | permissionsLoading, 23 | permissionsFirstTimeLoading, 24 | }; 25 | }); 26 | 27 | export default usePermissionsStore; 28 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | // \App\Models\User::factory()->create([ 18 | // 'name' => 'Test User', 19 | // 'email' => 'test@example.com', 20 | // ]); 21 | 22 | $this->call(UserTableSeeder::class); 23 | $this->call(RoleTableSeeder::class); 24 | $this->call(PermissionTableSeeder::class); 25 | $this->call(RoleUserTableSeeder::class); 26 | $this->call(PermissionRoleTableSeeder::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/js/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import routes from './routes'; 3 | import useAuth from '../composables/useAuth'; 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(), 7 | routes, 8 | }); 9 | 10 | router.beforeEach(async (to, from, next) => { 11 | const { isUserAuthenticated } = useAuth(); 12 | 13 | if (from.name === to.name) { 14 | return next(); 15 | } 16 | 17 | const isUserLoggedIn = await isUserAuthenticated(); 18 | 19 | if (isUserLoggedIn) { 20 | if (to.name === 'login') { 21 | return next({ name: 'users' }); 22 | } 23 | } else { 24 | if (to.name !== 'login') { 25 | return next({ name: 'login' }); 26 | } 27 | } 28 | return next(); 29 | }); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /resources/js/src/components/page/AuthorizationFallback.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 33 | -------------------------------------------------------------------------------- /database/seeders/RoleTableSeeder.php: -------------------------------------------------------------------------------- 1 | 'super-admin', 18 | 'created_at' => now(), 19 | ], 20 | [ 21 | 'name' => 'admin', 22 | 'created_at' => now(), 23 | ], 24 | [ 25 | 'name' => 'author', 26 | 'created_at' => now(), 27 | ], 28 | [ 29 | 'name' => 'editor', 30 | 'created_at' => now(), 31 | ], 32 | ]; 33 | 34 | DB::table('roles')->insert($roles); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 24 | return redirect(RouteServiceProvider::HOME); 25 | } 26 | } 27 | 28 | return $next($request); 29 | } 30 | } -------------------------------------------------------------------------------- /resources/js/src/directives/focus.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mounted: function (el, { value, arg, modifiers }) { 3 | if (!el) return; 4 | if (arg === 'mounted' || value) { 5 | el.focus(); 6 | } 7 | if (arg === 'select' || modifiers.select) { 8 | setTimeout(() => { 9 | if (document.activeElement === el) el.select(); 10 | }, 50); 11 | } 12 | }, 13 | updated: function (el, { value, oldValue, arg, modifiers }) { 14 | if (!el) return; 15 | if (modifiers.once && arg === 'mounted') return; 16 | if (value && value !== oldValue) { 17 | el.focus(); 18 | if (arg === 'select' || modifiers.select) { 19 | setTimeout(() => { 20 | if (document.activeElement === el) el.select(); 21 | }, 50); 22 | } 23 | } 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /resources/js/src/store/useUserStore.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | import { defineStore } from 'pinia'; 3 | 4 | import useHttpRequest from '../composables/useHttpRequest'; 5 | 6 | const useUserStore = defineStore('users', () => { 7 | const { 8 | index: getUsers, 9 | loading: usersLoading, 10 | initialLoading: usersFirstTimeLoading, 11 | } = useHttpRequest('/users'); 12 | 13 | const user = ref(null); 14 | const users = ref([]); 15 | 16 | const setUser = (authUser) => { 17 | user.value = authUser; 18 | }; 19 | 20 | const loadUsers = async () => { 21 | const response = await getUsers(); 22 | users.value = response; 23 | }; 24 | 25 | return { 26 | user, 27 | setUser, 28 | users, 29 | usersLoading, 30 | usersFirstTimeLoading, 31 | loadUsers, 32 | }; 33 | }); 34 | 35 | export default useUserStore; 36 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('users'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('uuid')->unique(); 17 | $table->text('connection'); 18 | $table->text('queue'); 19 | $table->longText('payload'); 20 | $table->longText('exception'); 21 | $table->timestamp('failed_at')->useCurrent(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('failed_jobs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => true, 33 | 34 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build" 7 | }, 8 | "devDependencies": { 9 | "@prettier/plugin-php": "^0.21.0", 10 | "@vitejs/plugin-vue": "^4.5.0", 11 | "autoprefixer": "^10.4.16", 12 | "axios": "^1.6.1", 13 | "laravel-vite-plugin": "^0.8.0", 14 | "postcss": "^8.4.31", 15 | "prettier": "^3.1.0", 16 | "tailwindcss": "^3.3.5", 17 | "tailwindcss-animation-delay": "^1.2.0", 18 | "vite": "^4.0.0" 19 | }, 20 | "dependencies": { 21 | "dotenv": "^16.3.1", 22 | "pinia": "^2.1.7", 23 | "tiny-emitter": "^2.1.0", 24 | "uuid": "^9.0.1", 25 | "vue": "^3.3.9", 26 | "vue-loader": "^17.3.1", 27 | "vue-router": "^4.2.5", 28 | "vue-select": "^4.0.0-beta.6", 29 | "vue-toastification": "^2.0.0-rc.5", 30 | "yup": "^1.3.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2023_11_25_192311_create_role_user_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->unsignedBigInteger('role_id'); 17 | $table->unsignedBigInteger('user_id'); 18 | $table->timestamps(); 19 | 20 | $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); 21 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('role_user'); 31 | } 32 | }; -------------------------------------------------------------------------------- /database/migrations/2023_11_25_192402_create_permission_role_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->unsignedBigInteger('role_id'); 17 | $table->unsignedBigInteger('permission_id'); 18 | $table->timestamps(); 19 | 20 | $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); 21 | $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('permission_role'); 31 | } 32 | }; -------------------------------------------------------------------------------- /database/seeders/RoleUserTableSeeder.php: -------------------------------------------------------------------------------- 1 | 1, 18 | 'user_id' => 1, 19 | 'created_at' => now(), 20 | ], 21 | [ 22 | 'role_id' => 2, 23 | 'user_id' => 2, 24 | 'created_at' => now(), 25 | ], 26 | [ 27 | 'role_id' => 3, 28 | 'user_id' => 3, 29 | 'created_at' => now(), 30 | ], 31 | [ 32 | 'role_id' => 4, 33 | 'user_id' => 4, 34 | 'created_at' => now(), 35 | ], 36 | ]; 37 | 38 | DB::table('role_user')->insert($roleUser); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('tokenable'); 17 | $table->string('name'); 18 | $table->string('token', 64)->unique(); 19 | $table->text('abilities')->nullable(); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('expires_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('personal_access_tokens'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | */ 26 | public function boot(): void 27 | { 28 | // 29 | } 30 | 31 | /** 32 | * Determine if events and listeners should be automatically discovered. 33 | */ 34 | public function shouldDiscoverEvents(): bool 35 | { 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/js/src/layouts/LayoutFull.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /resources/js/src/composables/useModalToast.js: -------------------------------------------------------------------------------- 1 | import emitter from 'tiny-emitter/instance'; 2 | import { useToast } from 'vue-toastification'; 3 | 4 | const useModalToast = () => { 5 | const toast = useToast(); 6 | // valid value of 'data' function argument 7 | // value should be null if want to render the default data 8 | // { 9 | // message: string // any text to show, 10 | // actionButton: { 11 | // class: string // action button class, 12 | // text: string // any text to render on the action button, 13 | // }, 14 | // returnButton: { 15 | // class: string // action button class, 16 | // text: string // any text to render on the return button, 17 | // }, 18 | // } 19 | const showConfirmModal = (data, callback) => { 20 | emitter.emit('show-confirm-modal', data, callback); 21 | }; 22 | 23 | const showToast = (message, type = 'success', options = {}) => { 24 | toast[type](message, options); 25 | }; 26 | 27 | return { 28 | showConfirmModal, 29 | showToast, 30 | }; 31 | }; 32 | 33 | export default useModalToast; 34 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /resources/js/src/router/routes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | name: 'login', 5 | component: () => import('../pages/Login.vue'), 6 | meta: { 7 | layout: 'full', 8 | permissions: [], 9 | }, 10 | }, 11 | 12 | { 13 | path: '/users', 14 | name: 'users', 15 | component: () => import('../pages/Users.vue'), 16 | meta: { 17 | layout: 'dashboard', 18 | permissions: ['users-all', 'users-view'], 19 | }, 20 | }, 21 | 22 | { 23 | path: '/roles', 24 | name: 'roles', 25 | component: () => import('../pages/Roles.vue'), 26 | meta: { 27 | layout: 'dashboard', 28 | permissions: ['roles-all', 'roles-view'], 29 | }, 30 | }, 31 | 32 | { 33 | path: '/permissions', 34 | name: 'permissions', 35 | component: () => import('../pages/Permissions.vue'), 36 | meta: { 37 | layout: 'dashboard', 38 | permissions: ['permissions-all', 'permissions-view'], 39 | }, 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | protected static ?string $password; 15 | 16 | /** 17 | * Define the model's default state. 18 | * 19 | * @return array 20 | */ 21 | public function definition(): array 22 | { 23 | return [ 24 | 'name' => fake()->name(), 25 | 'email' => fake()->unique()->safeEmail(), 26 | 'email_verified_at' => now(), 27 | 'password' => static::$password ??= Hash::make('password'), 28 | 'remember_token' => Str::random(10), 29 | ]; 30 | } 31 | 32 | /** 33 | * Indicate that the model's email address should be unverified. 34 | */ 35 | public function unverified(): static 36 | { 37 | return $this->state(fn (array $attributes) => [ 38 | 'email_verified_at' => null, 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/js/src/components/ui/401.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /database/seeders/PermissionTableSeeder.php: -------------------------------------------------------------------------------- 1 | $name, 37 | 'created_at' => now(), 38 | ]; 39 | }, $permissions); 40 | 41 | DB::table('permissions')->insert($permissions); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | ]; 25 | 26 | /** 27 | * The attributes that should be hidden for serialization. 28 | * 29 | * @var array 30 | */ 31 | protected $hidden = [ 32 | 'password', 33 | 'remember_token', 34 | ]; 35 | 36 | /** 37 | * The attributes that should be cast. 38 | * 39 | * @var array 40 | */ 41 | protected $casts = [ 42 | 'email_verified_at' => 'datetime', 43 | 'password' => 'hashed', 44 | ]; 45 | 46 | public function roles() { 47 | return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id'); 48 | } 49 | } -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | by($request->user()?->id ?: $request->ip()); 29 | }); 30 | 31 | $this->routes(function () { 32 | Route::middleware('api') 33 | ->prefix('api') 34 | ->group(base_path('routes/api.php')); 35 | 36 | Route::middleware('web') 37 | ->group(base_path('routes/web.php')); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY=base64:BNRn58cujolcMCmBtdjN/8TLExrxoDQUBmiKw8h/p1o= 4 | APP_DEBUG=true 5 | APP_URL=http://127.0.0.1:8000 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=mysql 12 | DB_HOST=127.0.0.1 13 | DB_PORT=3306 14 | DB_DATABASE=laravel 15 | DB_USERNAME=root 16 | DB_PASSWORD= 17 | 18 | BROADCAST_DRIVER=log 19 | CACHE_DRIVER=file 20 | FILESYSTEM_DISK=local 21 | QUEUE_CONNECTION=sync 22 | SESSION_DRIVER=file 23 | SESSION_LIFETIME=120 24 | 25 | MEMCACHED_HOST=127.0.0.1 26 | 27 | REDIS_HOST=127.0.0.1 28 | REDIS_PASSWORD=null 29 | REDIS_PORT=6379 30 | 31 | MAIL_MAILER=smtp 32 | MAIL_HOST=mailpit 33 | MAIL_PORT=1025 34 | MAIL_USERNAME=null 35 | MAIL_PASSWORD=null 36 | MAIL_ENCRYPTION=null 37 | MAIL_FROM_ADDRESS="hello@example.com" 38 | MAIL_FROM_NAME="${APP_NAME}" 39 | 40 | AWS_ACCESS_KEY_ID= 41 | AWS_SECRET_ACCESS_KEY= 42 | AWS_DEFAULT_REGION=us-east-1 43 | AWS_BUCKET= 44 | AWS_USE_PATH_STYLE_ENDPOINT=false 45 | 46 | PUSHER_APP_ID= 47 | PUSHER_APP_KEY= 48 | PUSHER_APP_SECRET= 49 | PUSHER_HOST= 50 | PUSHER_PORT=443 51 | PUSHER_SCHEME=https 52 | PUSHER_APP_CLUSTER=mt1 53 | 54 | SANCTUM_STATEFUL_DOMAINS=127.0.0.1:8000 55 | 56 | VITE_APP_NAME="${APP_NAME}" 57 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 58 | VITE_PUSHER_HOST="${PUSHER_HOST}" 59 | VITE_PUSHER_PORT="${PUSHER_PORT}" 60 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 61 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" -------------------------------------------------------------------------------- /resources/js/src/components/ui/FormLabelError.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 56 | -------------------------------------------------------------------------------- /resources/js/src/composables/useAppRouter.js: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { useRoute, useRouter } from 'vue-router'; 3 | 4 | const useAppRouter = () => { 5 | const route = useRoute(); 6 | const router = useRouter(); 7 | 8 | // Route Params 9 | const routeParams = computed(() => route.params); 10 | const routeQuery = computed(() => route.query); 11 | const routeName = computed(() => route.name); 12 | const routeMeta = computed(() => route.meta); 13 | 14 | const isActiveRoute = (givenRoute, allowChildMatch = true) => { 15 | const resolved = router.resolve(givenRoute); 16 | if (allowChildMatch) { 17 | return route.path.includes(resolved.path); 18 | } 19 | return Boolean(resolved.path === route.path); 20 | }; 21 | 22 | const resolvedRoute = (route) => router.resolve(route); 23 | 24 | const pushToRoute = async (givenRoute) => { 25 | const resolved = router.resolve(givenRoute); 26 | 27 | let success = null; 28 | if (resolved && resolved.fullPath !== route.fullPath) 29 | success = await router.push(resolved); 30 | return success; 31 | }; 32 | 33 | return { 34 | route, 35 | router, 36 | routeParams, 37 | routeQuery, 38 | routeName, 39 | routeMeta, 40 | isActiveRoute, 41 | resolvedRoute, 42 | pushToRoute, 43 | }; 44 | }; 45 | 46 | export default useAppRouter; 47 | -------------------------------------------------------------------------------- /resources/js/src/composables/useUtils.js: -------------------------------------------------------------------------------- 1 | const useUtils = () => { 2 | const stringCapitalize = (string) => 3 | string?.charAt(0).toUpperCase() + string?.slice(1).toLowerCase(); 4 | 5 | const isObject = (data) => { 6 | if (typeof data === 'object' && !Array.isArray(data) && data !== null) 7 | return true; 8 | return false; 9 | }; 10 | 11 | const isEmptyObject = (obj) => { 12 | return Object.keys(obj).length === 0; 13 | }; 14 | 15 | const omitPropsFromObject = (object, omittingProps = []) => { 16 | if (!omittingProps || !omittingProps.length) return object; 17 | return Object.entries(object).reduce((fields, field) => { 18 | if (!omittingProps.includes(field[0])) 19 | fields = { ...fields, [field[0]]: field[1] }; 20 | return fields; 21 | }, {}); 22 | }; 23 | 24 | const filterPropsFromObject = (object, filteringProps = []) => { 25 | if (!filteringProps || !filteringProps.length) return object; 26 | return Object.entries(object).reduce((fields, field) => { 27 | if (filteringProps.includes(field[0])) 28 | fields = { ...fields, [field[0]]: field[1] }; 29 | return fields; 30 | }, {}); 31 | }; 32 | 33 | return { 34 | stringCapitalize, 35 | isObject, 36 | isEmptyObject, 37 | omitPropsFromObject, 38 | filterPropsFromObject, 39 | }; 40 | }; 41 | 42 | export default useUtils; 43 | -------------------------------------------------------------------------------- /resources/js/src/composables/useSlider.js: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue'; 2 | const useSlider = (sliderName = '', options = {}) => { 3 | const defaultOptions = { 4 | initialSliderValue: false, 5 | disableWindowScrollOnActive: true, 6 | }; 7 | options = { ...defaultOptions, options }; 8 | 9 | const slider = ref(options.initialSliderValue); 10 | const sliderData = ref(null); 11 | 12 | const showSlider = (name, data = null) => { 13 | slider.value = name; 14 | sliderData.value = data; 15 | }; 16 | const hideSlider = () => { 17 | slider.value = typeof slider.value === 'boolean' ? false : null; 18 | sliderData.value = null; 19 | }; 20 | 21 | const updateSliderData = (items, itemKey = 'id') => { 22 | if (!sliderData.value || !items || !items.length) return; 23 | 24 | const item = items.value.find( 25 | (item) => item[itemKey] == sliderData.value[itemKey], 26 | ); 27 | if (item) sliderData.value = item; 28 | }; 29 | 30 | if (options.disableWindowScrollOnActive) { 31 | watch( 32 | () => slider.value, 33 | () => { 34 | if (slider.value) 35 | document.body.classList.add('overflow-hidden'); 36 | else document.body.classList.remove('overflow-hidden'); 37 | }, 38 | ); 39 | } 40 | return { 41 | slider, 42 | sliderData, 43 | sliderName, 44 | showSlider, 45 | hideSlider, 46 | updateSliderData, 47 | }; 48 | }; 49 | 50 | export default useSlider; 51 | -------------------------------------------------------------------------------- /resources/js/src/layouts/LayoutDashboard.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 43 | -------------------------------------------------------------------------------- /resources/js/src/composables/useTheme.js: -------------------------------------------------------------------------------- 1 | import { ref, computed, provide, onMounted, onBeforeUnmount } from 'vue'; 2 | 3 | const useTheme = () => { 4 | const theme = ref('light'); 5 | const windowWidth = ref(1600); 6 | const isDarkMode = computed(() => Boolean(theme.value === 'dark')); 7 | 8 | if ( 9 | localStorage.theme === 'dark' || 10 | (!('theme' in localStorage) && 11 | window.matchMedia('(prefers-color-scheme: dark)').matches) 12 | ) { 13 | document.documentElement.classList.add('dark'); 14 | theme.value = 'dark'; 15 | } else { 16 | document.documentElement.classList.remove('dark'); 17 | theme.value = 'light'; 18 | } 19 | 20 | const setWindowWidth = () => { 21 | windowWidth.value = document.body.clientWidth; 22 | }; 23 | 24 | onMounted(() => { 25 | setWindowWidth(); 26 | }); 27 | 28 | window.addEventListener('resize', setWindowWidth, false); 29 | 30 | onBeforeUnmount(() => { 31 | window.removeEventListener('resize', setWindowWidth, false); 32 | }); 33 | 34 | const updateDarkMode = (darkMode) => { 35 | if (darkMode) { 36 | theme.value = 'dark'; 37 | localStorage.setItem('theme', 'dark'); 38 | document.documentElement.classList.add('dark'); 39 | } else { 40 | theme.value = 'light'; 41 | localStorage.setItem('theme', 'light'); 42 | document.documentElement.classList.remove('dark'); 43 | } 44 | }; 45 | 46 | provide('theme', { isDarkMode, updateDarkMode, windowWidth }); 47 | }; 48 | 49 | export default useTheme; 50 | -------------------------------------------------------------------------------- /app/Http/Middleware/EnsureUserHasPermission.php: -------------------------------------------------------------------------------- 1 | find(Auth::id()); 31 | if (!$user) { 32 | return response()->json(['message' => 'Unauthenticated.'], 401); 33 | } 34 | $user = $this->extractPermissionsFromUser($user); 35 | 36 | $userPermissions = $user->permissions 37 | ->map(fn($permission) => $permission->name) 38 | ->values() 39 | ->all(); 40 | 41 | if (!count(array_intersect($userPermissions, $requiredPermissions))) { 42 | return response()->json( 43 | [ 44 | 'message' => 'Permission not granted.', 45 | 'permissions' => $requiredPermissions, 46 | ], 47 | 401 48 | ); 49 | } 50 | 51 | $request->merge(['user' => $user]); 52 | 53 | return $next($request); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /resources/js/src/components/ui/Spinner.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /app/Traits/Error.php: -------------------------------------------------------------------------------- 1 | formatValidationError($error); 13 | } else { 14 | return $this->formatExceptionError($error); 15 | } 16 | } 17 | 18 | protected function formatExceptionError($error): array 19 | { 20 | $errorsArray = explode('--', $error->getMessage()); 21 | $errorStatus = count($errorsArray) > 1 ? (int) $errorsArray[1] : 500; 22 | 23 | $errorData = explode('|', $errorsArray[0]); 24 | $errorMessage = $errorData[0]; 25 | $errorText = count($errorData) > 1 ? $errorData[1] : ''; 26 | 27 | return [ 28 | 'errorMessage' => $errorMessage, 29 | 'errorText' => $errorText, 30 | 'errorCode' => $error->getCode(), 31 | 'errorStatus' => $errorStatus, 32 | ]; 33 | } 34 | 35 | protected function formatValidationError($error): array 36 | { 37 | $errorText = array_map(function ($errorsArray) { 38 | return implode(', ', array_values($errorsArray)); 39 | }, $error->errors()); 40 | $errorText = implode(', ', array_values($errorText)); 41 | return [ 42 | 'errorMessage' => 'Data validation error', 43 | 'errorText' => $errorText, 44 | 'errorCode' => 13335, 45 | 'errorStatus' => 403, 46 | ]; 47 | } 48 | 49 | protected function errorResponse(\Exception $error): JsonResponse 50 | { 51 | $errorData = $this->formatError($error); 52 | return response()->json($errorData, $errorData['errorStatus']); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 12), 33 | 'verify' => true, 34 | ], 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Argon Options 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Here you may specify the configuration options that should be used when 42 | | passwords are hashed using the Argon algorithm. These will allow you 43 | | to control the amount of time it takes to hash the given password. 44 | | 45 | */ 46 | 47 | 'argon' => [ 48 | 'memory' => 65536, 49 | 'threads' => 1, 50 | 'time' => 4, 51 | 'verify' => true, 52 | ], 53 | 54 | ]; 55 | -------------------------------------------------------------------------------- /resources/js/src/layouts/PageLoader.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 64 | 65 | 70 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import vue from '@vitejs/plugin-vue'; 4 | import path from 'path'; 5 | import dotenv from 'dotenv'; 6 | 7 | export default defineConfig(() => { 8 | const isDevCommantRunOnClient = process.env.INIT_CWD.includes('client'); 9 | 10 | const env = dotenv.config({ 11 | path: path.join( 12 | process.env.INIT_CWD, 13 | `${isDevCommantRunOnClient ? '../' : ''}.env`, 14 | ), 15 | }); 16 | 17 | const frontendEnvs = ['APP_URL']; 18 | 19 | const clientEnv = Object.entries(env.parsed) 20 | .filter(([key, _]) => frontendEnvs.includes(key)) 21 | .reduce( 22 | (clientEnv, [key, value]) => ({ ...clientEnv, [key]: value }), 23 | {}, 24 | ); 25 | 26 | return { 27 | plugins: [ 28 | laravel({ 29 | input: ['resources/js/app.js'], 30 | refresh: true, 31 | }), 32 | vue({ 33 | template: { 34 | transformAssetUrls: { 35 | // The Vue plugin will re-write asset URLs, when referenced 36 | // in Single File Components, to point to the Laravel web 37 | // server. Setting this to `null` allows the Laravel plugin 38 | // to instead re-write asset URLs to point to the Vite 39 | // server instead. 40 | base: null, 41 | 42 | // The Vue plugin will parse absolute URLs and treat them 43 | // as absolute paths to files on disk. Setting this to 44 | // `false` will leave absolute URLs un-touched so they can 45 | // reference assets in the public directory as expected. 46 | includeAbsolute: false, 47 | }, 48 | }, 49 | }), 50 | ], 51 | define: { 52 | 'process.env': clientEnv, 53 | }, 54 | }; 55 | }); 56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The skeleton application for the Laravel framework.", 5 | "keywords": ["laravel", "framework"], 6 | "license": "MIT", 7 | "require": { 8 | "php": "^8.1", 9 | "guzzlehttp/guzzle": "^7.2", 10 | "laravel/framework": "^10.10", 11 | "laravel/sanctum": "^3.3", 12 | "laravel/tinker": "^2.8" 13 | }, 14 | "require-dev": { 15 | "fakerphp/faker": "^1.9.1", 16 | "laravel/pint": "^1.0", 17 | "laravel/sail": "^1.18", 18 | "mockery/mockery": "^1.4.4", 19 | "nunomaduro/collision": "^7.0", 20 | "phpunit/phpunit": "^10.1", 21 | "spatie/laravel-ignition": "^2.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "App\\": "app/", 26 | "Database\\Factories\\": "database/factories/", 27 | "Database\\Seeders\\": "database/seeders/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Tests\\": "tests/" 33 | } 34 | }, 35 | "scripts": { 36 | "post-autoload-dump": [ 37 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 38 | "@php artisan package:discover --ansi" 39 | ], 40 | "post-update-cmd": [ 41 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 42 | ], 43 | "post-root-package-install": [ 44 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 45 | ], 46 | "post-create-project-cmd": [ 47 | "@php artisan key:generate --ansi" 48 | ] 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "dont-discover": [] 53 | } 54 | }, 55 | "config": { 56 | "optimize-autoloader": true, 57 | "preferred-install": "dist", 58 | "sort-packages": true, 59 | "allow-plugins": { 60 | "pestphp/pest-plugin": true, 61 | "php-http/discovery": true 62 | } 63 | }, 64 | "minimum-stability": "stable", 65 | "prefer-stable": true 66 | } 67 | -------------------------------------------------------------------------------- /app/Http/Controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | validate([ 20 | 'email' => ['required', 'email'], 21 | 'password' => ['required'], 22 | ]); 23 | 24 | $user = User::with('roles.permissions') 25 | ->where('email', $credentials['email']) 26 | ->first(); 27 | if ( 28 | !$user || 29 | !Hash::check($credentials['password'], $user->password) 30 | ) { 31 | throw new \Exception( 32 | 'Error|Credentials doesn\'t match--403', 33 | 13333 34 | ); 35 | } 36 | 37 | $request->session()->regenerate(); 38 | Auth::loginUsingId($user->id, true); 39 | 40 | return response()->json($this->extractPermissionsFromUser($user)); 41 | } catch (\Exception $error) { 42 | return $this->errorResponse($error); 43 | } 44 | } 45 | 46 | public function verify() 47 | { 48 | if (!($id = Auth::id())) { 49 | return response()->json(['message' => 'Unauthenticated.'], 401); 50 | } 51 | $user = User::with('roles.permissions')->find($id); 52 | 53 | return response()->json($this->extractPermissionsFromUser($user)); 54 | } 55 | 56 | public function logout(Request $request) 57 | { 58 | try { 59 | Auth::guard('web')->logout(); 60 | 61 | $request->session()->invalidate(); 62 | $request->session()->regenerateToken(); 63 | 64 | return response()->json(true); 65 | } catch (\Exception $error) { 66 | return $this->errorResponse($error); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /resources/js/src/composables/useValidation.js: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | const useValidation = () => { 4 | const getData = (schema, value) => { 5 | if (!schema?.fields || !Object.keys(schema?.fields)?.length) { 6 | return value; 7 | } 8 | 9 | return Object.entries(schema?.fields || {}).reduce((r, field) => { 10 | const [key, val] = field; 11 | if ('fields' in val && Object.keys(val)?.length) { 12 | if (value && typeof value === 'object' && key in value) { 13 | return { ...r, [key]: getData(val, value[key]) }; 14 | } 15 | return r; 16 | } else { 17 | const keyVal = 18 | value && typeof value === 'object' ? value[key] : value; 19 | 20 | return { ...r, [key]: keyVal }; 21 | } 22 | }, {}); 23 | }; 24 | 25 | const runYupValidation = async (schema, value) => { 26 | try { 27 | await schema.validate(value, { 28 | abortEarly: false, 29 | }); 30 | 31 | return { 32 | validated: true, 33 | data: getData(schema, value), 34 | errors: {}, 35 | }; 36 | } catch (error) { 37 | if (error instanceof yup.ValidationError) { 38 | return { 39 | validated: false, 40 | data: {}, 41 | errors: error.inner.reduce((prev, err, index) => { 42 | const { path, message } = err; 43 | if (!message) return prev; 44 | return { ...prev, [path ? path : index]: message }; 45 | }, {}), 46 | }; 47 | } 48 | 49 | return { 50 | validated: true, 51 | data: getData(schema, value), 52 | errors: {}, 53 | }; 54 | } 55 | }; 56 | 57 | const requiredIfExists = (value, { createError, path }) => { 58 | if (value === undefined) return true; 59 | if (!value) return createError({ message: `${path} is required` }); 60 | return true; 61 | }; 62 | 63 | return { 64 | runYupValidation, 65 | requiredIfExists, 66 | }; 67 | }; 68 | 69 | export default useValidation; 70 | -------------------------------------------------------------------------------- /resources/js/app.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Nabla:EDPT,EHLT@30..200,24&display=swap'); 2 | 3 | :root { 4 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 5 | line-height: 1.5; 6 | font-weight: 400; 7 | 8 | font-synthesis: none; 9 | text-rendering: optimizeLegibility; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | /* a { 15 | font-weight: 500; 16 | color: #646cff; 17 | text-decoration: inherit; 18 | } 19 | a:hover { 20 | color: #747bff; 21 | } 22 | 23 | .dark a:hover { 24 | color: #535bf2; 25 | } */ 26 | 27 | @tailwind base; 28 | @tailwind components; 29 | @tailwind utilities; 30 | 31 | @layer components { 32 | .flex-start { 33 | @apply flex flex-row items-center; 34 | } 35 | .flex-between { 36 | @apply flex flex-row justify-between items-center; 37 | } 38 | .flex-center { 39 | @apply flex flex-row justify-center items-center; 40 | } 41 | .flex-end { 42 | @apply flex flex-row justify-end items-center; 43 | } 44 | } 45 | 46 | @font-palette-values --Nabla { 47 | font-family: Nabla; 48 | /* base-palette: 1; */ 49 | 50 | override-colors: 51 | 0 #fb923c, 52 | 1 #009688, 53 | 2 #fb923c, 54 | 3 #009688, 55 | 4 #009688, 56 | 5 #6ee7b7, 57 | 6 #16a34a, 58 | 7 #6ee7b7, 59 | 8 #009688, 60 | 9 #fda4af; 61 | } 62 | 63 | span.logo-char { 64 | animation: depth 1s ease-in-out alternate infinite; 65 | position: relative; 66 | display: inline-block; 67 | font-variation-settings: 'EDPT' 30; 68 | font-palette: --Nabla; 69 | } 70 | 71 | @keyframes depth { 72 | 0% { 73 | transform: translatex(0) translatey(0); 74 | } 75 | 100% { 76 | font-variation-settings: 'EDPT' 200; 77 | transform: translatex(0.07em) translatey(0.05em); 78 | } 79 | } 80 | 81 | .edit-list-move, 82 | .edit-list-enter-active, 83 | .edit-list-leave-active { 84 | transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); 85 | } 86 | 87 | .edit-list-enter-from, 88 | .edit-list-leave-to { 89 | opacity: 0; 90 | transform: scaleY(0.01) translate(30px, 0); 91 | } 92 | 93 | .edit-list-leave-active { 94 | width: 100%; 95 | position: absolute; 96 | } 97 | -------------------------------------------------------------------------------- /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 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 41 | 'port' => env('PUSHER_PORT', 443), 42 | 'scheme' => env('PUSHER_SCHEME', 'https'), 43 | 'encrypted' => true, 44 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 45 | ], 46 | 'client_options' => [ 47 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 48 | ], 49 | ], 50 | 51 | 'ably' => [ 52 | 'driver' => 'ably', 53 | 'key' => env('ABLY_KEY'), 54 | ], 55 | 56 | 'redis' => [ 57 | 'driver' => 'redis', 58 | 'connection' => 'default', 59 | ], 60 | 61 | 'log' => [ 62 | 'driver' => 'log', 63 | ], 64 | 65 | 'null' => [ 66 | 'driver' => 'null', 67 | ], 68 | 69 | ], 70 | 71 | ]; 72 | -------------------------------------------------------------------------------- /database/seeders/UserTableSeeder.php: -------------------------------------------------------------------------------- 1 | 'Mr. Super Admin', 21 | 'email' => 'sadmin@sadmin.com', 22 | 'password' => $password, 23 | 'created_at' => now(), 24 | ], 25 | [ 26 | 'name' => 'Mr. Admin', 27 | 'email' => 'admin@admin.com', 28 | 'password' => $password, 29 | 'created_at' => now(), 30 | ], 31 | [ 32 | 'name' => 'Mr. Author', 33 | 'email' => 'author@author.com', 34 | 'password' => $password, 35 | 'created_at' => now(), 36 | ], 37 | [ 38 | 'name' => 'Mr. Editor', 39 | 'email' => 'editor@editor.com', 40 | 'password' => $password, 41 | 'created_at' => now(), 42 | ], 43 | [ 44 | 'name' => 'Mr. User 1', 45 | 'email' => 'user1@user.com', 46 | 'password' => $password, 47 | 'created_at' => now(), 48 | ], 49 | [ 50 | 'name' => 'Mr. User 2', 51 | 'email' => 'user2@user.com', 52 | 'password' => $password, 53 | 'created_at' => now(), 54 | ], 55 | [ 56 | 'name' => 'Mr. User 3', 57 | 'email' => 'user3@user.com', 58 | 'password' => $password, 59 | 'created_at' => now(), 60 | ], 61 | [ 62 | 'name' => 'Mr. User 4', 63 | 'email' => 'user4@user.com', 64 | 'password' => $password, 65 | 'created_at' => now(), 66 | ], 67 | [ 68 | 'name' => 'Mr. User', 69 | 'email' => 'user@user.com', 70 | 'password' => $password, 71 | 'created_at' => now(), 72 | ], 73 | ]; 74 | 75 | DB::table('users')->insert($users); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been set up for each driver as an example of the required values. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | 'throw' => false, 37 | ], 38 | 39 | 'public' => [ 40 | 'driver' => 'local', 41 | 'root' => storage_path('app/public'), 42 | 'url' => env('APP_URL').'/storage', 43 | 'visibility' => 'public', 44 | 'throw' => false, 45 | ], 46 | 47 | 's3' => [ 48 | 'driver' => 's3', 49 | 'key' => env('AWS_ACCESS_KEY_ID'), 50 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 51 | 'region' => env('AWS_DEFAULT_REGION'), 52 | 'bucket' => env('AWS_BUCKET'), 53 | 'url' => env('AWS_URL'), 54 | 'endpoint' => env('AWS_ENDPOINT'), 55 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 56 | 'throw' => false, 57 | ], 58 | 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Symbolic Links 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Here you may configure the symbolic links that will be created when the 67 | | `storage:link` Artisan command is executed. The array keys should be 68 | | the locations of the links and the values should be their targets. 69 | | 70 | */ 71 | 72 | 'links' => [ 73 | public_path('storage') => storage_path('app/public'), 74 | ], 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /resources/js/src/components/ui/Button.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 74 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | import colors from 'tailwindcss/colors'; 3 | import animationDelay from 'tailwindcss-animation-delay'; 4 | 5 | export default { 6 | darkMode: 'class', 7 | content: [ 8 | './resources/**/*.blade.php', 9 | './resources/**/*.js', 10 | './resources/**/*.vue', 11 | ], 12 | theme: { 13 | extend: { 14 | screens: { 15 | xs: '470px', 16 | mobile: '420px', 17 | }, 18 | fontFamily: { 19 | lora: ['Lora'], 20 | nabla: ['Nabla'], 21 | }, 22 | fontSize: { 23 | xsm: '13px', 24 | '2xs': '11px', 25 | xxs: '10px', 26 | }, 27 | colors: { 28 | 'dark-bg': '#060818', 29 | 'dark-color': 'rgba(255, 255, 255, 0.87)', 30 | 'light-bg': '#ffffff', 31 | 'light-color': '#213547', 32 | active: { 33 | // DEFAULT: '#E2C537', 34 | DEFAULT: colors.emerald[500], 35 | light: colors.emerald[700], 36 | dark: colors.emerald[500], 37 | hover: colors.emerald[200], 38 | }, 39 | 'cc-12': '#1b2e4b', 40 | 'cc-18': '#191e3a', 41 | 'cc-10': '#0e1726', 42 | 'cc-13': '#25d5e4', 43 | 'cc-14': '#009688', 44 | 'cc-19': '#060818', 45 | 'cc-20': '#1a1c2d', 46 | 'cc-21': '#3b3f5c', 47 | }, 48 | transitionProperty: { 49 | width: 'width', 50 | height: 'height', 51 | 'max-height': 'max-height', 52 | }, 53 | boxShadow: { 54 | google: '0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12), 0px 5px 5px -3px rgba(0, 0, 0, 0.2)', 55 | 'google-sm': 56 | '0px 2px 2px 1px rgba(0, 0, 0, 0.14), 0px 2px 2px 2px rgba(0, 0, 0, 0.12), 0px 2px 2px 0px rgba(0, 0, 0, 0.2)', 57 | 58 | 'google-dark': 59 | '0px 8px 10px 1px rgba(14, 16, 9, 0.86), 0px 3px 14px 2px rgba(14, 16, 9, 0.88), 0px 5px 5px -3px rgba(14, 16, 9, 0.8)', 60 | }, 61 | borderRadius: { 62 | sm: '4px', 63 | }, 64 | animationDelay: { 65 | 100: '100ms', 66 | 200: '200ms', 67 | 300: '300ms', 68 | 400: '400ms', 69 | 500: '500ms', 70 | 600: '600ms', 71 | 700: '700ms', 72 | 800: '800ms', 73 | 900: '900ms', 74 | 1000: '1000ms', 75 | 1100: '1100ms', 76 | 1200: '1200ms', 77 | 1300: '1300ms', 78 | 1400: '1400ms', 79 | 1500: '1500ms', 80 | 1600: '1600ms', 81 | }, 82 | }, 83 | }, 84 | plugins: [animationDelay], 85 | }; 86 | -------------------------------------------------------------------------------- /app/Http/Controllers/RoleController.php: -------------------------------------------------------------------------------- 1 | get(); 16 | return response()->json($roles); 17 | } 18 | 19 | public function store(Request $request) 20 | { 21 | try { 22 | $validated = $request->validate([ 23 | 'name' => ['required', 'unique:roles,name'], 24 | ]); 25 | 26 | $role = new Role(); 27 | foreach ($validated as $key => $val) { 28 | $role->{$key} = $val; 29 | } 30 | $role->save(); 31 | 32 | if (isset($request->permissions) && is_array($request->permissions)) { 33 | $role->permissions()->sync($request->permissions); 34 | } 35 | return response()->json($role); 36 | } catch (\Exception $error) { 37 | return $this->errorResponse($error); 38 | } 39 | } 40 | 41 | public function update(Request $request) 42 | { 43 | try { 44 | $validated = $request->validate([ 45 | 'name' => [ 46 | 'required', 47 | Rule::unique('roles')->ignore($request->roleId), 48 | ], 49 | ]); 50 | 51 | $role = Role::find($request->roleId); 52 | if (!$role) { 53 | throw new \Exception('Error|Role not found--404', 13333); 54 | } 55 | if ($role->name === 'super-admin') { 56 | throw new \Exception( 57 | 'Error|Super admin role can\'t be updated--404', 58 | 13333 59 | ); 60 | } 61 | 62 | foreach ($validated as $key => $val) { 63 | $role->{$key} = $val; 64 | } 65 | $role->save(); 66 | 67 | if (isset($request->permissions) && is_array($request->permissions)) { 68 | $role->permissions()->sync($request->permissions); 69 | } 70 | return response()->json($role); 71 | } catch (\Exception $error) { 72 | return $this->errorResponse($error); 73 | } 74 | } 75 | 76 | public function destroy(Request $request) 77 | { 78 | try { 79 | $role = Role::find($request->roleId); 80 | if (!$role) { 81 | throw new \Exception('Error|Role not found--404', 13333); 82 | } 83 | if ($role->name === 'super-admin') { 84 | throw new \Exception( 85 | 'Error|Super admin role can\'t be deleted--404', 86 | 13333 87 | ); 88 | } 89 | 90 | $role->delete(); 91 | return response()->json([], 204); 92 | } catch (\Exception $error) { 93 | return $this->errorResponse($error); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 19 | '%s%s', 20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 21 | Sanctum::currentApplicationUrlWithPort() 22 | ))), 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Sanctum Guards 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This array contains the authentication guards that will be checked when 30 | | Sanctum is trying to authenticate a request. If none of these guards 31 | | are able to authenticate the request, Sanctum will use the bearer 32 | | token that's present on an incoming request for authentication. 33 | | 34 | */ 35 | 36 | 'guard' => ['web'], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Expiration Minutes 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This value controls the number of minutes until an issued token will be 44 | | considered expired. This will override any values set in the token's 45 | | "expires_at" attribute, but first-party sessions are not affected. 46 | | 47 | */ 48 | 49 | 'expiration' => null, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Token Prefix 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Sanctum can prefix new tokens in order to take advantage of numerous 57 | | security scanning initiatives maintained by open source platforms 58 | | that notify developers if they commit tokens into repositories. 59 | | 60 | | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning 61 | | 62 | */ 63 | 64 | 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Sanctum Middleware 69 | |-------------------------------------------------------------------------- 70 | | 71 | | When authenticating your first-party SPA with Sanctum you may need to 72 | | customize some of the middleware Sanctum uses while processing the 73 | | request. You may change the middleware listed below as required. 74 | | 75 | */ 76 | 77 | 'middleware' => [ 78 | 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 79 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 80 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 81 | ], 82 | 83 | ]; 84 | -------------------------------------------------------------------------------- /resources/js/src/components/ui/FormInput.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 117 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $middleware = [ 17 | // \App\Http\Middleware\TrustHosts::class, 18 | \App\Http\Middleware\TrustProxies::class, 19 | \Illuminate\Http\Middleware\HandleCors::class, 20 | \App\Http\Middleware\PreventRequestsDuringMaintenance::class, 21 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 22 | \App\Http\Middleware\TrimStrings::class, 23 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 24 | ]; 25 | 26 | /** 27 | * The application's route middleware groups. 28 | * 29 | * @var array> 30 | */ 31 | protected $middlewareGroups = [ 32 | 'web' => [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 37 | \App\Http\Middleware\VerifyCsrfToken::class, 38 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 39 | ], 40 | 41 | 'api' => [ 42 | // \App\Http\Middleware\EncryptCookies::class, 43 | // \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 44 | \Illuminate\Session\Middleware\StartSession::class, 45 | // \Illuminate\View\Middleware\ShareErrorsFromSession::class, 46 | // \App\Http\Middleware\VerifyCsrfToken::class, 47 | // \Illuminate\Routing\Middleware\SubstituteBindings::class, 48 | 49 | 50 | \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 51 | 52 | 53 | \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api', 54 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 55 | ], 56 | ]; 57 | 58 | /** 59 | * The application's middleware aliases. 60 | * 61 | * Aliases may be used instead of class names to conveniently assign middleware to routes and groups. 62 | * 63 | * @var array 64 | */ 65 | protected $middlewareAliases = [ 66 | 'auth' => \App\Http\Middleware\Authenticate::class, 67 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 68 | 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 69 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 70 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 71 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 72 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 73 | 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, 74 | 'signed' => \App\Http\Middleware\ValidateSignature::class, 75 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 76 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 77 | 'permission' => \App\Http\Middleware\EnsureUserHasPermission::class 78 | ]; 79 | } 80 | -------------------------------------------------------------------------------- /app/Http/Controllers/PermissionController.php: -------------------------------------------------------------------------------- 1 | json($permissions); 37 | } 38 | 39 | public function store(Request $request) 40 | { 41 | try { 42 | $validated = $request->validate([ 43 | 'name' => ['required', 'unique:permissions,name'], 44 | ]); 45 | 46 | $permission = new Permission(); 47 | foreach ($validated as $key => $val) { 48 | $permission->{$key} = $val; 49 | } 50 | $permission->save(); 51 | 52 | $superAdminRole = Role::where('name', 'super-admin')->first(); 53 | $superAdminRole->permissions()->attach([$permission->id]); 54 | 55 | return response()->json($permission); 56 | } catch (\Exception $error) { 57 | return $this->errorResponse($error); 58 | } 59 | } 60 | 61 | public function update(Request $request) 62 | { 63 | try { 64 | $validated = $request->validate([ 65 | 'name' => [ 66 | 'required', 67 | Rule::unique('permissions')->ignore($request->permissionId), 68 | ], 69 | ]); 70 | 71 | $permission = Permission::find($request->permissionId); 72 | if (!$permission) { 73 | throw new \Exception('Error|Permission not found--404', 13333); 74 | } 75 | 76 | if (in_array($permission->name, $this->systemPermissions)) { 77 | throw new \Exception( 78 | 'Error|System permissions can\'t be edited--403', 79 | 13333 80 | ); 81 | } 82 | 83 | foreach ($validated as $key => $val) { 84 | $permission->{$key} = $val; 85 | } 86 | $permission->save(); 87 | 88 | return response()->json($permission); 89 | } catch (\Exception $error) { 90 | return $this->errorResponse($error); 91 | } 92 | } 93 | 94 | public function destroy(Request $request) 95 | { 96 | try { 97 | $permission = Permission::find($request->permissionId); 98 | if (!$permission) { 99 | throw new \Exception('Error|Permission not found--404', 13333); 100 | } 101 | 102 | if (in_array($permission->name, $this->systemPermissions)) { 103 | throw new \Exception( 104 | 'Error|System permissions can\'t be deleted--403', 105 | 13333 106 | ); 107 | } 108 | 109 | $permission->delete(); 110 | 111 | return response()->json([], 204); 112 | } catch (\Exception $error) { 113 | return $this->errorResponse($error); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /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 | | Supported drivers: "apc", "array", "database", "file", 30 | | "memcached", "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'apc' => [ 37 | 'driver' => 'apc', 38 | ], 39 | 40 | 'array' => [ 41 | 'driver' => 'array', 42 | 'serialize' => false, 43 | ], 44 | 45 | 'database' => [ 46 | 'driver' => 'database', 47 | 'table' => 'cache', 48 | 'connection' => null, 49 | 'lock_connection' => null, 50 | ], 51 | 52 | 'file' => [ 53 | 'driver' => 'file', 54 | 'path' => storage_path('framework/cache/data'), 55 | 'lock_path' => storage_path('framework/cache/data'), 56 | ], 57 | 58 | 'memcached' => [ 59 | 'driver' => 'memcached', 60 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 61 | 'sasl' => [ 62 | env('MEMCACHED_USERNAME'), 63 | env('MEMCACHED_PASSWORD'), 64 | ], 65 | 'options' => [ 66 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 67 | ], 68 | 'servers' => [ 69 | [ 70 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 71 | 'port' => env('MEMCACHED_PORT', 11211), 72 | 'weight' => 100, 73 | ], 74 | ], 75 | ], 76 | 77 | 'redis' => [ 78 | 'driver' => 'redis', 79 | 'connection' => 'cache', 80 | 'lock_connection' => 'default', 81 | ], 82 | 83 | 'dynamodb' => [ 84 | 'driver' => 'dynamodb', 85 | 'key' => env('AWS_ACCESS_KEY_ID'), 86 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 87 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 88 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 89 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 90 | ], 91 | 92 | 'octane' => [ 93 | 'driver' => 'octane', 94 | ], 95 | 96 | ], 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Cache Key Prefix 101 | |-------------------------------------------------------------------------- 102 | | 103 | | When utilizing the APC, database, memcached, Redis, or DynamoDB cache 104 | | stores there might be other applications using the same cache. For 105 | | that reason, you may prefix every cache key to avoid collisions. 106 | | 107 | */ 108 | 109 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 110 | 111 | ]; 112 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | 'after_commit' => false, 43 | ], 44 | 45 | 'beanstalkd' => [ 46 | 'driver' => 'beanstalkd', 47 | 'host' => 'localhost', 48 | 'queue' => 'default', 49 | 'retry_after' => 90, 50 | 'block_for' => 0, 51 | 'after_commit' => false, 52 | ], 53 | 54 | 'sqs' => [ 55 | 'driver' => 'sqs', 56 | 'key' => env('AWS_ACCESS_KEY_ID'), 57 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 58 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 59 | 'queue' => env('SQS_QUEUE', 'default'), 60 | 'suffix' => env('SQS_SUFFIX'), 61 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 62 | 'after_commit' => false, 63 | ], 64 | 65 | 'redis' => [ 66 | 'driver' => 'redis', 67 | 'connection' => 'default', 68 | 'queue' => env('REDIS_QUEUE', 'default'), 69 | 'retry_after' => 90, 70 | 'block_for' => null, 71 | 'after_commit' => false, 72 | ], 73 | 74 | ], 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Job Batching 79 | |-------------------------------------------------------------------------- 80 | | 81 | | The following options configure the database and table that store job 82 | | batching information. These options can be updated to any database 83 | | connection and table which has been defined by your application. 84 | | 85 | */ 86 | 87 | 'batching' => [ 88 | 'database' => env('DB_CONNECTION', 'mysql'), 89 | 'table' => 'job_batches', 90 | ], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Failed Queue Jobs 95 | |-------------------------------------------------------------------------- 96 | | 97 | | These options configure the behavior of failed queue job logging so you 98 | | can control which database and table are used to store the jobs that 99 | | have failed. You may change them to any database / table you wish. 100 | | 101 | */ 102 | 103 | 'failed' => [ 104 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 105 | 'database' => env('DB_CONNECTION', 'mysql'), 106 | 'table' => 'failed_jobs', 107 | ], 108 | 109 | ]; 110 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 18 | /** 19 | * ------------------------------------------------------------------------ 20 | * common routes 21 | * ------------------------------------------------------------------------ 22 | */ 23 | Route::get('logout', [ 24 | \App\Http\Controllers\AuthController::class, 25 | 'logout', 26 | ]); 27 | Route::get('user', function (Request $request) { 28 | return $request->user(); 29 | }); 30 | 31 | /** 32 | * ------------------------------------------------------------------------ 33 | * users routes 34 | * ------------------------------------------------------------------------ 35 | */ 36 | Route::get('users', [ 37 | \App\Http\Controllers\UserController::class, 38 | 'index', 39 | ])->middleware('permission:users-all|users-view'); 40 | 41 | Route::post('users', [ 42 | \App\Http\Controllers\UserController::class, 43 | 'store', 44 | ])->middleware('permission:users-all|users-create'); 45 | 46 | Route::patch('users/{userId}', [ 47 | \App\Http\Controllers\UserController::class, 48 | 'update', 49 | ])->middleware('permission:users-all|users-edit'); 50 | 51 | Route::delete('users/{userId}', [ 52 | \App\Http\Controllers\UserController::class, 53 | 'destroy', 54 | ])->middleware('permission:users-all|users-delete'); 55 | 56 | /** 57 | * ------------------------------------------------------------------------ 58 | * roles routes 59 | * ------------------------------------------------------------------------ 60 | */ 61 | Route::get('roles', [ 62 | \App\Http\Controllers\RoleController::class, 63 | 'index', 64 | ])->middleware('permission:roles-all|roles-view'); 65 | 66 | Route::post('roles', [ 67 | \App\Http\Controllers\RoleController::class, 68 | 'store', 69 | ])->middleware('permission:roles-all|roles-create'); 70 | 71 | Route::patch('roles/{roleId}', [ 72 | \App\Http\Controllers\RoleController::class, 73 | 'update', 74 | ])->middleware('permission:roles-all|roles-edit'); 75 | 76 | Route::delete('roles/{roleId}', [ 77 | \App\Http\Controllers\RoleController::class, 78 | 'destroy', 79 | ])->middleware('permission:roles-all|roles-delete'); 80 | 81 | /** 82 | * ------------------------------------------------------------------------ 83 | * permissions routes 84 | * ------------------------------------------------------------------------ 85 | */ 86 | Route::get('permissions', [ 87 | \App\Http\Controllers\PermissionController::class, 88 | 'index', 89 | ])->middleware('permission:permissions-all|permissions-view'); 90 | 91 | Route::post('permissions', [ 92 | \App\Http\Controllers\PermissionController::class, 93 | 'store', 94 | ])->middleware('permission:permissions-all|permissions-create'); 95 | 96 | Route::patch('permissions/{permissionId}', [ 97 | \App\Http\Controllers\PermissionController::class, 98 | 'update', 99 | ])->middleware('permission:permissions-all|permissions-edit'); 100 | 101 | Route::delete('permissions/{permissionId}', [ 102 | \App\Http\Controllers\PermissionController::class, 103 | 'destroy', 104 | ])->middleware('permission:permissions-all|permissions-delete'); 105 | }); 106 | -------------------------------------------------------------------------------- /resources/js/src/components/ui/Slider.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 101 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'smtp'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Mailer Configurations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure all of the mailers used by your application plus 24 | | their respective settings. Several examples have been configured for 25 | | you and you are free to add your own as your application requires. 26 | | 27 | | Laravel supports a variety of mail "transport" drivers to be used while 28 | | sending an e-mail. You will specify which one you are using for your 29 | | mailers below. You are free to add additional mailers as required. 30 | | 31 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 32 | | "postmark", "log", "array", "failover" 33 | | 34 | */ 35 | 36 | 'mailers' => [ 37 | 'smtp' => [ 38 | 'transport' => 'smtp', 39 | 'url' => env('MAIL_URL'), 40 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 41 | 'port' => env('MAIL_PORT', 587), 42 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 43 | 'username' => env('MAIL_USERNAME'), 44 | 'password' => env('MAIL_PASSWORD'), 45 | 'timeout' => null, 46 | 'local_domain' => env('MAIL_EHLO_DOMAIN'), 47 | ], 48 | 49 | 'ses' => [ 50 | 'transport' => 'ses', 51 | ], 52 | 53 | 'mailgun' => [ 54 | 'transport' => 'mailgun', 55 | // 'client' => [ 56 | // 'timeout' => 5, 57 | // ], 58 | ], 59 | 60 | 'postmark' => [ 61 | 'transport' => 'postmark', 62 | // 'message_stream_id' => null, 63 | // 'client' => [ 64 | // 'timeout' => 5, 65 | // ], 66 | ], 67 | 68 | 'sendmail' => [ 69 | 'transport' => 'sendmail', 70 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 71 | ], 72 | 73 | 'log' => [ 74 | 'transport' => 'log', 75 | 'channel' => env('MAIL_LOG_CHANNEL'), 76 | ], 77 | 78 | 'array' => [ 79 | 'transport' => 'array', 80 | ], 81 | 82 | 'failover' => [ 83 | 'transport' => 'failover', 84 | 'mailers' => [ 85 | 'smtp', 86 | 'log', 87 | ], 88 | ], 89 | ], 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Global "From" Address 94 | |-------------------------------------------------------------------------- 95 | | 96 | | You may wish for all e-mails sent by your application to be sent from 97 | | the same address. Here, you may specify a name and address that is 98 | | used globally for all e-mails that are sent by your application. 99 | | 100 | */ 101 | 102 | 'from' => [ 103 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 104 | 'name' => env('MAIL_FROM_NAME', 'Example'), 105 | ], 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | Markdown Mail Settings 110 | |-------------------------------------------------------------------------- 111 | | 112 | | If you are using Markdown based email rendering, you may configure your 113 | | theme and component paths here, allowing you to customize the design 114 | | of the emails. Or, you may simply stick with the Laravel defaults! 115 | | 116 | */ 117 | 118 | 'markdown' => [ 119 | 'theme' => 'default', 120 | 121 | 'paths' => [ 122 | resource_path('views/vendor/mail'), 123 | ], 124 | ], 125 | 126 | ]; 127 | -------------------------------------------------------------------------------- /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" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | 44 | 'api' => [ 45 | 'driver' => 'token', 46 | 'provider' => 'users', 47 | 'hash' => false, 48 | ], 49 | ], 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | User Providers 54 | |-------------------------------------------------------------------------- 55 | | 56 | | All authentication drivers have a user provider. This defines how the 57 | | users are actually retrieved out of your database or other storage 58 | | mechanisms used by this application to persist your user's data. 59 | | 60 | | If you have multiple user tables or models you may configure multiple 61 | | sources which represent each model / table. These sources may then 62 | | be assigned to any extra authentication guards you have defined. 63 | | 64 | | Supported: "database", "eloquent" 65 | | 66 | */ 67 | 68 | 'providers' => [ 69 | 'users' => [ 70 | 'driver' => 'eloquent', 71 | 'model' => App\Models\User::class, 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 expiry time is the number of minutes that each reset token will 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 | | The throttle setting is the number of seconds a user must wait before 94 | | generating more password reset tokens. This prevents the user from 95 | | quickly generating a very large amount of password reset tokens. 96 | | 97 | */ 98 | 99 | 'passwords' => [ 100 | 'users' => [ 101 | 'provider' => 'users', 102 | 'table' => 'password_reset_tokens', 103 | 'expire' => 60, 104 | 'throttle' => 60, 105 | ], 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Password Confirmation Timeout 111 | |-------------------------------------------------------------------------- 112 | | 113 | | Here you may define the amount of seconds before a password confirmation 114 | | times out and the user is prompted to re-enter their password via the 115 | | confirmation screen. By default, the timeout lasts for three hours. 116 | | 117 | */ 118 | 119 | 'password_timeout' => 10800, 120 | 121 | ]; 122 | -------------------------------------------------------------------------------- /resources/js/src/pages/Permissions.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About This Repository 2 | 3 | This repository uses PHP Laravel version 10, Javascript Vue.js version 3 and Tailwind css frameworks to implement Role Permissions management in CMS. Vite is used to compile the frontend assets. Cookie based SPA authorization feature of Laravel Sanctum package is used to authorize the user inside the content management system. Vue router is used to handle the routing and Pinia is used as the frontend store management system. If a logged user has permission to do specific task, the link to visit the task and the page containing the task will be available to the logged user. Otherwise the page will be unavailable to the logged user with 401 unauthorized error even though try to access the route manually. Though some actions are visible on the frontend for better user experience, but it will handled on the backend and shows a error toast with relevant error message as well as permission requirements. This repo utilizes the SPA feature of Vue.js. 4 | 5 | ## Logging In 6 | 7 | Following credentials can be used to log in the system 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
SlRoleEmail AddressPassword
01Super Adminsadmin@sadmin.compassword
02Adminadmin@admin.compassword
03Authorauthor@author.compassword
04Editoreditor@editor.compassword
05Useruser@user.compassword
06Useruser1@user.compassword
07Useruser2@user.compassword
08Useruser3@user.compassword
09Useruser4@user.compassword
91 |
92 | 93 | Initailly only Super Admin is granted all the permissions. All other role has no permissions. To add permissions to any other role, first log in as super admin, then go to the roles page. Assign necessary permissions to the role. Then come to the users page and update users roles. One user can have multiple roles. Overall user permissions will be the collection of all roles permissions combined. Super admin role is readonly(can not be edited or deleted). This role can not be applied to any other user. If a new permission is created, updated or deleted, it will be automatically applied to super admin. 94 | 95 | ## Installation 96 | 97 | First download this repository. Navigate to root of the project and then 98 | 99 |
100 |     composer install
101 |     npm install
102 | 
103 | 104 | Copy the contents of .env.example to .env file. Fill up the database credentials(DB_DATABASE, DB_USERNAME, DB_PASSWORD) according to your database. At the root of your project run the following commands on terminal sequentially. 105 | 106 |
107 |     php artisan key:generate
108 |     php artisan migrate
109 |     php artisan db:seed
110 | 
111 | 112 | This will store all the default data into the database. Then compile the assets and run development server by 113 | 114 |
115 |     npm run dev
116 | 
117 | 118 | Finally initiate your server on a new terminal and enjoy !!! 119 | 120 |
121 |     php artisan serve
122 | 
123 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Deprecations Log Channel 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This option controls the log channel that should be used to log warnings 29 | | regarding deprecated PHP and library features. This allows you to get 30 | | your application ready for upcoming major versions of dependencies. 31 | | 32 | */ 33 | 34 | 'deprecations' => [ 35 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 36 | 'trace' => false, 37 | ], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Log Channels 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Here you may configure the log channels for your application. Out of 45 | | the box, Laravel uses the Monolog PHP logging library. This gives 46 | | you a variety of powerful log handlers / formatters to utilize. 47 | | 48 | | Available Drivers: "single", "daily", "slack", "syslog", 49 | | "errorlog", "monolog", 50 | | "custom", "stack" 51 | | 52 | */ 53 | 54 | 'channels' => [ 55 | 'stack' => [ 56 | 'driver' => 'stack', 57 | 'channels' => ['single'], 58 | 'ignore_exceptions' => false, 59 | ], 60 | 61 | 'single' => [ 62 | 'driver' => 'single', 63 | 'path' => storage_path('logs/laravel.log'), 64 | 'level' => env('LOG_LEVEL', 'debug'), 65 | 'replace_placeholders' => true, 66 | ], 67 | 68 | 'daily' => [ 69 | 'driver' => 'daily', 70 | 'path' => storage_path('logs/laravel.log'), 71 | 'level' => env('LOG_LEVEL', 'debug'), 72 | 'days' => 14, 73 | 'replace_placeholders' => true, 74 | ], 75 | 76 | 'slack' => [ 77 | 'driver' => 'slack', 78 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 79 | 'username' => 'Laravel Log', 80 | 'emoji' => ':boom:', 81 | 'level' => env('LOG_LEVEL', 'critical'), 82 | 'replace_placeholders' => true, 83 | ], 84 | 85 | 'papertrail' => [ 86 | 'driver' => 'monolog', 87 | 'level' => env('LOG_LEVEL', 'debug'), 88 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 89 | 'handler_with' => [ 90 | 'host' => env('PAPERTRAIL_URL'), 91 | 'port' => env('PAPERTRAIL_PORT'), 92 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), 93 | ], 94 | 'processors' => [PsrLogMessageProcessor::class], 95 | ], 96 | 97 | 'stderr' => [ 98 | 'driver' => 'monolog', 99 | 'level' => env('LOG_LEVEL', 'debug'), 100 | 'handler' => StreamHandler::class, 101 | 'formatter' => env('LOG_STDERR_FORMATTER'), 102 | 'with' => [ 103 | 'stream' => 'php://stderr', 104 | ], 105 | 'processors' => [PsrLogMessageProcessor::class], 106 | ], 107 | 108 | 'syslog' => [ 109 | 'driver' => 'syslog', 110 | 'level' => env('LOG_LEVEL', 'debug'), 111 | 'facility' => LOG_USER, 112 | 'replace_placeholders' => true, 113 | ], 114 | 115 | 'errorlog' => [ 116 | 'driver' => 'errorlog', 117 | 'level' => env('LOG_LEVEL', 'debug'), 118 | 'replace_placeholders' => true, 119 | ], 120 | 121 | 'null' => [ 122 | 'driver' => 'monolog', 123 | 'handler' => NullHandler::class, 124 | ], 125 | 126 | 'emergency' => [ 127 | 'path' => storage_path('logs/laravel.log'), 128 | ], 129 | ], 130 | 131 | ]; 132 | -------------------------------------------------------------------------------- /resources/js/src/pages/Roles.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 115 | -------------------------------------------------------------------------------- /resources/js/src/components/page/PermissionSlider.vue: -------------------------------------------------------------------------------- 1 | 122 | 123 | 153 | -------------------------------------------------------------------------------- /resources/js/src/composables/useHttpRequest.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | import axios, { AxiosError } from 'axios'; 3 | import useModalToast from './useModalToast'; 4 | 5 | const useHttpRequest = (path = '') => { 6 | const { showToast } = useModalToast(); 7 | const loading = ref(false); 8 | const initialLoading = ref(true); 9 | const saving = ref(false); 10 | const updating = ref(false); 11 | const deleting = ref(false); 12 | 13 | const index = async (callback = null) => { 14 | try { 15 | loading.value = true; 16 | const response = await axios.get(path); 17 | loading.value = false; 18 | 19 | if (typeof callback === 'function') { 20 | callback(null, response); 21 | } 22 | initialLoading.value = false; 23 | 24 | if (response.data) { 25 | return response.data; 26 | } 27 | return []; 28 | } catch (error) { 29 | loading.value = false; 30 | return handleError(error, [], callback, false); 31 | } 32 | }; 33 | 34 | const store = async (data, callback = null) => { 35 | try { 36 | saving.value = true; 37 | const response = await axios.post(path, data); 38 | saving.value = false; 39 | 40 | if (typeof callback === 'function') { 41 | callback(null, response); 42 | } 43 | 44 | if (response.data) { 45 | return response.data; 46 | } 47 | return null; 48 | } catch (error) { 49 | saving.value = false; 50 | return handleError(error, null, callback); 51 | } 52 | }; 53 | 54 | const update = async (id, data, callback = null) => { 55 | try { 56 | updating.value = true; 57 | const response = await axios.patch(`${path}/${id}`, data); 58 | updating.value = false; 59 | 60 | if (typeof callback === 'function') { 61 | callback(null, response); 62 | } 63 | 64 | if (response.data) { 65 | return response.data; 66 | } 67 | return null; 68 | } catch (error) { 69 | updating.value = false; 70 | return handleError(error, null, callback); 71 | } 72 | }; 73 | 74 | const destroy = async (id, callback = null) => { 75 | try { 76 | deleting.value = true; 77 | const response = await axios.delete(`${path}/${id}`); 78 | deleting.value = false; 79 | 80 | if (typeof callback === 'function') { 81 | callback(null, response); 82 | } 83 | 84 | if (response.status === 204) { 85 | return true; 86 | } 87 | return false; 88 | } catch (error) { 89 | deleting.value = false; 90 | return handleError(error, false, callback); 91 | } 92 | }; 93 | 94 | const handleError = ( 95 | error, 96 | returnValue, 97 | callback, 98 | showUnauthorizedToast = true, 99 | ) => { 100 | if (error instanceof AxiosError) { 101 | const errorData = error.response.data; 102 | if ([13333, 13334, 13335].includes(errorData?.errorCode)) { 103 | showToast( 104 | `${errorData?.errorMessage}${ 105 | errorData?.errorText 106 | ? `\r\n${errorData?.errorText}` 107 | : '' 108 | }`, 109 | errorData?.errorCode === 13334 ? 'success' : 'error', 110 | ); 111 | } 112 | 113 | if ( 114 | showUnauthorizedToast && 115 | error.response.status === 401 && 116 | error.response.data?.message === 'Permission not granted.' 117 | ) { 118 | showToast( 119 | `${error.response.data?.message}${ 120 | error.response.data?.permissions?.length 121 | ? `\r\nRequired permissions: ${error.response.data?.permissions.join( 122 | ' or ', 123 | )}` 124 | : '' 125 | }`, 126 | 'error', 127 | ); 128 | } 129 | 130 | if (typeof callback === 'function') { 131 | callback(error); 132 | } 133 | } 134 | 135 | return returnValue; 136 | }; 137 | 138 | return { 139 | loading, 140 | initialLoading, 141 | saving, 142 | updating, 143 | deleting, 144 | 145 | index, 146 | store, 147 | update, 148 | destroy, 149 | }; 150 | }; 151 | 152 | export default useHttpRequest; 153 | -------------------------------------------------------------------------------- /resources/js/src/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 131 | ../store/useAuthUserStore 132 | -------------------------------------------------------------------------------- /app/Http/Controllers/UserController.php: -------------------------------------------------------------------------------- 1 | get(); 19 | $users = $users->map( 20 | fn($user) => $this->extractPermissionsFromUser($user) 21 | ); 22 | return response()->json($users); 23 | } 24 | 25 | public function store(Request $request) 26 | { 27 | try { 28 | $validated = $request->validate([ 29 | 'name' => ['required'], 30 | 'email' => ['required', 'email', 'unique:users,email'], 31 | 'password' => ['required'], 32 | ]); 33 | 34 | $validated['password'] = Hash::make($validated['password']); 35 | 36 | $user = new User(); 37 | foreach ($validated as $key => $val) { 38 | $user->{$key} = $val; 39 | } 40 | $user->save(); 41 | 42 | if (isset($request->roles) && is_array($request->roles)) { 43 | // removing super admin role if it is exists on the role list; 44 | // so there is no possibility to add super-admin role for newly created users 45 | $superAdminRole = Role::where('name', 'super-admin')->first(); 46 | $roles = array_filter( 47 | $request->roles, 48 | fn($roleId) => $roleId !== $superAdminRole->id 49 | ); 50 | 51 | $user->roles()->sync($roles); 52 | } 53 | 54 | return response()->json($user); 55 | } catch (\Exception $error) { 56 | return $this->errorResponse($error); 57 | } 58 | } 59 | 60 | public function update(Request $request) 61 | { 62 | try { 63 | $validated = $request->validate([ 64 | 'name' => ['required', 'sometimes'], 65 | 'email' => [ 66 | 'required', 67 | 'email', 68 | 'sometimes', 69 | Rule::unique('users')->ignore($request->userId), 70 | ], 71 | 'password' => ['required', 'sometimes'], 72 | ]); 73 | 74 | if (isset($validated['password'])) { 75 | $validated['password'] = Hash::make($validated['password']); 76 | } 77 | 78 | $user = User::with('roles')->find($request->userId); 79 | if (!$user) { 80 | throw new \Exception('Error|User not found--404', 13333); 81 | } 82 | 83 | $usersSuperAdminRole = $user->roles->firstWhere( 84 | 'name', 85 | 'super-admin' 86 | ); 87 | if ($usersSuperAdminRole) { 88 | throw new \Exception( 89 | 'Error|Super admin user can\'t be updated--401', 90 | 13333 91 | ); 92 | } 93 | 94 | foreach ($validated as $key => $val) { 95 | $user->{$key} = $val; 96 | } 97 | $user->save(); 98 | 99 | if (isset($request->roles) && is_array($request->roles)) { 100 | // removing super admin role if it is exists on the role list; 101 | // so there is no possibility to add super-admin role for updated users 102 | $superAdminRole = Role::where('name', 'super-admin')->first(); 103 | 104 | $roles = []; 105 | if ($superAdminRole) { 106 | $roles = array_filter( 107 | $request->roles, 108 | fn($roleId) => $roleId !== $superAdminRole->id 109 | ); 110 | } else { 111 | $roles = $request->roles; 112 | } 113 | 114 | $user->roles()->sync($roles); 115 | } 116 | 117 | return response()->json($user); 118 | } catch (\Exception $error) { 119 | return $this->errorResponse($error); 120 | } 121 | } 122 | 123 | public function destroy(Request $request) 124 | { 125 | try { 126 | $user = User::with('roles')->find($request->userId); 127 | if (!$user) { 128 | throw new \Exception('Error|User not found--404', 13333); 129 | } 130 | 131 | $superAdminRole = $user->roles->firstWhere('name', 'super-admin'); 132 | if ($superAdminRole) { 133 | throw new \Exception( 134 | 'Error|Super admin user can\'t be deleted--401', 135 | 13333 136 | ); 137 | } 138 | 139 | $user->delete(); 140 | 141 | return response()->json([], 204); 142 | } catch (\Exception $error) { 143 | return $this->errorResponse($error); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /resources/js/src/pages/Users.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 125 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'mysql'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Database Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here are each of the database connections setup for your application. 26 | | Of course, examples of configuring each database platform that is 27 | | supported by Laravel is shown below to make development simple. 28 | | 29 | | 30 | | All database work in Laravel is done through the PHP PDO facilities 31 | | so make sure you have the driver for your particular database of 32 | | choice installed on your machine before you begin development. 33 | | 34 | */ 35 | 36 | 'connections' => [ 37 | 38 | 'sqlite' => [ 39 | 'driver' => 'sqlite', 40 | 'url' => env('DATABASE_URL'), 41 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 42 | 'prefix' => '', 43 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 44 | ], 45 | 46 | 'mysql' => [ 47 | 'driver' => 'mysql', 48 | 'url' => env('DATABASE_URL'), 49 | 'host' => env('DB_HOST', '127.0.0.1'), 50 | 'port' => env('DB_PORT', '3306'), 51 | 'database' => env('DB_DATABASE', 'forge'), 52 | 'username' => env('DB_USERNAME', 'forge'), 53 | 'password' => env('DB_PASSWORD', ''), 54 | 'unix_socket' => env('DB_SOCKET', ''), 55 | 'charset' => 'utf8mb4', 56 | 'collation' => 'utf8mb4_unicode_ci', 57 | 'prefix' => '', 58 | 'prefix_indexes' => true, 59 | 'strict' => true, 60 | 'engine' => null, 61 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 62 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 63 | ]) : [], 64 | ], 65 | 66 | 'pgsql' => [ 67 | 'driver' => 'pgsql', 68 | 'url' => env('DATABASE_URL'), 69 | 'host' => env('DB_HOST', '127.0.0.1'), 70 | 'port' => env('DB_PORT', '5432'), 71 | 'database' => env('DB_DATABASE', 'forge'), 72 | 'username' => env('DB_USERNAME', 'forge'), 73 | 'password' => env('DB_PASSWORD', ''), 74 | 'charset' => 'utf8', 75 | 'prefix' => '', 76 | 'prefix_indexes' => true, 77 | 'search_path' => 'public', 78 | 'sslmode' => 'prefer', 79 | ], 80 | 81 | 'sqlsrv' => [ 82 | 'driver' => 'sqlsrv', 83 | 'url' => env('DATABASE_URL'), 84 | 'host' => env('DB_HOST', 'localhost'), 85 | 'port' => env('DB_PORT', '1433'), 86 | 'database' => env('DB_DATABASE', 'forge'), 87 | 'username' => env('DB_USERNAME', 'forge'), 88 | 'password' => env('DB_PASSWORD', ''), 89 | 'charset' => 'utf8', 90 | 'prefix' => '', 91 | 'prefix_indexes' => true, 92 | // 'encrypt' => env('DB_ENCRYPT', 'yes'), 93 | // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), 94 | ], 95 | 96 | ], 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Migration Repository Table 101 | |-------------------------------------------------------------------------- 102 | | 103 | | This table keeps track of all the migrations that have already run for 104 | | your application. Using this information, we can determine which of 105 | | the migrations on disk haven't actually been run in the database. 106 | | 107 | */ 108 | 109 | 'migrations' => 'migrations', 110 | 111 | /* 112 | |-------------------------------------------------------------------------- 113 | | Redis Databases 114 | |-------------------------------------------------------------------------- 115 | | 116 | | Redis is an open source, fast, and advanced key-value store that also 117 | | provides a richer body of commands than a typical key-value system 118 | | such as APC or Memcached. Laravel makes it easy to dig right in. 119 | | 120 | */ 121 | 122 | 'redis' => [ 123 | 124 | 'client' => env('REDIS_CLIENT', 'phpredis'), 125 | 126 | 'options' => [ 127 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 128 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 129 | ], 130 | 131 | 'default' => [ 132 | 'url' => env('REDIS_URL'), 133 | 'host' => env('REDIS_HOST', '127.0.0.1'), 134 | 'username' => env('REDIS_USERNAME'), 135 | 'password' => env('REDIS_PASSWORD'), 136 | 'port' => env('REDIS_PORT', '6379'), 137 | 'database' => env('REDIS_DB', '0'), 138 | ], 139 | 140 | 'cache' => [ 141 | 'url' => env('REDIS_URL'), 142 | 'host' => env('REDIS_HOST', '127.0.0.1'), 143 | 'username' => env('REDIS_USERNAME'), 144 | 'password' => env('REDIS_PASSWORD'), 145 | 'port' => env('REDIS_PORT', '6379'), 146 | 'database' => env('REDIS_CACHE_DB', '1'), 147 | ], 148 | 149 | ], 150 | 151 | ]; 152 | -------------------------------------------------------------------------------- /resources/js/src/components/page/ConfirmModal.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 170 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Application Environment 24 | |-------------------------------------------------------------------------- 25 | | 26 | | This value determines the "environment" your application is currently 27 | | running in. This may determine how you prefer to configure various 28 | | services the application utilizes. Set this in your ".env" file. 29 | | 30 | */ 31 | 32 | 'env' => env('APP_ENV', 'production'), 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Application Debug Mode 37 | |-------------------------------------------------------------------------- 38 | | 39 | | When your application is in debug mode, detailed error messages with 40 | | stack traces will be shown on every error that occurs within your 41 | | application. If disabled, a simple generic error page is shown. 42 | | 43 | */ 44 | 45 | 'debug' => (bool) env('APP_DEBUG', false), 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Application URL 50 | |-------------------------------------------------------------------------- 51 | | 52 | | This URL is used by the console to properly generate URLs when using 53 | | the Artisan command line tool. You should set this to the root of 54 | | your application so that it is used when running Artisan tasks. 55 | | 56 | */ 57 | 58 | 'url' => env('APP_URL', 'http://localhost'), 59 | 60 | 'asset_url' => env('ASSET_URL'), 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Application Timezone 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Here you may specify the default timezone for your application, which 68 | | will be used by the PHP date and date-time functions. We have gone 69 | | ahead and set this to a sensible default for you out of the box. 70 | | 71 | */ 72 | 73 | 'timezone' => 'UTC', 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Application Locale Configuration 78 | |-------------------------------------------------------------------------- 79 | | 80 | | The application locale determines the default locale that will be used 81 | | by the translation service provider. You are free to set this value 82 | | to any of the locales which will be supported by the application. 83 | | 84 | */ 85 | 86 | 'locale' => 'en', 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Application Fallback Locale 91 | |-------------------------------------------------------------------------- 92 | | 93 | | The fallback locale determines the locale to use when the current one 94 | | is not available. You may change the value to correspond to any of 95 | | the language folders that are provided through your application. 96 | | 97 | */ 98 | 99 | 'fallback_locale' => 'en', 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Faker Locale 104 | |-------------------------------------------------------------------------- 105 | | 106 | | This locale will be used by the Faker PHP library when generating fake 107 | | data for your database seeds. For example, this will be used to get 108 | | localized telephone numbers, street address information and more. 109 | | 110 | */ 111 | 112 | 'faker_locale' => 'en_US', 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Encryption Key 117 | |-------------------------------------------------------------------------- 118 | | 119 | | This key is used by the Illuminate encrypter service and should be set 120 | | to a random, 32 character string, otherwise these encrypted strings 121 | | will not be safe. Please do this before deploying an application! 122 | | 123 | */ 124 | 125 | 'key' => env('APP_KEY'), 126 | 127 | 'cipher' => 'AES-256-CBC', 128 | 129 | /* 130 | |-------------------------------------------------------------------------- 131 | | Maintenance Mode Driver 132 | |-------------------------------------------------------------------------- 133 | | 134 | | These configuration options determine the driver used to determine and 135 | | manage Laravel's "maintenance mode" status. The "cache" driver will 136 | | allow maintenance mode to be controlled across multiple machines. 137 | | 138 | | Supported drivers: "file", "cache" 139 | | 140 | */ 141 | 142 | 'maintenance' => [ 143 | 'driver' => 'file', 144 | // 'store' => 'redis', 145 | ], 146 | 147 | /* 148 | |-------------------------------------------------------------------------- 149 | | Autoloaded Service Providers 150 | |-------------------------------------------------------------------------- 151 | | 152 | | The service providers listed here will be automatically loaded on the 153 | | request to your application. Feel free to add your own services to 154 | | this array to grant expanded functionality to your applications. 155 | | 156 | */ 157 | 158 | 'providers' => ServiceProvider::defaultProviders()->merge([ 159 | /* 160 | * Package Service Providers... 161 | */ 162 | 163 | /* 164 | * Application Service Providers... 165 | */ 166 | App\Providers\AppServiceProvider::class, 167 | App\Providers\AuthServiceProvider::class, 168 | // App\Providers\BroadcastServiceProvider::class, 169 | App\Providers\EventServiceProvider::class, 170 | App\Providers\RouteServiceProvider::class, 171 | ])->toArray(), 172 | 173 | /* 174 | |-------------------------------------------------------------------------- 175 | | Class Aliases 176 | |-------------------------------------------------------------------------- 177 | | 178 | | This array of class aliases will be registered when this application 179 | | is started. However, feel free to register as many as you wish as 180 | | the aliases are "lazy" loaded so they don't hinder performance. 181 | | 182 | */ 183 | 184 | 'aliases' => Facade::defaultAliases()->merge([ 185 | // 'Example' => App\Facades\Example::class, 186 | ])->toArray(), 187 | 188 | ]; 189 | --------------------------------------------------------------------------------