├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── Actions │ └── Fortify │ │ ├── CreateNewUser.php │ │ ├── LoginResponse.php │ │ ├── PasswordValidationRules.php │ │ ├── RegisterResponse.php │ │ ├── ResetUserPassword.php │ │ ├── UpdateUserPassword.php │ │ └── UpdateUserProfileInformation.php ├── Exceptions │ ├── Handler.php │ └── UserAccountBannedException.php ├── Http │ ├── Controllers │ │ ├── Admin │ │ │ ├── RoleController.php │ │ │ └── UserController.php │ │ ├── Auth │ │ │ ├── ApiController.php │ │ │ ├── AvatarController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── ConfirmedPasswordStatusController.php │ │ │ └── UserController.php │ │ ├── CommonController.php │ │ ├── Controller.php │ │ └── SpaController.php │ ├── Middleware │ │ ├── Authenticate.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── RedirectIfBanned.php │ │ ├── RequirePassword.php │ │ └── SetLocale.php │ ├── Requests │ │ ├── UserStoreRequest.php │ │ └── UserUpdateRequest.php │ └── Resources │ │ ├── Auth.php │ │ ├── Helpers.php │ │ ├── Permission.php │ │ ├── Role.php │ │ └── User.php ├── Models │ ├── PersonalAccessToken.php │ └── User.php ├── Notifications │ └── UserAccountGenerated.php ├── Policies │ ├── RolePolicy.php │ └── UserPolicy.php └── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ └── FortifyServiceProvider.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── client ├── assets │ ├── app.scss │ └── variables.scss ├── components │ ├── AppFooter.vue │ ├── AppNavbar.vue │ ├── DarkMode.vue │ ├── LangSwitcher.vue │ ├── Logo.vue │ ├── OverlayLoader.vue │ ├── Snackbar.vue │ ├── VuetifyLogo.vue │ ├── admin │ │ └── forms │ │ │ ├── Role.vue │ │ │ └── User.vue │ ├── auth │ │ ├── ConfirmPasswordForm.vue │ │ ├── EmailVerificationNotificationRequest.vue │ │ ├── NavDropdown.vue │ │ ├── PasswordResetRequest.vue │ │ ├── UpdateAvatarForm.vue │ │ ├── UpdatePasswordForm.vue │ │ └── UpdateProfileInformationForm.vue │ ├── global │ │ ├── AppDataTable.vue │ │ ├── AppForm.vue │ │ ├── AppSelect.vue │ │ ├── AppTable.vue │ │ └── Pagination.vue │ └── mixins │ │ ├── HasForm.js │ │ └── InteractsWithDataTable.js ├── lang │ ├── en.js │ └── es.js ├── layouts │ ├── auth.vue │ ├── default.vue │ └── error.vue ├── middleware │ ├── auth.js │ ├── check-auth.js │ ├── confirm-password.js │ ├── guest.js │ ├── permission.js │ └── verified.js ├── nuxt.config.js ├── pages │ ├── about.vue │ ├── admin │ │ ├── index.vue │ │ ├── roles.vue │ │ └── users.vue │ ├── auth │ │ ├── confirm-password.vue │ │ ├── index.vue │ │ ├── login.vue │ │ ├── register.vue │ │ ├── reset-password.vue │ │ └── verify-email.vue │ ├── contact.vue │ ├── dashboard.vue │ ├── index.vue │ └── profile │ │ ├── index.vue │ │ └── show.vue ├── plugins │ ├── auth.js │ ├── axios.js │ ├── helpers.js │ ├── i18n.js │ ├── nuxtClientInit.js │ ├── req-cookies.js │ ├── sweetalert.js │ └── vform.js ├── router.js ├── static │ ├── favicon.ico │ ├── icon.png │ ├── laravel-nuxt.gif │ ├── v.png │ └── vuetify-logo.svg └── store │ ├── auth.js │ ├── commons.js │ └── index.js ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── fortify.php ├── logging.php ├── mail.php ├── permission.php ├── queue.php ├── sanctum.php ├── services.php └── session.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2014_10_12_200000_add_two_factor_columns_to_users_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ └── 2021_07_05_015918_create_permission_tables.php └── seeders │ ├── DatabaseSeeder.php │ └── RolesAndPermissionsSeeder.php ├── lang ├── en │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── es.json └── es │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── package-lock.json ├── package.json ├── phpunit.xml ├── public ├── .htaccess ├── favicon.ico ├── index.php └── robots.txt ├── resources ├── css │ └── app.css ├── js │ ├── app.js │ └── bootstrap.js └── views │ └── welcome.blade.php ├── routes ├── api.php ├── auth.api.php ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ ├── .gitignore │ │ └── default-avatar.png ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore └── tests ├── Feature ├── Admin │ ├── RoleControllerTest.php │ └── UserControllerTest.php ├── ApiTest.php ├── AuthTest.php ├── AvatarTest.php ├── CommonControllerTest.php ├── EmailVerificationTest.php ├── PasswordConfirmationTest.php ├── PasswordResetTest.php ├── ProfileInformationTest.php ├── SetLocaleTest.php └── UpdatePasswordTest.php ├── TestCase.php └── Unit └── ExampleTest.php /.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 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Laravel Nuxt" 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost:8000 7 | SPA_URL=http://localhost:3000 8 | 9 | MULTI_LANG=false 10 | APP_LOCALE=en 11 | APP_FALLBACK_LOCALE=en 12 | APP_FAKER_LOCALE=en_US 13 | 14 | APP_MAINTENANCE_DRIVER=file 15 | # APP_MAINTENANCE_STORE=database 16 | 17 | PHP_CLI_SERVER_WORKERS=4 18 | 19 | BCRYPT_ROUNDS=12 20 | 21 | LOG_CHANNEL=stack 22 | LOG_STACK=single 23 | LOG_DEPRECATIONS_CHANNEL=null 24 | LOG_LEVEL=debug 25 | 26 | DB_CONNECTION=mysql 27 | DB_HOST=127.0.0.1 28 | DB_PORT=3306 29 | DB_DATABASE=laravel_nuxt 30 | DB_USERNAME=root 31 | DB_PASSWORD= 32 | 33 | SESSION_DRIVER=cookie 34 | SESSION_LIFETIME=120 35 | SESSION_ENCRYPT=false 36 | SESSION_PATH=/ 37 | SESSION_DOMAIN=localhost 38 | 39 | SANCTUM_TOKEN_EXPIRATION_TIME=null 40 | SANCTUM_STATEFUL_DOMAINS=laravel-nuxt.local,localhost,localhost:3000,localhost:8000 41 | 42 | BROADCAST_CONNECTION=log 43 | FILESYSTEM_DISK=public 44 | QUEUE_CONNECTION=database 45 | 46 | CACHE_STORE=database 47 | CACHE_PREFIX= 48 | 49 | MEMCACHED_HOST=127.0.0.1 50 | 51 | REDIS_CLIENT=phpredis 52 | REDIS_HOST=127.0.0.1 53 | REDIS_PASSWORD=null 54 | REDIS_PORT=6379 55 | 56 | MAIL_MAILER=log 57 | MAIL_SCHEME=null 58 | MAIL_HOST=127.0.0.1 59 | MAIL_PORT=2525 60 | MAIL_USERNAME=null 61 | MAIL_PASSWORD=null 62 | MAIL_FROM_ADDRESS="hello@example.com" 63 | MAIL_FROM_NAME="${APP_NAME}" 64 | 65 | AWS_ACCESS_KEY_ID= 66 | AWS_SECRET_ACCESS_KEY= 67 | AWS_DEFAULT_REGION=us-east-1 68 | AWS_BUCKET= 69 | AWS_USE_PATH_STYLE_ENDPOINT=false 70 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: '@babel/eslint-parser', 9 | requireConfigFile: false, 10 | sourceType: 'module', 11 | }, 12 | extends: [ 13 | '@nuxtjs', 14 | 'plugin:nuxt/recommended', 15 | ], 16 | plugins: [ 17 | ], 18 | // add your custom rules here 19 | rules: { 20 | 'comma-dangle': ['error', 'only-multiline'], 21 | 'template-curly-spacing': 'off', 22 | indent: ['error', 4], 23 | 24 | // Vue rules 25 | 'vue/html-indent': ['error', 4, { baseIndent: 1 }], 26 | 'vue/script-indent': ['error', 4, { baseIndent: 1 }], 27 | 'vue/html-closing-bracket-newline': ['error', { 28 | singleline: 'never', 29 | multiline: 'never', 30 | }], 31 | 'vue/max-attributes-per-line': 'off', 32 | 'vue/singleline-html-element-content-newline': ['error', { 33 | ignores: ['v-icon'], 34 | }], 35 | 'vue/multi-word-component-names': 'off', 36 | }, 37 | overrides: [ 38 | { 39 | files: ['*.vue'], 40 | rules: { 41 | 'indent': 'off' 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | * text=auto eol=lf 13 | 14 | *.blade.php diff=html 15 | *.css diff=css 16 | *.html diff=html 17 | *.md diff=markdown 18 | *.php diff=php 19 | 20 | /.github export-ignore 21 | CHANGELOG.md export-ignore 22 | .styleci.yml export-ignore 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/pail 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpactor.json 13 | .phpunit.result.cache 14 | docker-compose.override.yml 15 | Homestead.json 16 | Homestead.yaml 17 | auth.json 18 | npm-debug.log 19 | yarn-error.log 20 | /.fleet 21 | /.idea 22 | /.nova 23 | /.vscode 24 | /.zed 25 | 26 | 27 | # Logs 28 | /logs 29 | *.log 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | # Runtime data 35 | pids 36 | *.pid 37 | *.seed 38 | *.pid.lock 39 | 40 | # Directory for instrumented libs generated by jscoverage/JSCover 41 | lib-cov 42 | 43 | # Coverage directory used by tools like istanbul 44 | coverage 45 | 46 | # nyc test coverage 47 | .nyc_output 48 | 49 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 50 | .grunt 51 | 52 | # Bower dependency directory (https://bower.io/) 53 | bower_components 54 | 55 | # node-waf configuration 56 | .lock-wscript 57 | 58 | # Compiled binary addons (https://nodejs.org/api/addons.html) 59 | build/Release 60 | 61 | # Dependency directories 62 | jspm_packages/ 63 | 64 | # TypeScript v1 declaration files 65 | typings/ 66 | 67 | # Optional npm cache directory 68 | .npm 69 | 70 | # Optional eslint cache 71 | .eslintcache 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # next.js build output 86 | .next 87 | 88 | # nuxt.js build output 89 | .nuxt 90 | 91 | # Nuxt generate 92 | dist 93 | /public/_nuxt 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless 100 | 101 | # IDE / Editor 102 | .idea 103 | 104 | # Service worker 105 | sw.* 106 | 107 | # macOS 108 | .DS_Store 109 | 110 | # Vim swap files 111 | *.swp 112 | -------------------------------------------------------------------------------- /app/Actions/Fortify/CreateNewUser.php: -------------------------------------------------------------------------------- 1 | $input 19 | */ 20 | public function create(array $input): User 21 | { 22 | Validator::make($input, [ 23 | 'name' => ['required', 'string', 'max:255'], 24 | 'email' => [ 25 | 'required', 26 | 'string', 27 | 'email', 28 | 'max:255', 29 | Rule::unique(User::class), 30 | ], 31 | 'password' => $this->passwordRules(), 32 | ])->validate(); 33 | 34 | return User::create([ 35 | 'name' => $input['name'], 36 | 'email' => $input['email'], 37 | 'password' => Hash::make($input['password']), 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Actions/Fortify/LoginResponse.php: -------------------------------------------------------------------------------- 1 | expectsJson() 13 | ? new Auth($request->user()) 14 | : redirect(config('fortify.home')); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Actions/Fortify/PasswordValidationRules.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected function passwordRules(): array 15 | { 16 | return ['required', 'string', new Password, 'confirmed']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Actions/Fortify/RegisterResponse.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 13 | if ($request->routeIs('api.register')) { 14 | $token = $request->user()->createToken('API Token'); 15 | 16 | return response()->json([ 17 | 'token' => $token->plainTextToken, 18 | 'expires' => $token->accessToken->expiresIn, 19 | ], 201); 20 | } 21 | 22 | return response()->json(new Auth($request->user()), 201); 23 | } 24 | 25 | return redirect(config('fortify.home')); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Actions/Fortify/ResetUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function reset(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'password' => $this->passwordRules(), 23 | ])->validate(); 24 | 25 | $user->forceFill([ 26 | 'password' => Hash::make($input['password']), 27 | ])->save(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function update(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'current_password' => ['required', 'string'], 23 | 'password' => $this->passwordRules(), 24 | ])->after(function ($validator) use ($user, $input) { 25 | if (! isset($input['current_password']) || ! Hash::check($input['current_password'], $user->password)) { 26 | $validator->errors()->add('current_password', __('The provided password does not match your current password.')); 27 | } 28 | })->validateWithBag('updatePassword'); 29 | 30 | $user->forceFill([ 31 | 'password' => Hash::make($input['password']), 32 | ])->save(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserProfileInformation.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function update(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'name' => ['required', 'string', 'max:255'], 23 | 24 | 'email' => [ 25 | 'required', 26 | 'string', 27 | 'email', 28 | 'max:255', 29 | Rule::unique('users')->ignore($user->id), 30 | ], 31 | ])->validateWithBag('updateProfileInformation'); 32 | 33 | $data = [ 34 | 'name' => $input['name'], 35 | 'email' => $input['email'], 36 | ]; 37 | 38 | if ($data['email'] !== $user->email && $user instanceof MustVerifyEmail) { 39 | $this->updateVerifiedUser($user, $data); 40 | } else { 41 | $user->update($data); 42 | } 43 | } 44 | 45 | /** 46 | * Update the given verified user's profile information. 47 | * 48 | * @param array $input 49 | */ 50 | protected function updateVerifiedUser(User $user, array $input): void 51 | { 52 | $user->forceFill(array_merge($input, [ 53 | 'email_verified_at' => null, 54 | ]))->save(); 55 | 56 | $user->sendEmailVerificationNotification(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | , \Psr\Log\LogLevel::*> 14 | */ 15 | protected $levels = [ 16 | // 17 | ]; 18 | 19 | /** 20 | * A list of the exception types that are not reported. 21 | * 22 | * @var array> 23 | */ 24 | protected $dontReport = [ 25 | // 26 | ]; 27 | 28 | /** 29 | * A list of the inputs that are never flashed for validation exceptions. 30 | * 31 | * @var array 32 | */ 33 | protected $dontFlash = [ 34 | 'current_password', 35 | 'password', 36 | 'password_confirmation', 37 | ]; 38 | 39 | /** 40 | * Register the exception handling callbacks for the application. 41 | */ 42 | public function register(): void 43 | { 44 | $this->reportable(function (Throwable $e) { 45 | // 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Exceptions/UserAccountBannedException.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return response()->json(['status' => __('Your account is currently banned!')], 403); 19 | } 20 | 21 | return redirect(env('SPA_URL') . '/a/login', 403); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Controllers/Admin/RoleController.php: -------------------------------------------------------------------------------- 1 | authorize('index', Role::class); 20 | 21 | $search = $request->input('s'); 22 | 23 | return RoleResource::collection( 24 | Role::with('permissions:id,module,description') 25 | ->when(!empty($search), function ($query) use ($search) { 26 | $query->where('name', 'like', "%{$search}%"); 27 | }) 28 | ->orderBy('name') 29 | ->paginate(10) 30 | ); 31 | } 32 | 33 | /** 34 | * Store a newly created resource in storage. 35 | * 36 | * @return \Illuminate\Http\Response 37 | */ 38 | public function store(Request $request) 39 | { 40 | $request->validate([ 41 | 'name' => 'required|unique:roles', 42 | 'permissions' => 'required|array|min:1|exists:permissions,id' 43 | ]); 44 | 45 | $this->authorize('store', Role::class); 46 | 47 | $role = Role::create([ 48 | 'name' => $request->input('name'), 49 | 'guard_name' => 'web', 50 | ]); 51 | 52 | $role->givePermissionTo($request->only('permissions')); 53 | 54 | return new RoleResource($role->load('permissions:id,description')); 55 | } 56 | 57 | /** 58 | * Update the specified resource in storage. 59 | * 60 | * @return \Illuminate\Http\Response 61 | */ 62 | public function update(Request $request, Role $role) 63 | { 64 | $request->validate([ 65 | 'name' => 'required|unique:roles,name,' . $role->id, 66 | 'permissions' => 'required|array|min:1|exists:permissions,id' 67 | ]); 68 | 69 | $this->authorize('update', $role); 70 | 71 | $role->update($request->only('name')); 72 | 73 | $role->syncPermissions($request->only('permissions')); 74 | 75 | return new RoleResource($role->load('permissions:id,description')); 76 | } 77 | 78 | /** 79 | * Remove the specified resource from storage. 80 | * 81 | * @return \Illuminate\Http\Response 82 | */ 83 | public function destroy(Role $role) 84 | { 85 | $this->authorize('delete', $role); 86 | 87 | $role->delete(); 88 | 89 | return response()->json(['status' => 'OK']); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/Http/Controllers/Admin/UserController.php: -------------------------------------------------------------------------------- 1 | authorize('index', User::class); 23 | 24 | $search = $request->input('s'); 25 | 26 | return UserResource::collection( 27 | User::with('roles:id,name') 28 | ->orderByDesc('created_at') 29 | ->when(!empty($search), function ($query) use ($search) { 30 | $query->where( 31 | fn ($q) => 32 | $q->where('name', 'like', "%{$search}%") 33 | ->orWhere('email', 'like', "%{$search}%") 34 | ); 35 | }) 36 | ->when($request->filled('status'), function ($query) use ($request) { 37 | $query->where('active', (bool) json_decode($request->input('status'))); 38 | }) 39 | ->paginate(10) 40 | ); 41 | } 42 | 43 | /** 44 | * Store a newly created resource in storage. 45 | * 46 | * @return \Illuminate\Http\Response 47 | */ 48 | public function store(UserStoreRequest $request) 49 | { 50 | $this->authorize('store', User::class); 51 | 52 | $user = User::create($data = $request->validated()); 53 | 54 | if (!empty($data['role_id'])) { 55 | $user->assignRole($data['role_id']); 56 | } 57 | 58 | $user->notify(new UserAccountGenerated($data['plain_password'])); 59 | 60 | return new UserResource($user->load('roles:id,name')); 61 | } 62 | 63 | /** 64 | * Update the specified resource in storage. 65 | * 66 | * @return \Illuminate\Http\Response 67 | */ 68 | public function update(UserUpdateRequest $request, User $user) 69 | { 70 | $this->authorize('update', $user); 71 | 72 | $user->update($data = $request->validated()); 73 | 74 | if (array_key_exists('role_id', $data)) { 75 | $user->syncRoles($data['role_id']); 76 | } 77 | 78 | return new UserResource($user->load('roles:id,name')); 79 | } 80 | 81 | /** 82 | * Remove the specified resource from storage. 83 | * 84 | * @return \Illuminate\Http\Response 85 | */ 86 | public function destroy(User $user) 87 | { 88 | $this->authorize('delete', $user); 89 | 90 | $user->delete(); 91 | 92 | return response()->json(['status' => 'OK']); 93 | } 94 | 95 | /** 96 | * Toggle active field of the specified resource from storage. 97 | * 98 | * @return \Illuminate\Http\Response 99 | */ 100 | public function toggle(User $user) 101 | { 102 | $this->authorize('toggle', $user); 103 | 104 | $user->update([ 105 | 'active' => !$user->active 106 | ]); 107 | 108 | return response()->json(['active' => $user->active]); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ApiController.php: -------------------------------------------------------------------------------- 1 | validate([ 18 | 'email' => 'required|email', 19 | 'password' => 'required', 20 | 'device_name' => 'nullable', 21 | ]); 22 | 23 | $user = User::where('email', $request->email)->first(); 24 | 25 | if (!$user || !Hash::check($request->password, $user->password)) { 26 | throw ValidationException::withMessages([ 27 | 'email' => [trans('auth.failed')], 28 | ]); 29 | } 30 | 31 | if (!$user->active) { 32 | throw new UserAccountBannedException; 33 | } 34 | 35 | $tokenName = $request->device_name ?? 'API Token'; 36 | 37 | if ($token = $user->tokens()->where('name', $tokenName)->first()) { 38 | $token->delete(); 39 | } 40 | 41 | return response()->json($user->generateToken($tokenName)); 42 | } 43 | 44 | public function logout(Request $request) 45 | { 46 | $request->user()->currentAccessToken()->delete(); 47 | 48 | return response()->json(['status' => 'OK']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AvatarController.php: -------------------------------------------------------------------------------- 1 | validate([ 20 | 'avatar' => 'required|image|max:2048', 21 | ]); 22 | 23 | $this->deleteOldAvatar($user = $request->user()); 24 | 25 | $user->update([ 26 | 'avatar' => $request->file('avatar')->store('avatars') 27 | ]); 28 | 29 | return response()->json(['photo_url' => $user->photo_url]); 30 | } 31 | 32 | /** 33 | * Restore the user avatar to default value. 34 | * 35 | * @return \Illuminate\Http\JsonResponse 36 | */ 37 | public function destroy(Request $request) 38 | { 39 | $this->deleteOldAvatar($user = $request->user()); 40 | 41 | $user->update([ 42 | 'avatar' => User::DEFAULT_AVATAR_PATH 43 | ]); 44 | 45 | return response()->json(['photo_url' => $user->photo_url]); 46 | } 47 | 48 | /** 49 | * Delete from storage the current user avatar if it isn't the default one. 50 | */ 51 | protected function deleteOldAvatar(User $user): void 52 | { 53 | if ($user->avatar !== User::DEFAULT_AVATAR_PATH) { 54 | Storage::delete($user->avatar); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | user()->currentAccessToken(); 22 | 23 | if (!$token) { 24 | return parent::store($request); 25 | } 26 | 27 | $confirmed = app(ConfirmPassword::class)( 28 | $this->guard, 29 | $request->user(), 30 | $request->input('password') 31 | ); 32 | 33 | if ($confirmed) { 34 | Cache::put("auth.password_confirmed_at.$token->token", time(), config('auth.password_timeout', 900)); 35 | } 36 | 37 | return $confirmed 38 | ? app(PasswordConfirmedResponse::class) 39 | : app(FailedPasswordConfirmationResponse::class); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmedPasswordStatusController.php: -------------------------------------------------------------------------------- 1 | user()->currentAccessToken(); 18 | 19 | if (!$token) { 20 | return parent::show($request); 21 | } 22 | 23 | return response()->json([ 24 | 'confirmed' => (time() - cache("auth.password_confirmed_at.$token->token", 0)) < $request->input('seconds', config('auth.password_timeout', 900)), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/UserController.php: -------------------------------------------------------------------------------- 1 | user()->loadMissing('roles.permissions')); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Http/Controllers/CommonController.php: -------------------------------------------------------------------------------- 1 | authorize('assignRoles', User::class); 22 | 23 | return RoleResource::collection( 24 | Role::select(['id', 'name'])->orderBy('name')->paginate() 25 | ); 26 | } 27 | 28 | /** 29 | * Display a listing of all permissions to be used in roles permissions assignment. 30 | * 31 | * @return \Illuminate\Http\Response 32 | */ 33 | public function permissions(Request $request) 34 | { 35 | $this->authorize('index', Role::class); 36 | 37 | return PermissionResource::collection( 38 | Permission::select(['id', 'module', 'description'])->orderBy('name')->get() 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | expectsJson() ? null : route('login'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 24 | if ($request->expectsJson()) { 25 | return response()->json(['error' => 'Already authenticated.'], 200); 26 | } 27 | 28 | return redirect(RouteServiceProvider::HOME); 29 | } 30 | } 31 | 32 | return $next($request); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfBanned.php: -------------------------------------------------------------------------------- 1 | user(); 28 | 29 | if (Auth::guard($guard)->check() && !$user->active) { 30 | // Invalidate Session 31 | if ($request->hasSession()) { 32 | Auth::guard($guard)->logout(); 33 | 34 | $request->session()->invalidate(); 35 | 36 | $request->session()->regenerateToken(); 37 | } 38 | 39 | // Invalidate Access Token 40 | if ($request->bearerToken()) { 41 | $user->currentAccessToken()->delete(); 42 | } 43 | 44 | throw new UserAccountBannedException; 45 | } 46 | } 47 | 48 | return $response; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Http/Middleware/RequirePassword.php: -------------------------------------------------------------------------------- 1 | user()->currentAccessToken(); 20 | 21 | if (!$token) { 22 | return parent::shouldConfirmPassword($request); 23 | } 24 | 25 | $confirmedAt = time() - cache("auth.password_confirmed_at.$token->token", 0); 26 | 27 | return $confirmedAt > $this->passwordTimeout; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Middleware/SetLocale.php: -------------------------------------------------------------------------------- 1 | server('HTTP_ACCEPT_LANGUAGE'), 0, 2); 22 | 23 | if (array_key_exists($locale, $locales)) { 24 | App::setLocale($locale); 25 | } 26 | 27 | return $next($request); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Requests/UserStoreRequest.php: -------------------------------------------------------------------------------- 1 | |string> 42 | */ 43 | public function rules(): array 44 | { 45 | $rules = [ 46 | 'name' => 'required|string|max:255', 47 | 'email' => 'required|string|email|max:255|unique:users,email', 48 | ]; 49 | 50 | if ($this->user()->can('users.assign-role')) { 51 | $rules['role_id'] = ['nullable', Rule::exists(Role::class, 'id')]; 52 | } 53 | 54 | return $rules; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Http/Requests/UserUpdateRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | $rules = [ 27 | 'name' => 'required|string|max:255', 28 | 'email' => 'required|string|email|max:255|unique:users,email,' . $this->route('user')->id, 29 | ]; 30 | 31 | if ($this->user()->can('users.assign-role')) { 32 | $rules['role_id'] = ['nullable', Rule::exists(Role::class, 'id')]; 33 | } 34 | 35 | return $rules; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Resources/Auth.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function toArray(Request $request): array 17 | { 18 | return [ 19 | 'id' => (int) $this->id, 20 | 'name' => $this->name, 21 | 'email' => $this->email, 22 | 'photo_url' => $this->photo_url, 23 | 24 | 'email_verified_at' => $this->when($this->resource instanceof MustVerifyEmail, $this->email_verified_at), 25 | 26 | $this->mergeWhen($this->relationLoaded('roles'), fn () => [ 27 | 'roles' => $this->getRoleNames(), 28 | 'permissions' => $this->getAllPermissions()->pluck('name'), 29 | ]), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Resources/Helpers.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | return [ 20 | 'id' => (int) $this->id, 21 | 'name' => $this->whenNotNull($this->name), 22 | 'module' => $this->whenNotNull($this->module), 23 | 'description' => $this->whenNotNull($this->description), 24 | 'roles' => Role::collection($this->whenLoaded('roles')), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Resources/Role.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => (int) $this->id, 19 | 'name' => $this->name, 20 | 'permissions' => Permission::collection($this->whenLoaded('permissions')), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Resources/User.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => (int) $this->id, 19 | 'name' => $this->name, 20 | 'email' => $this->email, 21 | 'active' => $this->active, 22 | 'roles' => Role::collection($this->whenLoaded('roles')), 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Models/PersonalAccessToken.php: -------------------------------------------------------------------------------- 1 | remember("lastUsageUpdate.{$token->cacheKey}", 3600, function () use ($token) { 23 | $token->updateLastUsageUpdate($token->getDirty()); 24 | return now(); 25 | }); 26 | 27 | return false; 28 | }); 29 | 30 | static::created(function (self $token) { 31 | if (request()->filled('remember')) { 32 | static::cache()->forever("long-lived.{$token->cacheKey}", $token); 33 | } 34 | }); 35 | 36 | static::deleted(function (self $token) { 37 | static::cache()->forget($token->cacheKey); 38 | 39 | if ($token->isLongLived) { 40 | static::cache()->forget("long-lived.{$token->cacheKey}"); 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * Find the token instance matching the given token. 47 | * 48 | * @param string $token 49 | * @return static|null 50 | */ 51 | public static function findToken($token) 52 | { 53 | $token = static::cache()->remember($token, config('sanctum.expiration'), function () use ($token) { 54 | return parent::findToken($token) ?? '_null_'; 55 | }); 56 | 57 | if ($token === '_null_') { 58 | return null; 59 | } 60 | 61 | return $token; 62 | } 63 | 64 | /** 65 | * Determine if the token is long-lived. 66 | */ 67 | public function getIsLongLivedAttribute(): bool 68 | { 69 | return static::cache()->has("long-lived.{$this->cacheKey}"); 70 | } 71 | 72 | /** 73 | * Get the expiration date of the token. 74 | */ 75 | public function getExpirationDateAttribute(): Carbon 76 | { 77 | return $this->created_at->addMinutes( 78 | $this->isLongLived ? (60 * 24 * 365) : config('sanctum.expiration') 79 | ); 80 | } 81 | 82 | /** 83 | * Get the seconds until the token expires. 84 | */ 85 | public function getExpiresInAttribute(): int 86 | { 87 | return $this->created_at->diffInSeconds($this->expires_at); 88 | } 89 | 90 | /** 91 | * Update the last usage update manually. 92 | */ 93 | public function updateLastUsageUpdate(array $attrs): void 94 | { 95 | try { 96 | DB::table($this->getTable()) 97 | ->where($this->getKeyName(), $this->getKey()) 98 | ->update($attrs); 99 | } catch (\Exception $e) { 100 | logger()->critical($e->getMessage()); 101 | } 102 | } 103 | 104 | /** 105 | * Get the key to use for caching. 106 | */ 107 | protected function getCacheKeyAttribute(): string 108 | { 109 | return $this->getKey(); 110 | } 111 | 112 | /** 113 | * Get the tagged cache instance. 114 | */ 115 | protected static function cache(): TaggedCache 116 | { 117 | return Cache::tags('personal_access_tokens'); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | active = $model->active ?? true; 29 | $model->avatar = $model->avatar ?? static::DEFAULT_AVATAR_PATH; 30 | }); 31 | } 32 | 33 | /** 34 | * The attributes that are mass assignable. 35 | * 36 | * @var array 37 | */ 38 | protected $fillable = [ 39 | 'name', 40 | 'email', 41 | 'avatar', 42 | 'active', 43 | 'password', 44 | ]; 45 | 46 | /** 47 | * The attributes that should be hidden for arrays. 48 | * 49 | * @var array 50 | */ 51 | protected $hidden = [ 52 | 'password', 53 | 'remember_token', 54 | ]; 55 | 56 | /** 57 | * The attributes that should be cast to native types. 58 | * 59 | * @var array 60 | */ 61 | protected $casts = [ 62 | 'active' => 'boolean', 63 | 'email_verified_at' => 'datetime', 64 | ]; 65 | 66 | public function getPhotoUrlAttribute(): ?string 67 | { 68 | return $this->avatar ? Storage::url($this->avatar) : null; 69 | } 70 | 71 | public function generateToken(?string $name = null): array 72 | { 73 | $tokenName = $name ?? 'API Token'; 74 | 75 | if ($token = $this->tokens()->where('name', $tokenName)->first()) { 76 | $token->delete(); 77 | } 78 | 79 | $expires = now(); 80 | $expirationTime = request()->filled('remember') ? '30 day' : config('sanctum.expiration'). 'minutes'; 81 | $token = $this->createToken($tokenName, expiresAt: $expires->add($expirationTime)); 82 | 83 | return [ 84 | 'token' => $token->plainTextToken, 85 | 'expires' => $token->accessToken->expiresIn, 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/Notifications/UserAccountGenerated.php: -------------------------------------------------------------------------------- 1 | password = $password; 23 | $this->actionUrl = env('SPA_URL') . '/a/login'; 24 | } 25 | 26 | /** 27 | * Get the notification's delivery channels. 28 | * 29 | * @return array 30 | */ 31 | public function via(object $notifiable): array 32 | { 33 | return ['mail']; 34 | } 35 | 36 | /** 37 | * Get the mail representation of the notification. 38 | */ 39 | public function toMail(object $notifiable): MailMessage 40 | { 41 | $appName = env('APP_NAME'); 42 | 43 | return (new MailMessage) 44 | ->subject(trans('Welcome to :appName', ['appName' => $appName])) 45 | ->greeting(trans('Hello :name!', ['name' => $notifiable->name])) 46 | ->line(trans('Welcome to :appName, your account is ready!', ['appName' => $appName])) 47 | ->line(trans('Your password is: :password', ['password' => $this->password])) 48 | ->action(trans('Login'), $this->actionUrl) 49 | ->salutation(env('APP_NAME')); 50 | } 51 | 52 | /** 53 | * Get the array representation of the notification. 54 | * 55 | * @return array 56 | */ 57 | public function toArray(object $notifiable): array 58 | { 59 | return [ 60 | // 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Policies/RolePolicy.php: -------------------------------------------------------------------------------- 1 | hasPermissionTo('roles.index') ? true : null; 16 | } 17 | 18 | public function store(User $user): ?bool 19 | { 20 | return $user->hasPermissionTo('roles.store') ? true : null; 21 | } 22 | 23 | public function update(User $user, Role $role): ?bool 24 | { 25 | if (in_array($role->name, ['Super Admin'])) { 26 | return false; 27 | } 28 | 29 | return $user->hasPermissionTo('roles.update') ? true : null; 30 | } 31 | 32 | public function delete(User $user, Role $role): ?bool 33 | { 34 | if (in_array($role->name, ['Super Admin'])) { 35 | return false; 36 | } 37 | 38 | return $user->hasPermissionTo('roles.delete') ? true : null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Policies/UserPolicy.php: -------------------------------------------------------------------------------- 1 | hasPermissionTo('users.index') ? true : null; 15 | } 16 | 17 | public function store(User $user): ?bool 18 | { 19 | return $user->hasPermissionTo('users.store') ? true : null; 20 | } 21 | 22 | public function update(User $user, User $model): ?bool 23 | { 24 | if ($model->hasRole('Super Admin')) { 25 | return false; 26 | } 27 | 28 | return $user->hasPermissionTo('users.update') ? true : null; 29 | } 30 | 31 | public function delete(User $user, User $model): ?bool 32 | { 33 | if ($model->hasRole('Super Admin')) { 34 | return false; 35 | } 36 | 37 | return $user->hasPermissionTo('users.delete') ? true : null; 38 | } 39 | 40 | public function toggle(User $user, User $model): ?bool 41 | { 42 | if ($model->hasRole('Super Admin')) { 43 | return false; 44 | } 45 | 46 | return $user->hasPermissionTo('users.toggle') ? true : null; 47 | } 48 | 49 | public function assignRoles(User $user): ?bool 50 | { 51 | return $user->hasPermissionTo('users.assign-role') ? true : null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | by($request->user()?->id ?: $request->ip()); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\RolePolicy', 25 | ]; 26 | 27 | /** 28 | * Register any authentication / authorization services. 29 | */ 30 | public function boot(): void 31 | { 32 | Gate::after(function ($user, $ability, $result, $arguments) { 33 | return $user->hasRole('Super Admin'); 34 | }); 35 | 36 | ResetPassword::createUrlUsing(function ($user, string $token) { 37 | return env('SPA_URL') . "/a/reset-password/{$token}?email={$user->email}"; 38 | }); 39 | 40 | VerifyEmail::createUrlUsing(function ($notifiable) { 41 | $appUrl = env('SPA_URL', config('app.url')); 42 | 43 | $verifyUrl = URL::temporarySignedRoute( 44 | 'api.verification.verify', 45 | Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), 46 | [ 47 | 'id' => $notifiable->getKey(), 48 | 'hash' => sha1($notifiable->getEmailForVerification()), 49 | ] 50 | ); 51 | 52 | return $appUrl . '/a/email/verify?verify_url=' . urlencode($verifyUrl); 53 | }); 54 | 55 | Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); 56 | Sanctum::authenticateAccessTokensUsing(function ($accessToken, $isValid) { 57 | if ($accessToken->isLongLived) { 58 | $isValid = now()->lt($accessToken->expirationDate); 59 | } 60 | 61 | if (!$isValid) { 62 | $accessToken->delete(); 63 | } 64 | 65 | return $isValid; 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/Providers/FortifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(LoginResponseContract::class, LoginResponse::class); 27 | $this->app->singleton(RegisterResponseContract::class, RegisterResponse::class); 28 | } 29 | 30 | /** 31 | * Bootstrap any application services. 32 | */ 33 | public function boot(): void 34 | { 35 | Fortify::createUsersUsing(CreateNewUser::class); 36 | Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class); 37 | Fortify::updateUserPasswordsUsing(UpdateUserPassword::class); 38 | Fortify::resetUserPasswordsUsing(ResetUserPassword::class); 39 | 40 | RateLimiter::for('login', function (Request $request) { 41 | return Limit::perMinute(5)->by($request->email.$request->ip()); 42 | }); 43 | 44 | RateLimiter::for('two-factor', function (Request $request) { 45 | return Limit::perMinute(5)->by($request->session()->get('login.id')); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 11 | api: __DIR__.'/../routes/api.php', 12 | web: __DIR__.'/../routes/web.php', 13 | commands: __DIR__.'/../routes/console.php', 14 | health: '/up', 15 | ) 16 | ->withMiddleware(function (Middleware $middleware) { 17 | $middleware->statefulApi(); 18 | 19 | $middleware->append([ 20 | SetLocale::class, 21 | RedirectIfBanned::class, 22 | ]); 23 | 24 | $middleware->alias([ 25 | 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, 26 | 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, 27 | 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, 28 | ]); 29 | }) 30 | ->withExceptions(function (Exceptions $exceptions) { 31 | // 32 | })->create(); 33 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | {{ link.icon }} 17 | 18 | {{ link.text }} 19 | 20 | 21 | 22 | 23 | {{ new Date().getFullYear() }} — {{ $config.appName }} 24 | 25 | 26 | 27 | 28 | 29 | 49 | -------------------------------------------------------------------------------- /client/components/DarkMode.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 86 | -------------------------------------------------------------------------------- /client/components/LangSwitcher.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 42 | -------------------------------------------------------------------------------- /client/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 80 | -------------------------------------------------------------------------------- /client/components/OverlayLoader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /client/components/Snackbar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | -------------------------------------------------------------------------------- /client/components/VuetifyLogo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /client/components/admin/forms/User.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 98 | -------------------------------------------------------------------------------- /client/components/auth/ConfirmPasswordForm.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 78 | -------------------------------------------------------------------------------- /client/components/auth/EmailVerificationNotificationRequest.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 72 | -------------------------------------------------------------------------------- /client/components/auth/NavDropdown.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 55 | -------------------------------------------------------------------------------- /client/components/auth/PasswordResetRequest.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 54 | -------------------------------------------------------------------------------- /client/components/auth/UpdatePasswordForm.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 67 | -------------------------------------------------------------------------------- /client/components/auth/UpdateProfileInformationForm.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 90 | -------------------------------------------------------------------------------- /client/components/global/AppSelect.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 98 | -------------------------------------------------------------------------------- /client/components/global/AppTable.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 48 | -------------------------------------------------------------------------------- /client/components/global/Pagination.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 68 | -------------------------------------------------------------------------------- /client/components/mixins/HasForm.js: -------------------------------------------------------------------------------- 1 | /* @vue/component */ 2 | export default { 3 | props: { 4 | readonly: { 5 | type: Boolean, 6 | required: false, 7 | default: () => false, 8 | }, 9 | }, 10 | 11 | data: () => ({ 12 | formHasFiles: false, 13 | }), 14 | 15 | computed: { 16 | method () { 17 | return !this.formHasFiles && this[this.formInitialValuesProp] ? 'put' : 'post' 18 | }, 19 | }, 20 | 21 | mounted () { 22 | if (this.formInitialValuesProp) { 23 | this.$watch(this.formInitialValuesProp, this.onformInitialValuesPropChange, { 24 | immediate: true 25 | }) 26 | } 27 | }, 28 | 29 | methods: { 30 | onFormSuccess (data) { 31 | this.clearForm() 32 | }, 33 | onFormCancelled () { 34 | this.clearForm() 35 | }, 36 | onformInitialValuesPropChange () { 37 | this.fillForm() 38 | }, 39 | clearForm () { 40 | this.form.clear() 41 | this.form.reset() 42 | }, 43 | fillForm () { 44 | this.clearForm() 45 | this.form.fill(this.getFormValues()) 46 | }, 47 | getFormValues () { 48 | return { ...this[this.formInitialValuesProp] } 49 | }, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /client/components/mixins/InteractsWithDataTable.js: -------------------------------------------------------------------------------- 1 | /* @vue/component */ 2 | export default { 3 | data: () => ({ 4 | items: [], 5 | pagination: {}, 6 | fetching: true, 7 | }), 8 | 9 | watchQuery: ['page', 's'], 10 | 11 | methods: { 12 | onItemCreated (data) { 13 | this.items.unshift(data) 14 | 15 | if (this.items.length > this.pagination.per_page) { 16 | if (this.pagination.current_page === this.pagination.last_page) { 17 | this.pagination.last_page++ 18 | } 19 | 20 | this.items.splice(this.items.length - 1, 1) 21 | } 22 | }, 23 | onItemUpdated ({ item, data }) { 24 | Object.assign(item, data) 25 | }, 26 | onItemDeleted ({ item, index }, message) { 27 | this.$notify(message ?? this.$t('alerts.deleted')) 28 | 29 | this.pagination.total-- 30 | this.items.splice(index, 1) 31 | 32 | /* eslint-disable camelcase */ 33 | const { current_page, last_page, per_page } = this.pagination 34 | 35 | if (current_page === last_page) { 36 | this.pagination.to-- 37 | } 38 | 39 | if (current_page < last_page && this.items.length < per_page) { 40 | this.fetchItems(this.$route.query) 41 | } else if (!this.items.length && current_page > 1) { 42 | this.updateQueryString({ ...this.$route.query, page: current_page - 1 }) 43 | } 44 | /* eslint-enable camelcase */ 45 | }, 46 | onPaginate ($event) { 47 | this.fetching = $event.status === 'start' 48 | 49 | if ($event.data?.data) { 50 | this.items = $event.data.data 51 | this.pagination = $event.data.meta 52 | } 53 | }, 54 | async updateQueryString (keys) { 55 | const query = {} 56 | 57 | keys = Array.isArray(keys) ? keys : Array(...arguments) 58 | 59 | for (const key of keys) { 60 | if (typeof key === 'object') { 61 | Object.assign(query, key) 62 | } else { 63 | query[key] = this[key] || undefined 64 | } 65 | } 66 | 67 | this.fetching = true 68 | 69 | try { 70 | await this.$router.push({ name: this.$route.name, query }) 71 | } catch (e) {} 72 | }, 73 | async fetchItems (params) { 74 | this.fetching = true 75 | 76 | try { 77 | const data = await this.$axios.$get(this.serverAction, { params }) 78 | 79 | this.items = data.data 80 | this.pagination = data.meta 81 | } catch (e) { 82 | } 83 | 84 | this.fetching = false 85 | }, 86 | }, 87 | } 88 | -------------------------------------------------------------------------------- /client/layouts/auth.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | -------------------------------------------------------------------------------- /client/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | -------------------------------------------------------------------------------- /client/layouts/error.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | -------------------------------------------------------------------------------- /client/middleware/auth.js: -------------------------------------------------------------------------------- 1 | export default ({ store, redirect, route }) => { 2 | if (!store.getters['auth/check']) { 3 | return redirect({ name: 'login', query: { redirect: route.fullPath } }) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/middleware/check-auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | export default async ({ $auth, $axios, $config, $reqCookies, store }) => { 4 | let loggedIn = false 5 | if (process.server) { 6 | loggedIn = $reqCookies.has('XSRF-TOKEN') || (!$config.isStateful && store.getters['auth/token']) 7 | } else { 8 | const loggedOut = JSON.parse(localStorage.loggedOut ?? false) 9 | loggedIn = $config.isStateful 10 | ? !loggedOut && Cookies.get('XSRF-TOKEN') 11 | : !loggedOut && store.getters['auth/token'] 12 | } 13 | 14 | if (!store.getters['auth/check'] && loggedIn) { 15 | await store.dispatch('auth/fetchUser') 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/middleware/confirm-password.js: -------------------------------------------------------------------------------- 1 | export default async ({ $auth, redirect, route }) => { 2 | const { confirmed } = await $auth.confirmedPasswordStatus() 3 | 4 | if (!confirmed) { 5 | return redirect({ name: 'password.confirm', query: { redirect: route.fullPath } }) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/middleware/guest.js: -------------------------------------------------------------------------------- 1 | export default ({ store, redirect }) => { 2 | if (store.getters['auth/check']) { 3 | return redirect({ name: 'dashboard' }) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/middleware/permission.js: -------------------------------------------------------------------------------- 1 | export default ({ $auth, redirect, route }) => { 2 | const permission = route.matched.find(r => r.name === route.name).meta.permission 3 | 4 | if (!permission) { 5 | return true 6 | } 7 | 8 | const isAllowed = Array.isArray(permission) ? $auth.hasAnyPermission(permission) : $auth.hasPermissionTo(permission) 9 | 10 | return isAllowed || redirect('/') 11 | } 12 | -------------------------------------------------------------------------------- /client/middleware/verified.js: -------------------------------------------------------------------------------- 1 | export default ({ store, redirect }) => { 2 | if (store.getters['auth/check']?.email_verified_at === null) { 3 | return redirect({ name: 'dashboard' }) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/pages/about.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /client/pages/admin/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /client/pages/admin/roles.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 73 | -------------------------------------------------------------------------------- /client/pages/auth/confirm-password.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | -------------------------------------------------------------------------------- /client/pages/auth/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /client/pages/auth/register.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 98 | -------------------------------------------------------------------------------- /client/pages/auth/reset-password.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 101 | -------------------------------------------------------------------------------- /client/pages/auth/verify-email.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 53 | -------------------------------------------------------------------------------- /client/pages/contact.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /client/pages/dashboard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | -------------------------------------------------------------------------------- /client/pages/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | -------------------------------------------------------------------------------- /client/pages/profile/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /client/plugins/axios.js: -------------------------------------------------------------------------------- 1 | export default function ({ app, $axios, $config, store, redirect }) { 2 | $axios.setBaseURL($config.apiUrl) 3 | 4 | if ($config.isStateful) { 5 | $axios.defaults.withCredentials = true 6 | } 7 | 8 | $axios.onRequest((config) => { 9 | const token = store.getters['auth/token'] 10 | if (token) { 11 | $axios.setToken(token, 'Bearer') 12 | } 13 | 14 | const locale = app.i18n.getLocaleCookie() 15 | if (locale) { 16 | $axios.setHeader('Accept-Language', locale) 17 | } 18 | 19 | if (process.server && $config.isStateful && !config.headers.common.referer) { 20 | $axios.setHeader('Referer', process.env.SPA_URL) 21 | } 22 | }) 23 | 24 | $axios.onError((error) => { 25 | const status = error.response.status 26 | const auth = store.getters['auth/check'] 27 | const currentPath = app.router.currentRoute.fullPath 28 | 29 | if (status === 401) { 30 | store.commit('auth/CLEAR') 31 | 32 | return redirect({ name: 'login', query: { redirect: currentPath } }) 33 | } 34 | 35 | if (status === 403) { 36 | app.$swal.warning({ 37 | title: app.i18n.t('alerts.unauthorized'), 38 | text: error.response.data.message, 39 | }).then(() => { 40 | redirect(auth ? { name: 'dashboard' } : '/') 41 | }) 42 | } 43 | 44 | if (status === 423) { 45 | return redirect({ name: 'password.confirm', query: { redirect: currentPath } }) 46 | } 47 | 48 | Promise.reject(error) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /client/plugins/i18n.js: -------------------------------------------------------------------------------- 1 | export default function ({ app, $vuetify }) { 2 | $vuetify.lang.t = (key, ...params) => app.i18n.t(key, params) 3 | 4 | app.i18n.onBeforeLanguageSwitch = (oldLocale, newLocale, isInitialSetup, context) => { 5 | $vuetify.lang.current = newLocale 6 | } 7 | 8 | app.i18n.onLanguageSwitched = (oldLocale, newLocale) => { 9 | // 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/plugins/nuxtClientInit.js: -------------------------------------------------------------------------------- 1 | export default async function (context) { 2 | await context.store.dispatch('nuxtClientInit', context) 3 | } 4 | -------------------------------------------------------------------------------- /client/plugins/req-cookies.js: -------------------------------------------------------------------------------- 1 | export default ({ req }, inject) => { 2 | const helpers = {} 3 | const cookies = req.headers.cookie 4 | ? req.headers.cookie.split('; ').reduce((items, item) => { 5 | const [name, value] = item.split('=') 6 | return Object.assign(items, { [`${name}`]: value }) 7 | }, {}) 8 | : {} 9 | 10 | helpers.has = function (key) { 11 | return Boolean(cookies[key]) 12 | } 13 | 14 | helpers.get = function (key) { 15 | return cookies[key] 16 | } 17 | 18 | helpers.all = function () { 19 | return cookies 20 | } 21 | 22 | inject('reqCookies', helpers) 23 | } 24 | -------------------------------------------------------------------------------- /client/plugins/sweetalert.js: -------------------------------------------------------------------------------- 1 | import SweetAlert from 'sweetalert2' 2 | 3 | export default function ({ app, $i18n, $axios, $vform }, inject) { 4 | const swal = {} 5 | 6 | const Swal = SweetAlert.mixin({ 7 | confirmButtonText: app.i18n.t('btns.ok'), 8 | cancelButtonText: app.i18n.t('btns.cancel'), 9 | willOpen: (container) => { 10 | const { currentTheme: colors, isDark } = app.vuetify.framework.theme 11 | const options = { 12 | confirmButtonColor: colors.primary, 13 | cancelButtonColor: colors.error, 14 | denyButtonColor: colors.error, 15 | } 16 | 17 | if (isDark) { 18 | options.background = '#1E1E1E' 19 | container.getElementsByClassName('swal2-title')[0].style.color = '#E0E0E0' 20 | container.getElementsByClassName('swal2-html-container')[0].style.color = '#E0E0E0' 21 | } 22 | 23 | Swal.update(options) 24 | }, 25 | }) 26 | 27 | swal.message = function (options) { 28 | return Swal.fire(options) 29 | } 30 | 31 | swal.info = function (options) { 32 | return this.message({ ...options, icon: 'info' }) 33 | } 34 | 35 | swal.success = function (options) { 36 | return this.message({ ...options, icon: 'success' }) 37 | } 38 | 39 | swal.question = function (options) { 40 | return this.message({ ...options, icon: 'question' }) 41 | } 42 | 43 | swal.error = function (options) { 44 | return this.message({ ...options, icon: 'error' }) 45 | } 46 | 47 | swal.warning = function (options) { 48 | return this.message({ ...options, icon: 'warning' }) 49 | } 50 | 51 | swal.confirm = async function ({ 52 | url, 53 | form, 54 | text, 55 | title = app.i18n.t('alerts.question'), 56 | success = app.i18n.t('alerts.done'), 57 | method = 'post', 58 | options = {} 59 | }) { 60 | try { 61 | const result = await Swal.fire({ 62 | text, 63 | title, 64 | focusCancel: true, 65 | reverseButtons: true, 66 | showCancelButton: true, 67 | showLoaderOnConfirm: Boolean(url), 68 | backdrop: () => !Swal.isLoading(), 69 | allowOutsideClick: () => !Swal.isLoading(), 70 | preConfirm: url 71 | ? async () => { 72 | try { 73 | return form instanceof $vform 74 | ? await form[method](url) 75 | : await $axios[method](url, form ?? {}) 76 | } catch (e) { 77 | Swal.showValidationMessage( 78 | form instanceof $vform 79 | ? form.errors.first() 80 | : e?.response?.data?.message || e 81 | ) 82 | } 83 | } 84 | : undefined, 85 | ...options, 86 | }) 87 | 88 | if (result.isConfirmed) { 89 | if (success) { 90 | this.success({ title: success }) 91 | } 92 | 93 | return result.value 94 | } 95 | 96 | return result 97 | } catch (e) { 98 | } 99 | } 100 | 101 | swal.delete = function (options = {}) { 102 | return this.confirm({ 103 | title: app.i18n.t('alerts.sure'), 104 | text: app.i18n.t('alerts.will_delete'), 105 | method: 'delete', 106 | ...options 107 | }) 108 | } 109 | 110 | inject('swal', swal) 111 | } 112 | -------------------------------------------------------------------------------- /client/plugins/vform.js: -------------------------------------------------------------------------------- 1 | import { Form, Errors } from 'vform' 2 | 3 | export default function ({ $axios, redirect }, inject) { 4 | Errors.prototype.first = function () { 5 | return this.flatten()[0] 6 | } 7 | 8 | Form.axios = $axios 9 | 10 | inject('vform', Form) 11 | } 12 | -------------------------------------------------------------------------------- /client/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | const page = path => () => import(`~/pages/${path}`).then(m => m.default || m) 7 | 8 | export function createRouter () { 9 | return new Router({ 10 | mode: 'history', 11 | routes: [ 12 | { 13 | path: '/a', 14 | name: 'auth', 15 | redirect: { name: 'login' }, 16 | component: page('auth/index'), 17 | children: [ 18 | { 19 | path: 'login', 20 | name: 'login', 21 | component: page('auth/login') 22 | }, 23 | { 24 | path: 'register', 25 | name: 'register', 26 | component: page('auth/register') 27 | }, 28 | { 29 | path: 'confirm-password', 30 | name: 'password.confirm', 31 | component: page('auth/confirm-password') 32 | }, 33 | { 34 | path: 'reset-password/:token', 35 | name: 'password.reset', 36 | component: page('auth/reset-password') 37 | }, 38 | { 39 | path: 'email/verify', 40 | name: 'email.verify', 41 | component: page('auth/verify-email') 42 | }, 43 | ] 44 | }, 45 | 46 | { 47 | name: 'home', 48 | path: '/', 49 | component: page('index'), 50 | }, 51 | { 52 | name: 'about', 53 | path: '/about', 54 | component: page('about'), 55 | }, 56 | { 57 | name: 'contact', 58 | path: '/contact', 59 | component: page('contact'), 60 | }, 61 | { 62 | name: 'dashboard', 63 | path: '/dashboard', 64 | component: page('dashboard'), 65 | }, 66 | 67 | // Admin routes 68 | { 69 | path: '/admin', 70 | name: 'admin.index', 71 | component: page('admin/index'), 72 | children: [ 73 | { 74 | path: 'users', 75 | name: 'admin.users', 76 | component: page('admin/users'), 77 | meta: { permission: 'users.index' }, 78 | }, 79 | { 80 | path: 'roles', 81 | name: 'admin.roles', 82 | component: page('admin/roles'), 83 | meta: { permission: 'roles.index' }, 84 | }, 85 | ] 86 | }, 87 | 88 | // Profile routes 89 | { 90 | path: '/profile', 91 | name: 'profile', 92 | redirect: { name: 'profile.show' }, 93 | component: page('profile/index'), 94 | children: [ 95 | { 96 | path: 'me', 97 | name: 'profile.show', 98 | component: page('profile/show'), 99 | }, 100 | ] 101 | }, 102 | ] 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /client/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjoset/laravel-nuxt/ee0bfe4b247f94fdb037d6f2968e46bc62f1d896/client/static/favicon.ico -------------------------------------------------------------------------------- /client/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjoset/laravel-nuxt/ee0bfe4b247f94fdb037d6f2968e46bc62f1d896/client/static/icon.png -------------------------------------------------------------------------------- /client/static/laravel-nuxt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjoset/laravel-nuxt/ee0bfe4b247f94fdb037d6f2968e46bc62f1d896/client/static/laravel-nuxt.gif -------------------------------------------------------------------------------- /client/static/v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjoset/laravel-nuxt/ee0bfe4b247f94fdb037d6f2968e46bc62f1d896/client/static/v.png -------------------------------------------------------------------------------- /client/static/vuetify-logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /client/store/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | export const state = () => ({ 4 | user: null, 5 | token: null, 6 | }) 7 | 8 | export const getters = { 9 | user: state => state.user, 10 | token: state => state.token, 11 | check: state => Boolean(state.user), 12 | } 13 | 14 | export const mutations = { 15 | SET_USER (state, user) { 16 | state.user = user 17 | }, 18 | 19 | SET_TOKEN (state, token) { 20 | state.token = token 21 | }, 22 | 23 | CLEAR (state) { 24 | state.user = null 25 | state.token = null 26 | 27 | if (process.browser) { 28 | localStorage.loggedOut = true 29 | } 30 | 31 | if (!this.$config.isStateful) { 32 | Cookies.remove('token') 33 | delete this.$axios.defaults.headers.common.Authorization 34 | } 35 | }, 36 | 37 | UPDATE_USER (state, user) { 38 | Object.assign(state.user, user) 39 | }, 40 | } 41 | 42 | export const actions = { 43 | async fetchUser ({ commit }) { 44 | try { 45 | const user = await this.$axios.$get('/user') 46 | 47 | commit('SET_USER', user) 48 | } catch (e) { 49 | commit('CLEAR') 50 | } 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /client/store/commons.js: -------------------------------------------------------------------------------- 1 | 2 | export const state = () => ({ 3 | roles: [], 4 | permissions: [], 5 | }) 6 | 7 | export const getters = { 8 | roles: state => state.roles, 9 | permissions: state => state.permissions, 10 | } 11 | 12 | export const mutations = { 13 | SET_ROLES (state, roles) { 14 | state.roles = roles 15 | }, 16 | 17 | SET_PERMISSIONS (state, permissions) { 18 | state.permissions = permissions 19 | }, 20 | } 21 | 22 | export const actions = { 23 | async fetchRoles ({ commit }) { 24 | try { 25 | const roles = await this.$axios.$get('/roles') 26 | 27 | commit('SET_ROLES', roles) 28 | } catch (e) { 29 | } 30 | }, 31 | 32 | async fetchPermissions ({ commit }) { 33 | try { 34 | const roles = await this.$axios.$get('/permissions') 35 | 36 | commit('SET_PERMISSIONS', roles) 37 | } catch (e) { 38 | } 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | export const state = () => ({ 4 | snackbar: { 5 | message: '', 6 | color: '', 7 | outlined: false, 8 | timeout: 5000, 9 | }, 10 | overlay: { 11 | show: false, 12 | }, 13 | }) 14 | 15 | export const getters = { 16 | snackbar: (state, getters, rootState) => rootState.snackbar, 17 | overlay: (state, getters, rootState) => rootState.overlay, 18 | } 19 | 20 | export const mutations = { 21 | SHOW_SNACKBAR_MESSAGE (state, config = {}) { 22 | Object.assign(state.snackbar, config) 23 | }, 24 | 25 | TOGGLE_OVERLAY (state, options = {}) { 26 | Object.assign(state.overlay, { 27 | ...options, 28 | show: !state.overlay.show, 29 | }) 30 | }, 31 | } 32 | 33 | export const actions = { 34 | nuxtServerInit ({ commit }, { $reqCookies }) { 35 | if ($reqCookies.has('token')) { 36 | commit('auth/SET_TOKEN', $reqCookies.get('token')) 37 | } 38 | }, 39 | 40 | nuxtClientInit ({ commit, getters }, { $config }) { 41 | if (!$config.isStateful) { 42 | const token = Cookies.get('token') 43 | 44 | if (token) { 45 | commit('auth/SET_TOKEN', token) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": ["framework", "laravel"], 6 | "license": "MIT", 7 | "require": { 8 | "php": "^8.1", 9 | "guzzlehttp/guzzle": "^7.2", 10 | "laravel/fortify": "^1.16", 11 | "laravel/framework": "^11.0", 12 | "laravel/sanctum": "^4.0", 13 | "laravel/tinker": "^2.8", 14 | "spatie/laravel-permission": "^6.0" 15 | }, 16 | "require-dev": { 17 | "beyondcode/laravel-dump-server": "^2.0", 18 | "fakerphp/faker": "^1.9.1", 19 | "laravel/sail": "^1.18", 20 | "mockery/mockery": "^1.4.4", 21 | "nunomaduro/collision": "^8.1", 22 | "phpunit/phpunit": "^11.0", 23 | "spatie/laravel-ignition": "^2.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "App\\": "app/", 28 | "Database\\Factories\\": "database/factories/", 29 | "Database\\Seeders\\": "database/seeders/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Tests\\": "tests/" 35 | } 36 | }, 37 | "scripts": { 38 | "post-autoload-dump": [ 39 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 40 | "@php artisan package:discover --ansi" 41 | ], 42 | "post-update-cmd": [ 43 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 44 | ], 45 | "post-root-package-install": [ 46 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 47 | ], 48 | "post-create-project-cmd": [ 49 | "@php artisan key:generate --ansi" 50 | ] 51 | }, 52 | "extra": { 53 | "laravel": { 54 | "dont-discover": [] 55 | } 56 | }, 57 | "config": { 58 | "optimize-autoloader": true, 59 | "preferred-install": "dist", 60 | "sort-packages": true 61 | }, 62 | "minimum-stability": "stable", 63 | "prefer-stable": true 64 | } 65 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORE', 'database'), 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: "array", "database", "file", "memcached", 30 | | "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'array' => [ 37 | 'driver' => 'array', 38 | 'serialize' => false, 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'connection' => env('DB_CACHE_CONNECTION'), 44 | 'table' => env('DB_CACHE_TABLE', 'cache'), 45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 46 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'), 47 | ], 48 | 49 | 'file' => [ 50 | 'driver' => 'file', 51 | 'path' => storage_path('framework/cache/data'), 52 | 'lock_path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 77 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 78 | ], 79 | 80 | 'dynamodb' => [ 81 | 'driver' => 'dynamodb', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 86 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 87 | ], 88 | 89 | 'octane' => [ 90 | 'driver' => 'octane', 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Cache Key Prefix 98 | |-------------------------------------------------------------------------- 99 | | 100 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 101 | | stores, there might be other applications using the same cache. For 102 | | that reason, you may prefix every cache key to avoid collisions. 103 | | 104 | */ 105 | 106 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 107 | 108 | ]; 109 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'api/*', 20 | 'login', 21 | 'logout', 22 | 'register', 23 | 'user/password', 24 | 'reset-password', 25 | 'forgot-password', 26 | 'sanctum/csrf-cookie', 27 | 'user/profile-information', 28 | 'user/confirmed-password-status', 29 | 'user/confirm-password', 30 | 'email/verification-notification', 31 | ], 32 | 33 | 'allowed_methods' => ['*'], 34 | 35 | 'allowed_origins' => ['*'], 36 | 37 | 'allowed_origins_patterns' => [], 38 | 39 | 'allowed_headers' => [ 40 | 'Origin', 41 | 'Content-Type', 42 | 'X-Auth-Token', 43 | 'Authorization', 44 | 'X-XSRF-TOKEN', 45 | // 'X-Socket-Id', 46 | ], 47 | 48 | 'exposed_headers' => [ 49 | 'Cache-Control', 50 | 'Content-Language', 51 | 'Content-Type', 52 | 'Expires', 53 | 'Last-Modified', 54 | 'Pragma', 55 | ], 56 | 57 | 'max_age' => 60 * 60 * 24, 58 | 59 | 'supports_credentials' => true, 60 | 61 | ]; 62 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/private'), 36 | 'serve' => true, 37 | 'throw' => false, 38 | ], 39 | 40 | 'public' => [ 41 | 'driver' => 'local', 42 | 'root' => storage_path('app/public'), 43 | 'url' => env('APP_URL').'/storage', 44 | 'visibility' => 'public', 45 | 'throw' => false, 46 | ], 47 | 48 | 's3' => [ 49 | 'driver' => 's3', 50 | 'key' => env('AWS_ACCESS_KEY_ID'), 51 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 52 | 'region' => env('AWS_DEFAULT_REGION'), 53 | 'bucket' => env('AWS_BUCKET'), 54 | 'url' => env('AWS_URL'), 55 | 'endpoint' => env('AWS_ENDPOINT'), 56 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 57 | 'throw' => false, 58 | ], 59 | 60 | ], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Symbolic Links 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Here you may configure the symbolic links that will be created when the 68 | | `storage:link` Artisan command is executed. The array keys should be 69 | | the locations of the links and the values should be their targets. 70 | | 71 | */ 72 | 73 | 'links' => [ 74 | public_path('storage') => storage_path('app/public'), 75 | ], 76 | 77 | ]; 78 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'log'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Mailer Configurations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Here you may configure all of the mailers used by your application plus 25 | | their respective settings. Several examples have been configured for 26 | | you and you are free to add your own as your application requires. 27 | | 28 | | Laravel supports a variety of mail "transport" drivers that can be used 29 | | when delivering an email. You may specify which one you're using for 30 | | your mailers below. You may also add additional mailers if needed. 31 | | 32 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 33 | | "postmark", "resend", "log", "array", 34 | | "failover", "roundrobin" 35 | | 36 | */ 37 | 38 | 'mailers' => [ 39 | 40 | 'smtp' => [ 41 | 'transport' => 'smtp', 42 | 'scheme' => env('MAIL_SCHEME'), 43 | 'url' => env('MAIL_URL'), 44 | 'host' => env('MAIL_HOST', '127.0.0.1'), 45 | 'port' => env('MAIL_PORT', 2525), 46 | 'username' => env('MAIL_USERNAME'), 47 | 'password' => env('MAIL_PASSWORD'), 48 | 'timeout' => null, 49 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 50 | ], 51 | 52 | 'ses' => [ 53 | 'transport' => 'ses', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 59 | // 'client' => [ 60 | // 'timeout' => 5, 61 | // ], 62 | ], 63 | 64 | 'resend' => [ 65 | 'transport' => 'resend', 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 | 'roundrobin' => [ 91 | 'transport' => 'roundrobin', 92 | 'mailers' => [ 93 | 'ses', 94 | 'postmark', 95 | ], 96 | ], 97 | 98 | ], 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Global "From" Address 103 | |-------------------------------------------------------------------------- 104 | | 105 | | You may wish for all emails sent by your application to be sent from 106 | | the same address. Here you may specify a name and address that is 107 | | used globally for all emails that are sent by your application. 108 | | 109 | */ 110 | 111 | 'from' => [ 112 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 113 | 'name' => env('MAIL_FROM_NAME', 'Example'), 114 | ], 115 | 116 | ]; 117 | -------------------------------------------------------------------------------- /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. If this value is null, personal access tokens do 45 | | not expire. This won't tweak the lifetime of first-party sessions. 46 | | 47 | */ 48 | 49 | 'expiration' => env('SANCTUM_TOKEN_EXPIRATION_TIME') ?: 120, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Sanctum Middleware 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When authenticating your first-party SPA with Sanctum you may need to 57 | | customize some of the middleware Sanctum uses while processing the 58 | | request. You may change the middleware listed below as required. 59 | | 60 | */ 61 | 62 | 'middleware' => [ 63 | 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 64 | 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, 65 | 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, 66 | ], 67 | 68 | ]; 69 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class UserFactory extends Factory 14 | { 15 | /** 16 | * The current password being used by the factory. 17 | */ 18 | protected static ?string $password; 19 | 20 | /** 21 | * Define the model's default state. 22 | * 23 | * @return array 24 | */ 25 | public function definition(): array 26 | { 27 | return [ 28 | 'name' => $this->faker->name(), 29 | 'email' => $this->faker->unique()->safeEmail(), 30 | 'email_verified_at' => now(), 31 | 'active' => true, 32 | 'password' => static::$password ??= Hash::make('password'), 33 | 'remember_token' => Str::random(10), 34 | ]; 35 | } 36 | 37 | /** 38 | * Indicate that the model's email address should be unverified. 39 | */ 40 | public function unverified(): static 41 | { 42 | return $this->state(fn (array $attributes) => [ 43 | 'email_verified_at' => null, 44 | ]); 45 | } 46 | 47 | /** 48 | * Indicate that the model's status is not banned. 49 | */ 50 | public function active(): static 51 | { 52 | return $this->state(fn (array $attributes) => [ 53 | 'active' => true, 54 | ]); 55 | } 56 | 57 | /** 58 | * Indicate that the model's status is banned. 59 | */ 60 | public function banned(): static 61 | { 62 | return $this->state(fn (array $attributes) => [ 63 | 'active' => false, 64 | ]); 65 | } 66 | 67 | /** 68 | * Set the model's status randomly. 69 | */ 70 | public function randomStatus(): static 71 | { 72 | return $this->state(fn (array $attributes) => [ 73 | 'active' => $this->faker->randomElement([true, false]), 74 | ]); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('name'); 16 | $table->string('email')->unique(); 17 | $table->string('avatar')->default('default-avatar.png'); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->boolean('active'); 21 | $table->rememberToken(); 22 | $table->timestamps(); 23 | }); 24 | 25 | Schema::create('password_reset_tokens', function (Blueprint $table) { 26 | $table->string('email')->primary(); 27 | $table->string('token'); 28 | $table->timestamp('created_at')->nullable(); 29 | }); 30 | 31 | Schema::create('sessions', function (Blueprint $table) { 32 | $table->string('id')->primary(); 33 | $table->foreignId('user_id')->nullable()->index(); 34 | $table->string('ip_address', 45)->nullable(); 35 | $table->text('user_agent')->nullable(); 36 | $table->longText('payload'); 37 | $table->integer('last_activity')->index(); 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | */ 44 | public function down(): void 45 | { 46 | Schema::dropIfExists('users'); 47 | Schema::dropIfExists('password_reset_tokens'); 48 | Schema::dropIfExists('sessions'); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php: -------------------------------------------------------------------------------- 1 | text('two_factor_secret') 15 | ->after('password') 16 | ->nullable(); 17 | 18 | $table->text('two_factor_recovery_codes') 19 | ->after('two_factor_secret') 20 | ->nullable(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::table('users', function (Blueprint $table) { 30 | $table->dropColumn('two_factor_secret', 'two_factor_recovery_codes'); 31 | }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->morphs('tokenable'); 16 | $table->string('name'); 17 | $table->string('token', 64)->unique(); 18 | $table->text('abilities')->nullable(); 19 | $table->timestamp('last_used_at')->nullable(); 20 | $table->timestamp('expires_at')->nullable(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('personal_access_tokens'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /database/seeders/RolesAndPermissionsSeeder.php: -------------------------------------------------------------------------------- 1 | forgetCachedPermissions(); 18 | 19 | Role::create(['name' => 'Super Admin']); 20 | 21 | // Users 22 | Permission::create([ 23 | 'name' => 'users.index', 24 | 'module' => 'Users', 25 | 'description' => 'List users.', 26 | ]); 27 | 28 | Permission::create([ 29 | 'name' => 'users.store', 30 | 'module' => 'Users', 31 | 'description' => 'Create users.', 32 | ]); 33 | 34 | Permission::create([ 35 | 'name' => 'users.update', 36 | 'module' => 'Users', 37 | 'description' => 'Update users info.', 38 | ]); 39 | 40 | Permission::create([ 41 | 'name' => 'users.delete', 42 | 'module' => 'Users', 43 | 'description' => 'Delete users.', 44 | ]); 45 | 46 | Permission::create([ 47 | 'name' => 'users.toggle', 48 | 'module' => 'Users', 49 | 'description' => 'Ban & unban users.', 50 | ]); 51 | 52 | Permission::create([ 53 | 'name' => 'users.assign-role', 54 | 'module' => 'Users', 55 | 'description' => 'Assign roles to users.', 56 | ]); 57 | 58 | // Roles 59 | Permission::create([ 60 | 'name' => 'roles.index', 61 | 'module' => 'Roles & Permissions', 62 | 'description' => 'List roles', 63 | ]); 64 | 65 | Permission::create([ 66 | 'name' => 'roles.store', 67 | 'module' => 'Roles & Permissions', 68 | 'description' => 'Create roles', 69 | ]); 70 | 71 | Permission::create([ 72 | 'name' => 'roles.update', 73 | 'module' => 'Roles & Permissions', 74 | 'description' => 'Update roles', 75 | ]); 76 | 77 | Permission::create([ 78 | 'name' => 'roles.delete', 79 | 'module' => 'Roles & Permissions', 80 | 'description' => 'Delete roles', 81 | ]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'The provided credentials are incorrect.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /lang/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hello!": "¡Hola!", 3 | "Hello :name!": "¡Hola :name!", 4 | "Regards": "Saludos", 5 | "Regards, :app": "Saludos, :app", 6 | "Welcome to :appName": "Bienvenido a :appName", 7 | "Welcome to :appName, your account is ready!": "¡Bienvenido al equipo de :appName, tu cuenta está lista!", 8 | "Your password is: :password": "Tu contraseña es: :password", 9 | "Login": "Ingresar", 10 | "The provided password does not match your current password.": "La contraseña ingresada no coincide con tu contraseña actual", 11 | "Your account is currently banned!": "¡Tu cuenta se encuentra bloqueada!", 12 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser: [:actionURL](:actionURL)": "Si tienes problemas al hacer click en el botón \":actionText\", copia y pega el siguiente\nenlace en tu navegador: [:actionURL](:actionURL)", 13 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser: [:displayableActionUrl](:actionURL)": "Si tienes problemas al hacer click en el botón \":actionText\", copia y pega el siguiente\nenlace en tu navegador: [:displayableActionUrl](:actionURL)", 14 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si tienes problemas al hacer click en el botón \":actionText\", copia y pega el siguiente\nenlace en tu navegador:", 15 | "Send Password Reset Link": "Enviar enlace para restablecer contraseña", 16 | "Verify Email Address": "Confirmar correo electrónico", 17 | "Verify Your Email Address": "Confirma tu correo electrónico", 18 | "Please click the button below to verify your email address.": "Por favor pulsa el siguiente botón para confirmar tu correo electrónico.", 19 | "If you did not create an account, no further action is required.": "Si no has creado una cuenta, puedes ignorar o eliminar este e-mail.", 20 | "A fresh verification link has been sent to your email address.": "Se ha enviado un nuevo enlace de verificación a tu correo electrónico.", 21 | "Before proceeding, please check your email for a verification link.": "Antes de poder continuar, por favor, confirma tu correo electrónico con el enlace que te hemos enviado.", 22 | "If you did not receive the email": "Si no has recibido el email", 23 | "click here to request another": "pulsa aquí para que te enviemos otro", 24 | "Reset Password Notification": "Notificación de restablecimiento de contraseña", 25 | "You are receiving this email because we received a password reset request for your account.": "Recibes este correo porque hemos recibido una solicitud de restablecimiento de contraseña para tu cuenta.", 26 | "Reset Password": "Restablecer contraseña", 27 | "This password reset link will expire in :count minutes.": "Este link expirará en :count minutos.", 28 | "If you did not request a password reset, no further action is required.": "Si no has solicitado un restablecimiento de contraseña puedes ignorar este correo.", 29 | "All rights reserved.": "Todos los derechos reservados.", 30 | "The provided password was incorrect.": "La contraseña es incorrecta." 31 | } -------------------------------------------------------------------------------- /lang/es/auth.php: -------------------------------------------------------------------------------- 1 | 'Estas credenciales no coinciden con nuestros registros.', 17 | 'throttle' => 'Demasiados intentos de acceso. Por favor intente nuevamente en :seconds segundos.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/es/pagination.php: -------------------------------------------------------------------------------- 1 | '« Anterior', 17 | 'next' => 'Siguiente »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/es/passwords.php: -------------------------------------------------------------------------------- 1 | '¡Su contraseña ha sido restablecida!', 17 | 'sent' => '¡Recordatorio de contraseña enviado!', 18 | 'throttled' => 'Por favor espere antes de volver a intentarlo.', 19 | 'token' => 'El token de restablecimiento de contraseña es inválido.', 20 | 'user' => 'No se ha encontrado un usuario con esa dirección de correo.', 21 | 'password' => 'Las contraseñas deben tener al menos seis caracteres y coincidir con la confirmación.' 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nuxt -c client/nuxt.config.js", 5 | "build": "nuxt build -c client/nuxt.config.js", 6 | "start": "nuxt start -c client/nuxt.config.js", 7 | "generate": "nuxt generate -c client/nuxt.config.js", 8 | "lint": "npm run lint:js", 9 | "lint:js": "eslint --ext .js,.vue client/", 10 | "lint:fix": "eslint --fix --ext .js,.vue client/" 11 | }, 12 | "dependencies": { 13 | "@nuxtjs/axios": "^5.13.6", 14 | "@nuxtjs/i18n": "^7.2.1", 15 | "@nuxtjs/pwa": "^3.3.5", 16 | "core-js": "^3.21.1", 17 | "js-cookie": "^3.0.1", 18 | "nuxt": "^2.15.8", 19 | "sweetalert2": "^11.4.8", 20 | "vform": "^2.1.2" 21 | }, 22 | "devDependencies": { 23 | "@babel/eslint-parser": "^7.17.0", 24 | "@mdi/font": "^6.6.96", 25 | "@mdi/js": "^6.6.96", 26 | "@nuxtjs/eslint-config": "^9.0.0", 27 | "@nuxtjs/eslint-module": "^3.0.2", 28 | "@nuxtjs/router": "^1.7.0", 29 | "@nuxtjs/vuetify": "^1.12.3", 30 | "eslint": "^8.12.0", 31 | "eslint-plugin-nuxt": "^3.2.0", 32 | "eslint-plugin-vue": "^8.5.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | 33 | 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjoset/laravel-nuxt/ee0bfe4b247f94fdb037d6f2968e46bc62f1d896/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjoset/laravel-nuxt/ee0bfe4b247f94fdb037d6f2968e46bc62f1d896/resources/css/app.css -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | require('./bootstrap'); 2 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | window._ = require('lodash'); 2 | 3 | /** 4 | * We'll load the axios HTTP library which allows us to easily issue requests 5 | * to our Laravel back-end. This library automatically handles sending the 6 | * CSRF token as a header based on the value of the "XSRF" token cookie. 7 | */ 8 | 9 | window.axios = require('axios'); 10 | 11 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 12 | 13 | /** 14 | * Echo exposes an expressive API for subscribing to channels and listening 15 | * for events that are broadcast by Laravel. Echo and event broadcasting 16 | * allows your team to easily build robust real-time web applications. 17 | */ 18 | 19 | // import Echo from 'laravel-echo'; 20 | 21 | // window.Pusher = require('pusher-js'); 22 | 23 | // window.Echo = new Echo({ 24 | // broadcaster: 'pusher', 25 | // key: process.env.MIX_PUSHER_APP_KEY, 26 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 27 | // forceTLS: true 28 | // }); 29 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 23 | Route::get('user', [UserController::class, 'current'])->name('user.current'); 24 | Route::put('user/avatar', [AvatarController::class, 'update'])->name('user.avatar.update'); 25 | Route::delete('user/avatar', [AvatarController::class, 'destroy'])->name('user.avatar.destroy'); 26 | 27 | Route::prefix('admin')->name('admin.')->group(function () { 28 | Route::apiResource('users', AdminUserController::class)->except('show'); 29 | Route::patch('users/{user}/toggle', [AdminUserController::class, 'toggle']); 30 | Route::apiResource('roles', RoleController::class)->except('show'); 31 | }); 32 | 33 | Route::get('roles', [CommonController::class, 'roles'])->name('roles.index'); 34 | Route::get('permissions', [CommonController::class, 'permissions'])->name('permissions.index'); 35 | }); 36 | 37 | Route::middleware('guest:sanctum')->group(function () { 38 | }); 39 | 40 | // Use fortify routes in api context for stateless apps, 41 | // comment or remove them if your app don't need stateless auth routes 42 | require __DIR__.'/auth.api.php'; 43 | -------------------------------------------------------------------------------- /routes/auth.api.php: -------------------------------------------------------------------------------- 1 | group(function () { 29 | Route::post('logout/token', [ApiController::class, 'logout'])->name('api.logout'); 30 | }); 31 | 32 | Route::middleware('guest:sanctum')->group(function () { 33 | Route::post('login/token', [ApiController::class, 'login'])->name('api.login'); 34 | Route::post('register', [RegisteredUserController::class, 'store'])->name('api.register'); 35 | }); 36 | 37 | // Email Verification... 38 | if (Features::enabled(Features::emailVerification())) { 39 | Route::get('email/verify/{id}/{hash}', [VerifyEmailController::class, '__invoke']) 40 | ->middleware(['auth:sanctum', 'signed', 'throttle:6,1']) 41 | ->name('api.verification.verify'); 42 | 43 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 44 | ->middleware(['auth:sanctum', 'throttle:6,1']) 45 | ->name('api.verification.send'); 46 | } 47 | 48 | // Profile Information... 49 | if (Features::enabled(Features::updateProfileInformation())) { 50 | Route::put('user/profile-information', [ProfileInformationController::class, 'update']) 51 | ->middleware(['auth:sanctum']) 52 | ->name('api.user-profile-information.update'); 53 | } 54 | 55 | // Passwords... 56 | if (Features::enabled(Features::updatePasswords())) { 57 | Route::put('/user/password', [PasswordController::class, 'update']) 58 | ->middleware(['auth:sanctum']) 59 | ->name('api.user-password.update'); 60 | } 61 | 62 | // Password Reset... 63 | if (Features::enabled(Features::resetPasswords())) { 64 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) 65 | ->middleware('guest:sanctum') 66 | ->name('api.password.email'); 67 | 68 | Route::post('reset-password', [NewPasswordController::class, 'store']) 69 | ->middleware('guest:sanctum') 70 | ->name('api.password.update'); 71 | } 72 | 73 | // Password Confirmation... 74 | Route::get('/user/confirmed-password-status', [ConfirmedPasswordStatusController::class, 'show']) 75 | ->middleware(['auth:sanctum']) 76 | ->name('api.password.confirmation'); 77 | 78 | Route::post('/user/confirm-password', [ConfirmablePasswordController::class, 'store']) 79 | ->middleware(['auth:sanctum']); 80 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | where('path', '(.*)'); 18 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !default-avatar.png 4 | -------------------------------------------------------------------------------- /storage/app/public/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjoset/laravel-nuxt/ee0bfe4b247f94fdb037d6f2968e46bc62f1d896/storage/app/public/default-avatar.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Feature/AuthTest.php: -------------------------------------------------------------------------------- 1 | postJson('/register', [ 29 | 'name' => 'Laravel Nuxt', 30 | 'email' => 'admin@test.test', 31 | 'password' => 'password', 32 | 'password_confirmation' => 'password', 33 | ]) 34 | ->assertCreated() 35 | ->assertJsonStructure($attrs); 36 | } 37 | 38 | public function test_can_login() 39 | { 40 | $attrs = ['id', 'name', 'email', 'photo_url']; 41 | 42 | if (Features::enabled(Features::emailVerification())) { 43 | $attrs[] = 'email_verified_at'; 44 | } 45 | 46 | $user = User::factory()->create(); 47 | 48 | $this->postJson('/login', [ 49 | 'email' => $user->email, 50 | 'password' => 'password' 51 | ]) 52 | ->assertStatus(200) 53 | ->assertJsonStructure($attrs); 54 | 55 | $this->assertAuthenticated('sanctum'); 56 | } 57 | 58 | public function test_cannot_login_if_account_is_banned() 59 | { 60 | $user = User::factory()->banned()->create(); 61 | 62 | $this->postJson('/login', [ 63 | 'email' => $user->email, 64 | 'password' => 'password' 65 | ]) 66 | ->assertForbidden() 67 | ->assertJson([ 68 | 'status' => __('Your account is currently banned!'), 69 | ]); 70 | 71 | $this->assertGuest('sanctum'); 72 | } 73 | 74 | public function test_can_logout() 75 | { 76 | $this->actingAs($user = User::factory()->create()); 77 | 78 | $this->postJson('/logout')->assertStatus(204); 79 | $this->assertGuest('sanctum'); 80 | } 81 | 82 | public function test_can_retrive_current_user() 83 | { 84 | Sanctum::actingAs($user = User::factory()->create()); 85 | 86 | $attrs = [ 87 | 'id' => $user->id, 88 | 'name' => $user->name, 89 | 'email' => $user->email, 90 | 'photo_url' => env('APP_URL') . '/storage/default-avatar.png', 91 | ]; 92 | 93 | if (Features::enabled(Features::emailVerification())) { 94 | $attrs['email_verified_at'] = optional($user->email_verified_at)->toJson(); 95 | } 96 | 97 | $this->getJson('/api/user') 98 | ->assertJson($attrs); 99 | } 100 | 101 | public function user_session_is_invalidated_after_its_account_has_been_banned() 102 | { 103 | Sanctum::actingAs($user = User::factory()->create()); 104 | 105 | $this->getJson('/api/user')->assertStatus(200); 106 | 107 | $user->update(['active' => false]); 108 | 109 | $this->getJson('/api/user') 110 | ->assertForbidden() 111 | ->assertJson([ 112 | 'status' => __('Your account is currently banned!'), 113 | ]); 114 | 115 | $this->assertGuest('sanctum'); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/Feature/AvatarTest.php: -------------------------------------------------------------------------------- 1 | create()); 20 | 21 | $avatar = UploadedFile::fake()->image('avatar.jpg'); 22 | 23 | $response = $this->putJson('/api/user/avatar', [ 24 | 'avatar' => $avatar 25 | ]) 26 | ->assertStatus(200) 27 | ->assertJsonStructure(['photo_url']); 28 | 29 | Storage::assertExists($path = 'avatars/' . $avatar->hashName()); 30 | 31 | // Ensure default is not being deleted 32 | Storage::assertExists(User::DEFAULT_AVATAR_PATH); 33 | 34 | $this->assertEquals($path, $user->fresh()->avatar); 35 | $this->assertEquals($response->json()['photo_url'], $user->fresh()->photo_url); 36 | } 37 | 38 | public function test_user_avatar_cannot_be_updated_if_file_is_not_an_image() 39 | { 40 | Sanctum::actingAs($user = User::factory()->create()); 41 | 42 | $this->putJson('/api/user/avatar', [ 43 | 'avatar' => UploadedFile::fake()->image('wrong-avatar.pdf') 44 | ]) 45 | ->assertStatus(422) 46 | ->assertJsonStructure([ 47 | 'message', 48 | 'errors' => ['avatar'] 49 | ]); 50 | } 51 | 52 | public function test_user_avatar_can_be_restored_to_defaults() 53 | { 54 | Sanctum::actingAs($user = User::factory()->create([ 55 | 'avatar' => $avatar = UploadedFile::fake()->image('avatar.jpg')->store('avatars') 56 | ])); 57 | 58 | $response = $this->deleteJson('/api/user/avatar') 59 | ->assertStatus(200) 60 | ->assertJsonStructure(['photo_url']); 61 | 62 | Storage::assertMissing($avatar); 63 | Storage::assertExists(User::DEFAULT_AVATAR_PATH); 64 | 65 | $this->assertEquals(User::DEFAULT_AVATAR_PATH, $user->fresh()->avatar); 66 | $this->assertEquals($response->json()['photo_url'], $user->fresh()->photo_url); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Feature/CommonControllerTest.php: -------------------------------------------------------------------------------- 1 | seed(RolesAndPermissionsSeeder::class); 22 | } 23 | 24 | public function test_can_retrive_all_roles_list() 25 | { 26 | Sanctum::actingAs($user = User::factory()->create()); 27 | 28 | $user->assignRole('Super Admin'); 29 | 30 | $this->getJson('/api/roles') 31 | ->assertStatus(200) 32 | ->assertJsonStructure([ 33 | 'data' => [ 34 | '*' => [ 35 | 'id', 36 | 'name', 37 | ] 38 | ], 39 | 'meta' => [ 40 | 'current_page', 41 | 'from', 42 | 'last_page', 43 | 'path', 44 | 'per_page', 45 | 'to', 46 | 'total', 47 | ], 48 | 'links', 49 | ]); 50 | } 51 | 52 | public function test_a_user_with_no_permissions_cannot_retrive_all_roles_list() 53 | { 54 | Sanctum::actingAs($user = User::factory()->create()); 55 | 56 | $this->getJson('/api/roles') 57 | ->assertForbidden(); 58 | } 59 | 60 | public function test_can_retrive_all_permissions_list() 61 | { 62 | Sanctum::actingAs($user = User::factory()->create()); 63 | 64 | $user->assignRole('Super Admin'); 65 | 66 | $this->getJson('/api/permissions') 67 | ->assertStatus(200) 68 | ->assertJsonStructure([ 69 | '*' => [ 70 | 'id', 71 | 'module', 72 | 'description', 73 | ] 74 | ]); 75 | } 76 | 77 | public function test_a_user_with_no_permissions_cannot_retrive_all_permissions_list() 78 | { 79 | Sanctum::actingAs($user = User::factory()->create()); 80 | 81 | $this->getJson('/api/roles') 82 | ->assertForbidden(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/Feature/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Email verification not enabled.'); 23 | } 24 | 25 | Event::fake(); 26 | 27 | $user = User::factory()->unverified()->create(); 28 | 29 | $verificationUrl = URL::temporarySignedRoute( 30 | 'api.verification.verify', 31 | now()->addMinutes(60), 32 | ['id' => $user->id, 'hash' => sha1($user->email)] 33 | ); 34 | 35 | $response = $this->actingAs($user)->getJson($verificationUrl); 36 | 37 | Event::assertDispatched(Verified::class); 38 | 39 | $this->assertTrue($user->fresh()->hasVerifiedEmail()); 40 | $response->assertStatus(204); 41 | } 42 | 43 | public function test_email_can_not_verified_with_invalid_hash() 44 | { 45 | if (! Features::enabled(Features::emailVerification())) { 46 | return $this->markTestSkipped('Email verification not enabled.'); 47 | } 48 | 49 | Event::fake(); 50 | 51 | $user = User::factory()->unverified()->create(); 52 | 53 | $verificationUrl = URL::temporarySignedRoute( 54 | 'api.verification.verify', 55 | now()->addMinutes(60), 56 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 57 | ); 58 | 59 | $response = $this->actingAs($user)->getJson($verificationUrl); 60 | 61 | Event::assertNotDispatched(Verified::class); 62 | 63 | $this->assertFalse($user->fresh()->hasVerifiedEmail()); 64 | $response->assertStatus(403); 65 | } 66 | 67 | public function test_email_verification_notification_can_be_sent() 68 | { 69 | if (! Features::enabled(Features::emailVerification())) { 70 | return $this->markTestSkipped('Email verification not enabled.'); 71 | } 72 | 73 | Notification::fake(); 74 | 75 | $user = User::factory()->unverified()->create(); 76 | 77 | $response = $this->actingAs($user)->postJson(route('verification.send')); 78 | 79 | Notification::assertSentTo($user, VerifyEmail::class); 80 | 81 | $response->assertStatus(202); 82 | } 83 | 84 | public function test_email_verification_notification_can_not_be_sent_if_email_is_already_verified() 85 | { 86 | if (! Features::enabled(Features::emailVerification())) { 87 | return $this->markTestSkipped('Email verification not enabled.'); 88 | } 89 | 90 | Notification::fake(); 91 | 92 | $user = User::factory()->create(); 93 | 94 | $response = $this->actingAs($user)->postJson(route('verification.send')); 95 | 96 | Notification::assertNothingSent(); 97 | 98 | $response->assertStatus(204); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/Feature/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $response = $this->actingAs($user)->getJson('/user/confirmed-password-status'); 18 | 19 | $response 20 | ->assertOk() 21 | ->assertJsonStructure(['confirmed']); 22 | } 23 | 24 | public function test_password_can_be_confirmed() 25 | { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this->actingAs($user)->postJson('/user/confirm-password', [ 29 | 'password' => 'password', 30 | ]); 31 | 32 | $response->assertStatus(201); 33 | 34 | $this->actingAs($user) 35 | ->getJson('/user/confirmed-password-status') 36 | ->assertOk() 37 | ->assertJson(['confirmed' => true]); 38 | } 39 | 40 | public function test_password_is_not_confirmed_with_invalid_password() 41 | { 42 | $user = User::factory()->create(); 43 | 44 | $response = $this->actingAs($user)->postJson('/user/confirm-password', [ 45 | 'password' => 'wrong-password', 46 | ]); 47 | 48 | $response 49 | ->assertStatus(422) 50 | ->assertJsonStructure([ 51 | 'message', 52 | 'errors' => ['password'] 53 | ]); 54 | 55 | $this->actingAs($user) 56 | ->getJson('/user/confirmed-password-status') 57 | ->assertOk() 58 | ->assertJson(['confirmed' => false]); 59 | } 60 | 61 | public function test_check_confirmed_password_status_from_api() 62 | { 63 | $token = User::factory()->create()->createToken('Test Token')->plainTextToken; 64 | 65 | $response = $this->withHeaders(['Authorization' => "Bearer {$token}"]) 66 | ->getJson('/api/user/confirmed-password-status'); 67 | 68 | $response 69 | ->assertOk() 70 | ->assertJsonStructure(['confirmed']); 71 | } 72 | 73 | public function test_password_can_be_confirmed_from_api() 74 | { 75 | $token = User::factory()->create()->createToken('Test Token')->plainTextToken; 76 | 77 | $response = $this->withHeaders(['Authorization' => "Bearer {$token}"]) 78 | ->postJson('/api/user/confirm-password', [ 79 | 'password' => 'password', 80 | ]); 81 | 82 | $response->assertStatus(201); 83 | 84 | $this->withHeaders(['Authorization' => "Bearer {$token}"]) 85 | ->getJson('/api/user/confirmed-password-status') 86 | ->assertOk() 87 | ->assertJson(['confirmed' => true]); 88 | } 89 | 90 | public function test_password_is_not_confirmed_with_invalid_password_from_api() 91 | { 92 | $token = User::factory()->create()->createToken('Test Token')->plainTextToken; 93 | 94 | $response = $this->withHeaders(['Authorization' => "Bearer {$token}"]) 95 | ->postJson('/api/user/confirm-password', [ 96 | 'password' => 'wrong-password', 97 | ]); 98 | 99 | $response 100 | ->assertStatus(422) 101 | ->assertJsonStructure([ 102 | 'message', 103 | 'errors' => ['password'] 104 | ]); 105 | 106 | $this->withHeaders(['Authorization' => "Bearer {$token}"]) 107 | ->getJson('/api/user/confirmed-password-status') 108 | ->assertOk() 109 | ->assertJson(['confirmed' => false]); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/Feature/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | user = User::factory()->create(); 26 | } 27 | 28 | public function test_reset_password_link_can_be_requested() 29 | { 30 | if (! Features::enabled(Features::updatePasswords())) { 31 | return $this->markTestSkipped('Password updates are not enabled.'); 32 | } 33 | 34 | Notification::fake(); 35 | 36 | $this->postJson('/forgot-password', [ 37 | 'email' => $this->user->email, 38 | ]); 39 | 40 | Notification::assertSentTo($this->user, ResetPassword::class); 41 | } 42 | 43 | public function test_password_can_be_reset_with_valid_token() 44 | { 45 | if (! Features::enabled(Features::updatePasswords())) { 46 | return $this->markTestSkipped('Password updates are not enabled.'); 47 | } 48 | 49 | Notification::fake(); 50 | 51 | $this->postJson('/forgot-password', [ 52 | 'email' => $this->user->email, 53 | ]); 54 | 55 | Notification::assertSentTo($this->user, ResetPassword::class, function ($notification) { 56 | $response = $this->postJson('/reset-password', [ 57 | 'token' => $notification->token, 58 | 'email' => $this->user->email, 59 | 'password' => 'password', 60 | 'password_confirmation' => 'password', 61 | ]); 62 | 63 | $response->assertSessionHasNoErrors(); 64 | 65 | return true; 66 | }); 67 | } 68 | 69 | public function test_password_cannot_be_reset_with_invalid_token() 70 | { 71 | if (! Features::enabled(Features::updatePasswords())) { 72 | return $this->markTestSkipped('Password updates are not enabled.'); 73 | } 74 | 75 | Notification::fake(); 76 | 77 | $this->postJson('/forgot-password', [ 78 | 'email' => $this->user->email, 79 | ]); 80 | 81 | Notification::assertSentTo($this->user, ResetPassword::class, function ($notification) { 82 | $response = $this->postJson('/reset-password', [ 83 | 'token' => 'invalid-token', 84 | 'email' => $this->user->email, 85 | 'password' => 'password', 86 | 'password_confirmation' => 'password', 87 | ]); 88 | 89 | $response->assertStatus(422); 90 | 91 | return true; 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Feature/ProfileInformationTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 19 | 20 | $response = $this->putJson('/user/profile-information', [ 21 | 'name' => 'Test Name', 22 | 'email' => 'test@example.com', 23 | ]); 24 | 25 | $user->fresh(); 26 | 27 | $this->assertEquals('Test Name', $user->name); 28 | $this->assertEquals('test@example.com', $user->email); 29 | } 30 | 31 | public function test_profile_information_cannot_be_updated_with_duplicated_email() 32 | { 33 | User::factory()->create(['email' => 'test@example.com']); 34 | 35 | $this->actingAs($user = User::factory()->create()); 36 | 37 | $this->putJson('/user/profile-information', [ 38 | 'name' => 'Test Name', 39 | 'email' => 'test@example.com', 40 | ]) 41 | ->assertStatus(422) 42 | ->assertJsonStructure([ 43 | 'message', 44 | 'errors' => ['email'] 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Feature/SetLocaleTest.php: -------------------------------------------------------------------------------- 1 | withHeaders(['Accept-Language' => 'es']) 14 | ->postJson('/login', [ 15 | 'email' => 'wrong-email@example.com', 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertEquals('es', $this->app->getLocale()); 20 | $this->assertEquals( 21 | $response->json('errors.email.0'), 22 | 'Estas credenciales no coinciden con nuestros registros.', 23 | ); 24 | } 25 | 26 | public function test_set_fallback_locale_if_locale_is_invalid() 27 | { 28 | $response = $this->withHeaders(['Accept-Language' => 'wrong-locale']) 29 | ->postJson('/login', [ 30 | 'email' => 'wrong-email@example.com', 31 | 'password' => 'password', 32 | ]); 33 | 34 | $this->assertEquals('en', $this->app->getLocale()); 35 | $this->assertEquals( 36 | $response->json('errors.email.0'), 37 | 'The provided credentials are incorrect.', 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Feature/UpdatePasswordTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 18 | 19 | $response = $this->putJson('/user/password', [ 20 | 'current_password' => 'password', 21 | 'password' => 'new-password', 22 | 'password_confirmation' => 'new-password', 23 | ]); 24 | 25 | $this->assertTrue(Hash::check('new-password', $user->fresh()->password)); 26 | } 27 | 28 | public function test_current_password_must_be_correct() 29 | { 30 | $this->actingAs($user = User::factory()->create()); 31 | 32 | $this->putJson('/user/password', [ 33 | 'current_password' => 'wrong-password', 34 | 'password' => 'new-password', 35 | 'password_confirmation' => 'new-password', 36 | ]) 37 | ->assertStatus(422) 38 | ->assertJsonStructure([ 39 | 'message', 40 | 'errors' => ['current_password'] 41 | ]); 42 | 43 | $this->assertTrue(Hash::check('password', $user->fresh()->password)); 44 | } 45 | 46 | public function test_new_passwords_must_match() 47 | { 48 | $this->actingAs($user = User::factory()->create()); 49 | 50 | $this->putJson('/user/password', [ 51 | 'current_password' => 'password', 52 | 'password' => 'new-password', 53 | 'password_confirmation' => 'wrong-password', 54 | ]) 55 | ->assertStatus(422) 56 | ->assertJsonStructure([ 57 | 'message', 58 | 'errors' => ['password'] 59 | ]); 60 | 61 | $this->assertTrue(Hash::check('password', $user->fresh()->password)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | --------------------------------------------------------------------------------