├── .chipperci.yml ├── .editorconfig ├── .env.example ├── .env.testing ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── Actions │ └── Fortify │ │ ├── CreateNewUser.php │ │ ├── PasswordValidationRules.php │ │ ├── ResetUserPassword.php │ │ ├── UpdateUserPassword.php │ │ └── UpdateUserProfileInformation.php ├── Http │ ├── Controllers │ │ ├── Api │ │ │ └── V1 │ │ │ │ └── User │ │ │ │ └── MeController.php │ │ └── Controller.php │ └── Resources │ │ └── UserResource.php ├── Models │ └── User.php ├── Policies │ └── TeamPolicy.php └── Providers │ ├── AppServiceProvider.php │ └── FortifyServiceProvider.php ├── art ├── bmc_qr.jpg ├── screenshot-home.png ├── screenshot-login.png └── screenshot-settings.png ├── artisan ├── bin └── install.sh ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── fortify.php ├── logging.php ├── mail.php ├── queue.php ├── sanctum.php ├── services.php └── session.php ├── database ├── .gitignore ├── factories │ ├── TeamFactory.php │ └── 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 │ ├── 2024_04_28_125542_add_two_factor_columns_to_users_table.php │ ├── 2024_04_28_125548_create_personal_access_tokens_table.php │ ├── 2024_04_28_125548_create_teams_table.php │ ├── 2024_04_28_125549_create_team_user_table.php │ └── 2024_04_28_125550_create_team_invitations_table.php └── seeders │ └── DatabaseSeeder.php ├── jsconfig.json ├── package-lock.json ├── package.json ├── phpunit.xml ├── pint.json ├── postcss.config.js ├── public ├── .htaccess ├── favicon.ico ├── index.php ├── manifest.json └── robots.txt ├── resources ├── css │ └── app.css ├── js │ ├── app.js │ ├── components │ │ ├── Logo.vue │ │ ├── Navigation.vue │ │ └── user │ │ │ ├── PasswordUpdate.vue │ │ │ ├── ProfileUpdate.vue │ │ │ └── TwoFactoreAuthentication.vue │ ├── layouts │ │ ├── App.vue │ │ ├── Authenticated.vue │ │ └── Guest.vue │ ├── pages │ │ ├── Home.vue │ │ ├── NotFound.vue │ │ ├── User.vue │ │ └── auth │ │ │ ├── ConfirmPassword.vue │ │ │ ├── ForgotPassword.vue │ │ │ ├── Login.vue │ │ │ ├── Register.vue │ │ │ ├── ResetPassword.vue │ │ │ ├── TwoFactorChallenge.vue │ │ │ └── VerifyEmail.vue │ ├── router │ │ └── index.js │ └── stores │ │ └── auth.js ├── markdown │ ├── policy.md │ └── terms.md └── views │ ├── app.blade.php │ └── emails │ └── team-invitation.blade.php ├── routes ├── api.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── AuthenticationTest.php │ ├── EmailVerificationTest.php │ ├── ExampleTest.php │ ├── PasswordConfirmationTest.php │ ├── PasswordResetTest.php │ ├── ProfileInformationTest.php │ ├── RegistrationTest.php │ ├── TwoFactorAuthenticationTest.php │ └── UpdatePasswordTest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── vite.config.js /.chipperci.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | environment: 4 | php: 8.3 5 | node: 16 6 | 7 | pipeline: 8 | - name: Setup 9 | cmd: | 10 | cp -v .env.testing .env 11 | composer install --no-interaction --prefer-dist --optimize-autoloader 12 | php artisan key:generate 13 | mkdir -p database 14 | touch database/database.sqlite 15 | php artisan migrate 16 | 17 | - name: Compile Assets 18 | cmd: | 19 | npm install 20 | npm run build 21 | 22 | - name: Run Tests 23 | cmd: phpunit 24 | -------------------------------------------------------------------------------- /.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,vue,js}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Vue Laravel SPA" 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://vue-laravel-spa.test 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | APP_MAINTENANCE_STORE=database 14 | 15 | BCRYPT_ROUNDS=12 16 | 17 | LOG_CHANNEL=stack 18 | LOG_STACK=single 19 | LOG_DEPRECATIONS_CHANNEL=null 20 | LOG_LEVEL=debug 21 | 22 | DB_CONNECTION=sqlite 23 | # DB_HOST=127.0.0.1 24 | # DB_PORT=3306 25 | # DB_DATABASE=laravel 26 | # DB_USERNAME=root 27 | # DB_PASSWORD= 28 | 29 | SESSION_DRIVER=database 30 | SESSION_LIFETIME=120 31 | SESSION_ENCRYPT=false 32 | SESSION_PATH=/ 33 | SESSION_DOMAIN=null 34 | 35 | BROADCAST_CONNECTION=log 36 | FILESYSTEM_DISK=local 37 | QUEUE_CONNECTION=database 38 | 39 | CACHE_STORE=database 40 | CACHE_PREFIX= 41 | 42 | MEMCACHED_HOST=127.0.0.1 43 | 44 | REDIS_CLIENT=phpredis 45 | REDIS_HOST=127.0.0.1 46 | REDIS_PASSWORD=null 47 | REDIS_PORT=6379 48 | 49 | MAIL_MAILER=log 50 | MAIL_HOST=127.0.0.1 51 | MAIL_PORT=2525 52 | MAIL_USERNAME=null 53 | MAIL_PASSWORD=null 54 | MAIL_ENCRYPTION=null 55 | MAIL_FROM_ADDRESS="hello@example.com" 56 | MAIL_FROM_NAME="${APP_NAME}" 57 | 58 | AWS_ACCESS_KEY_ID= 59 | AWS_SECRET_ACCESS_KEY= 60 | AWS_DEFAULT_REGION=us-east-1 61 | AWS_BUCKET= 62 | AWS_USE_PATH_STYLE_ENDPOINT=false 63 | 64 | VITE_APP_NAME="${APP_NAME}" 65 | -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- 1 | APP_NAME="Vue Laravel SPA" 2 | APP_ENV=local 3 | APP_KEY=base64:LVFvXoJkxD20eZcneq/+ERpmLaRwRnfebih2PCxp5qo= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=sqlite 12 | BROADCAST_DRIVER=log 13 | CACHE_DRIVER=file 14 | FILESYSTEM_DISK=local 15 | QUEUE_CONNECTION=sync 16 | SESSION_DRIVER=file 17 | SESSION_LIFETIME=120 18 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.phpunit.cache 3 | /node_modules 4 | /public/build 5 | /public/hot 6 | /public/storage 7 | /storage/*.key 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpunit.result.cache 13 | Homestead.json 14 | Homestead.yaml 15 | auth.json 16 | npm-debug.log 17 | yarn-error.log 18 | /.fleet 19 | /.idea 20 | /.vscode 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Build Status ChipperCI 3 | Build Status GitHub 4 |

5 | 6 | ![art/screenshot-login.png](art/screenshot-login.png) 7 | 8 | ## Vue Laravel SPA 9 | 10 | A simple and clean starter-kit to start a new SPA project. Its like Jetstream without Inertia but with Vue-Router and Pinia as store. 11 | 12 | This starter-kit uses the following tools: 13 | 14 | - [Vue 3](https://github.com/vuejs/vue) 15 | - [Vue Router](https://router.vuejs.org/) 16 | - [Pinia](https://pinia.vuejs.org/) 17 | - [Axios](https://axios-http.com/docs/intro) 18 | - [Tailwindcss 3.x](https://tailwindcss.com/) 19 | - [Laravel 11.x](https://laravel.com/docs/11.x) 20 | - [Laravel Sanctum](https://laravel.com/docs/11.x/sanctum) 21 | - [Laravel Fortify](https://laravel.com/docs/11.x/fortify) 22 | 23 | ## Features 24 | 25 | The following features are implemented in this Vue SPA: 26 | 27 | - ✅ Authentication (Cookie based by sanctum) 28 | - ✅ Passwort Reset 29 | - ✅ Registration 30 | - ✅ Profile Management 31 | - ✅ Password Confirmation 32 | - ✅ Two Factor Authentication 33 | - ✅ E-Mail Verification 34 | 35 | ## Roadmap 36 | 37 | Missing something? Feel free to tell me what would be a nice addition to this starter-kit. 38 | 39 | - 🔘 Darkmode 40 | - 🔘 Better error handling on UI 41 | - 🔘 Password confirmation fixes 42 | - 🔘 Example page with form and api controller 43 | - 🔘 Laravel Pint 44 | - 🔘 PWA 45 | - 🔘 More Tests 46 | - 🔘 CSP 47 | - 🔘 Replace PHPUnit with Pest 48 | 49 | ## Getting started 50 | 51 | You can quick start with this starter-kit by running the **`bin/install.sh`** script or manually run the containing commands. 52 | 53 | After that you can login with `test@example.com` and the password `password` 54 | 55 | Make sure to set your current domain in the .env file: 56 | 57 | ``` 58 | APP_URL=http://vue-laravel-spa.test 59 | ``` 60 | 61 | ## Screenshots 62 | 63 | A Picture Is Worth More Than A Thousand Words. 64 | 65 | ![art/screenshot-home.png](art/screenshot-home.png) 66 | 67 | ![art/screenshot-settings.png](art/screenshot-settings.png) 68 | 69 | Consider to support my work and say thank you with a coffee. I would very much appreciate that. 70 | 71 | ![art/bmc_qr.jpg](art/bmc_qr.jpg) 72 | 73 | [buymeacoffee.com/tobiasschulz](https://www.buymeacoffee.com/tobiasschulz) 74 | 75 | ## Testing 76 | 77 | PHPunit is ready setup to test the API side. Tested are all Sanctum and Fortify features cause there are heavily based on there original tests. Thats a good starting point to add tests for your next project. To run the tests you can call phpunit like this: 78 | 79 | ```bash 80 | php artisan test 81 | ``` 82 | 83 | ## Contributing 84 | 85 | Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). 86 | 87 | ## Code of Conduct 88 | 89 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 90 | 91 | ## Security Vulnerabilities 92 | 93 | If you discover a security vulnerability within Laravel, please send an e-mail via [tobias@byte.software](mailto:tobias@byte.software). All security vulnerabilities will be promptly addressed. 94 | 95 | ## License 96 | 97 | The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 98 | The Vue framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 99 | This repository is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 100 | -------------------------------------------------------------------------------- /app/Actions/Fortify/CreateNewUser.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function create(array $input): User 20 | { 21 | Validator::make($input, [ 22 | 'name' => ['required', 'string', 'max:255'], 23 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 24 | 'password' => $this->passwordRules(), 25 | ])->validate(); 26 | 27 | return User::create([ 28 | 'name' => $input['name'], 29 | 'email' => $input['email'], 30 | 'password' => Hash::make($input['password']), 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Actions/Fortify/PasswordValidationRules.php: -------------------------------------------------------------------------------- 1 | |string> 13 | */ 14 | protected function passwordRules(): array 15 | { 16 | return ['required', 'string', Password::default(), 'confirmed']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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', 'current_password:web'], 23 | 'password' => $this->passwordRules(), 24 | ], [ 25 | 'current_password.current_password' => __('The provided password does not match your current password.'), 26 | ])->validateWithBag('updatePassword'); 27 | 28 | $user->forceFill([ 29 | 'password' => Hash::make($input['password']), 30 | ])->save(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserProfileInformation.php: -------------------------------------------------------------------------------- 1 | $input 17 | */ 18 | public function update(User $user, array $input): void 19 | { 20 | Validator::make($input, [ 21 | 'name' => ['required', 'string', 'max:255'], 22 | 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], 23 | 'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'], 24 | ])->validateWithBag('updateProfileInformation'); 25 | 26 | if (isset($input['photo'])) { 27 | $user->updateProfilePhoto($input['photo']); 28 | } 29 | 30 | if ($input['email'] !== $user->email && 31 | $user instanceof MustVerifyEmail) { 32 | $this->updateVerifiedUser($user, $input); 33 | } else { 34 | $user->forceFill([ 35 | 'name' => $input['name'], 36 | 'email' => $input['email'], 37 | ])->save(); 38 | } 39 | } 40 | 41 | /** 42 | * Update the given verified user's profile information. 43 | * 44 | * @param array $input 45 | */ 46 | protected function updateVerifiedUser(User $user, array $input): void 47 | { 48 | $user->forceFill([ 49 | 'name' => $input['name'], 50 | 'email' => $input['email'], 51 | 'email_verified_at' => null, 52 | ])->save(); 53 | 54 | $user->sendEmailVerificationNotification(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/User/MeController.php: -------------------------------------------------------------------------------- 1 | user()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | 'email' => $this->email, 21 | 'email_verified_at' => $this->email_verified_at, 22 | 'created_at' => $this->created_at, 23 | 'updated_at' => $this->updated_at, 24 | 'two_factor' => $this->hasEnabledTwoFactorAuthentication(), // required in Vue for TwoFactoreAuthentication. 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected $fillable = [ 25 | 'name', 26 | 'email', 27 | 'password', 28 | ]; 29 | 30 | /** 31 | * The attributes that should be hidden for serialization. 32 | * 33 | * @var array 34 | */ 35 | protected $hidden = [ 36 | 'password', 37 | 'remember_token', 38 | 'two_factor_recovery_codes', 39 | 'two_factor_secret', 40 | ]; 41 | 42 | /** 43 | * The accessors to append to the model's array form. 44 | * 45 | * @var array 46 | */ 47 | protected $appends = [ 48 | 'profile_photo_url', 49 | ]; 50 | 51 | /** 52 | * Get the attributes that should be cast. 53 | * 54 | * @return array 55 | */ 56 | protected function casts(): array 57 | { 58 | return [ 59 | 'email_verified_at' => 'datetime', 60 | 'password' => 'hashed', 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Policies/TeamPolicy.php: -------------------------------------------------------------------------------- 1 | belongsToTeam($team); 27 | } 28 | 29 | /** 30 | * Determine whether the user can create models. 31 | */ 32 | public function create(User $user): bool 33 | { 34 | return true; 35 | } 36 | 37 | /** 38 | * Determine whether the user can update the model. 39 | */ 40 | public function update(User $user, Team $team): bool 41 | { 42 | return $user->ownsTeam($team); 43 | } 44 | 45 | /** 46 | * Determine whether the user can add team members. 47 | */ 48 | public function addTeamMember(User $user, Team $team): bool 49 | { 50 | return $user->ownsTeam($team); 51 | } 52 | 53 | /** 54 | * Determine whether the user can update team member permissions. 55 | */ 56 | public function updateTeamMember(User $user, Team $team): bool 57 | { 58 | return $user->ownsTeam($team); 59 | } 60 | 61 | /** 62 | * Determine whether the user can remove team members. 63 | */ 64 | public function removeTeamMember(User $user, Team $team): bool 65 | { 66 | return $user->ownsTeam($team); 67 | } 68 | 69 | /** 70 | * Determine whether the user can delete the model. 71 | */ 72 | public function delete(User $user, Team $team): bool 73 | { 74 | return $user->ownsTeam($team); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | input(Fortify::username())).'|'.$request->ip()); 38 | 39 | return Limit::perMinute(5)->by($throttleKey); 40 | }); 41 | 42 | RateLimiter::for('two-factor', function (Request $request) { 43 | return Limit::perMinute(5)->by($request->session()->get('login.id')); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /art/bmc_qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobischulz/vue-laravel-spa/8b00b166691bfaae7ca1d5d5bfcdd209078f0471/art/bmc_qr.jpg -------------------------------------------------------------------------------- /art/screenshot-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobischulz/vue-laravel-spa/8b00b166691bfaae7ca1d5d5bfcdd209078f0471/art/screenshot-home.png -------------------------------------------------------------------------------- /art/screenshot-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobischulz/vue-laravel-spa/8b00b166691bfaae7ca1d5d5bfcdd209078f0471/art/screenshot-login.png -------------------------------------------------------------------------------- /art/screenshot-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobischulz/vue-laravel-spa/8b00b166691bfaae7ca1d5d5bfcdd209078f0471/art/screenshot-settings.png -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bin/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f composer.json ]; then 4 | echo "Please make sure to run this script from the root directory of this repo." 5 | exit 1 6 | fi 7 | 8 | cp -v .env.example .env 9 | composer install --no-interaction 10 | php artisan key:generate 11 | touch database/database.sqlite 12 | php artisan migrate --seed 13 | npm install 14 | npm run build 15 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 9 | web: __DIR__.'/../routes/web.php', 10 | api: __DIR__.'/../routes/api.php', 11 | commands: __DIR__.'/../routes/console.php', 12 | health: '/up', 13 | ) 14 | ->withMiddleware(function (Middleware $middleware) { 15 | $middleware->web(append: [ 16 | \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, 17 | ])->statefulApi(); 18 | 19 | // 20 | }) 21 | ->withExceptions(function (Exceptions $exceptions) { 22 | // 23 | })->create(); 24 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Environment 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "environment" your application is currently 24 | | running in. This may determine how you prefer to configure various 25 | | services the application utilizes. Set this in your ".env" file. 26 | | 27 | */ 28 | 29 | 'env' => env('APP_ENV', 'production'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Debug Mode 34 | |-------------------------------------------------------------------------- 35 | | 36 | | When your application is in debug mode, detailed error messages with 37 | | stack traces will be shown on every error that occurs within your 38 | | application. If disabled, a simple generic error page is shown. 39 | | 40 | */ 41 | 42 | 'debug' => (bool) env('APP_DEBUG', false), 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Application URL 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This URL is used by the console to properly generate URLs when using 50 | | the Artisan command line tool. You should set this to the root of 51 | | the application so that it's available within Artisan commands. 52 | | 53 | */ 54 | 55 | 'url' => env('APP_URL', 'http://localhost'), 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Application Timezone 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Here you may specify the default timezone for your application, which 63 | | will be used by the PHP date and date-time functions. The timezone 64 | | is set to "UTC" by default as it is suitable for most use cases. 65 | | 66 | */ 67 | 68 | 'timezone' => env('APP_TIMEZONE', 'UTC'), 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Application Locale Configuration 73 | |-------------------------------------------------------------------------- 74 | | 75 | | The application locale determines the default locale that will be used 76 | | by Laravel's translation / localization methods. This option can be 77 | | set to any locale for which you plan to have translation strings. 78 | | 79 | */ 80 | 81 | 'locale' => env('APP_LOCALE', 'en'), 82 | 83 | 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), 84 | 85 | 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Encryption Key 90 | |-------------------------------------------------------------------------- 91 | | 92 | | This key is utilized by Laravel's encryption services and should be set 93 | | to a random, 32 character string to ensure that all encrypted values 94 | | are secure. You should do this prior to deploying the application. 95 | | 96 | */ 97 | 98 | 'cipher' => 'AES-256-CBC', 99 | 100 | 'key' => env('APP_KEY'), 101 | 102 | 'previous_keys' => [ 103 | ...array_filter( 104 | explode(',', env('APP_PREVIOUS_KEYS', '')) 105 | ), 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Maintenance Mode Driver 111 | |-------------------------------------------------------------------------- 112 | | 113 | | These configuration options determine the driver used to determine and 114 | | manage Laravel's "maintenance mode" status. The "cache" driver will 115 | | allow maintenance mode to be controlled across multiple machines. 116 | | 117 | | Supported drivers: "file", "cache" 118 | | 119 | */ 120 | 121 | 'maintenance' => [ 122 | 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 123 | 'store' => env('APP_MAINTENANCE_STORE', 'database'), 124 | ], 125 | 126 | ]; 127 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => env('AUTH_GUARD', 'web'), 18 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | which utilizes session storage plus the Eloquent user provider. 29 | | 30 | | All authentication guards have a user provider, which defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | system used by the application. Typically, Eloquent is utilized. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication guards have a user provider, which defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | system used by the application. Typically, Eloquent is utilized. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | providers to represent the model / table. These providers may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'eloquent', 65 | 'model' => env('AUTH_MODEL', App\Models\User::class), 66 | ], 67 | 68 | // 'users' => [ 69 | // 'driver' => 'database', 70 | // 'table' => 'users', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Resetting Passwords 77 | |-------------------------------------------------------------------------- 78 | | 79 | | These configuration options specify the behavior of Laravel's password 80 | | reset functionality, including the table utilized for token storage 81 | | and the user provider that is invoked to actually retrieve users. 82 | | 83 | | The expiry time is the number of minutes that each reset token will be 84 | | considered valid. This security feature keeps tokens short-lived so 85 | | they have less time to be guessed. You may change this as needed. 86 | | 87 | | The throttle setting is the number of seconds a user must wait before 88 | | generating more password reset tokens. This prevents the user from 89 | | quickly generating a very large amount of password reset tokens. 90 | | 91 | */ 92 | 93 | 'passwords' => [ 94 | 'users' => [ 95 | 'provider' => 'users', 96 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 97 | 'expire' => 60, 98 | 'throttle' => 60, 99 | ], 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Password Confirmation Timeout 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Here you may define the amount of seconds before a password confirmation 108 | | window expires and users are asked to re-enter their password via the 109 | | confirmation screen. By default, the timeout lasts for three hours. 110 | | 111 | */ 112 | 113 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 114 | 115 | ]; 116 | -------------------------------------------------------------------------------- /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: "apc", "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 | 'table' => env('DB_CACHE_TABLE', 'cache'), 44 | 'connection' => env('DB_CACHE_CONNECTION'), 45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 46 | ], 47 | 48 | 'file' => [ 49 | 'driver' => 'file', 50 | 'path' => storage_path('framework/cache/data'), 51 | 'lock_path' => storage_path('framework/cache/data'), 52 | ], 53 | 54 | 'memcached' => [ 55 | 'driver' => 'memcached', 56 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 57 | 'sasl' => [ 58 | env('MEMCACHED_USERNAME'), 59 | env('MEMCACHED_PASSWORD'), 60 | ], 61 | 'options' => [ 62 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 63 | ], 64 | 'servers' => [ 65 | [ 66 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 67 | 'port' => env('MEMCACHED_PORT', 11211), 68 | 'weight' => 100, 69 | ], 70 | ], 71 | ], 72 | 73 | 'redis' => [ 74 | 'driver' => 'redis', 75 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 76 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 77 | ], 78 | 79 | 'dynamodb' => [ 80 | 'driver' => 'dynamodb', 81 | 'key' => env('AWS_ACCESS_KEY_ID'), 82 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 83 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 84 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 85 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 86 | ], 87 | 88 | 'octane' => [ 89 | 'driver' => 'octane', 90 | ], 91 | 92 | ], 93 | 94 | /* 95 | |-------------------------------------------------------------------------- 96 | | Cache Key Prefix 97 | |-------------------------------------------------------------------------- 98 | | 99 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 100 | | stores, there might be other applications using the same cache. For 101 | | that reason, you may prefix every cache key to avoid collisions. 102 | | 103 | */ 104 | 105 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 106 | 107 | ]; 108 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => true, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'sqlite'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Database Connections 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Below are all of the database connections defined for your application. 27 | | An example configuration is provided for each database system which 28 | | is supported by Laravel. You're free to add / remove connections. 29 | | 30 | */ 31 | 32 | 'connections' => [ 33 | 34 | 'sqlite' => [ 35 | 'driver' => 'sqlite', 36 | 'url' => env('DB_URL'), 37 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 38 | 'prefix' => '', 39 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 40 | ], 41 | 42 | 'mysql' => [ 43 | 'driver' => 'mysql', 44 | 'url' => env('DB_URL'), 45 | 'host' => env('DB_HOST', '127.0.0.1'), 46 | 'port' => env('DB_PORT', '3306'), 47 | 'database' => env('DB_DATABASE', 'laravel'), 48 | 'username' => env('DB_USERNAME', 'root'), 49 | 'password' => env('DB_PASSWORD', ''), 50 | 'unix_socket' => env('DB_SOCKET', ''), 51 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 52 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 53 | 'prefix' => '', 54 | 'prefix_indexes' => true, 55 | 'strict' => true, 56 | 'engine' => null, 57 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 58 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 59 | ]) : [], 60 | ], 61 | 62 | 'mariadb' => [ 63 | 'driver' => 'mariadb', 64 | 'url' => env('DB_URL'), 65 | 'host' => env('DB_HOST', '127.0.0.1'), 66 | 'port' => env('DB_PORT', '3306'), 67 | 'database' => env('DB_DATABASE', 'laravel'), 68 | 'username' => env('DB_USERNAME', 'root'), 69 | 'password' => env('DB_PASSWORD', ''), 70 | 'unix_socket' => env('DB_SOCKET', ''), 71 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 72 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 73 | 'prefix' => '', 74 | 'prefix_indexes' => true, 75 | 'strict' => true, 76 | 'engine' => null, 77 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 78 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 79 | ]) : [], 80 | ], 81 | 82 | 'pgsql' => [ 83 | 'driver' => 'pgsql', 84 | 'url' => env('DB_URL'), 85 | 'host' => env('DB_HOST', '127.0.0.1'), 86 | 'port' => env('DB_PORT', '5432'), 87 | 'database' => env('DB_DATABASE', 'laravel'), 88 | 'username' => env('DB_USERNAME', 'root'), 89 | 'password' => env('DB_PASSWORD', ''), 90 | 'charset' => env('DB_CHARSET', 'utf8'), 91 | 'prefix' => '', 92 | 'prefix_indexes' => true, 93 | 'search_path' => 'public', 94 | 'sslmode' => 'prefer', 95 | ], 96 | 97 | 'sqlsrv' => [ 98 | 'driver' => 'sqlsrv', 99 | 'url' => env('DB_URL'), 100 | 'host' => env('DB_HOST', 'localhost'), 101 | 'port' => env('DB_PORT', '1433'), 102 | 'database' => env('DB_DATABASE', 'laravel'), 103 | 'username' => env('DB_USERNAME', 'root'), 104 | 'password' => env('DB_PASSWORD', ''), 105 | 'charset' => env('DB_CHARSET', 'utf8'), 106 | 'prefix' => '', 107 | 'prefix_indexes' => true, 108 | // 'encrypt' => env('DB_ENCRYPT', 'yes'), 109 | // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), 110 | ], 111 | 112 | ], 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Migration Repository Table 117 | |-------------------------------------------------------------------------- 118 | | 119 | | This table keeps track of all the migrations that have already run for 120 | | your application. Using this information, we can determine which of 121 | | the migrations on disk haven't actually been run on the database. 122 | | 123 | */ 124 | 125 | 'migrations' => [ 126 | 'table' => 'migrations', 127 | 'update_date_on_publish' => true, 128 | ], 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | Redis Databases 133 | |-------------------------------------------------------------------------- 134 | | 135 | | Redis is an open source, fast, and advanced key-value store that also 136 | | provides a richer body of commands than a typical key-value system 137 | | such as Memcached. You may define your connection settings here. 138 | | 139 | */ 140 | 141 | 'redis' => [ 142 | 143 | 'client' => env('REDIS_CLIENT', 'phpredis'), 144 | 145 | 'options' => [ 146 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 147 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 148 | ], 149 | 150 | 'default' => [ 151 | 'url' => env('REDIS_URL'), 152 | 'host' => env('REDIS_HOST', '127.0.0.1'), 153 | 'username' => env('REDIS_USERNAME'), 154 | 'password' => env('REDIS_PASSWORD'), 155 | 'port' => env('REDIS_PORT', '6379'), 156 | 'database' => env('REDIS_DB', '0'), 157 | ], 158 | 159 | 'cache' => [ 160 | 'url' => env('REDIS_URL'), 161 | 'host' => env('REDIS_HOST', '127.0.0.1'), 162 | 'username' => env('REDIS_USERNAME'), 163 | 'password' => env('REDIS_PASSWORD'), 164 | 'port' => env('REDIS_PORT', '6379'), 165 | 'database' => env('REDIS_CACHE_DB', '1'), 166 | ], 167 | 168 | ], 169 | 170 | ]; 171 | -------------------------------------------------------------------------------- /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'), 36 | 'throw' => false, 37 | ], 38 | 39 | 'public' => [ 40 | 'driver' => 'local', 41 | 'root' => storage_path('app/public'), 42 | 'url' => env('APP_URL').'/storage', 43 | 'visibility' => 'public', 44 | 'throw' => false, 45 | ], 46 | 47 | 's3' => [ 48 | 'driver' => 's3', 49 | 'key' => env('AWS_ACCESS_KEY_ID'), 50 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 51 | 'region' => env('AWS_DEFAULT_REGION'), 52 | 'bucket' => env('AWS_BUCKET'), 53 | 'url' => env('AWS_URL'), 54 | 'endpoint' => env('AWS_ENDPOINT'), 55 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 56 | 'throw' => false, 57 | ], 58 | 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Symbolic Links 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Here you may configure the symbolic links that will be created when the 67 | | `storage:link` Artisan command is executed. The array keys should be 68 | | the locations of the links and the values should be their targets. 69 | | 70 | */ 71 | 72 | 'links' => [ 73 | public_path('storage') => storage_path('app/public'), 74 | ], 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /config/fortify.php: -------------------------------------------------------------------------------- 1 | 'web', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Fortify Password Broker 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify which password broker Fortify can use when a user 26 | | is resetting their password. This configured value should match one 27 | | of your password brokers setup in your "auth" configuration file. 28 | | 29 | */ 30 | 31 | 'passwords' => 'users', 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Username / Email 36 | |-------------------------------------------------------------------------- 37 | | 38 | | This value defines which model attribute should be considered as your 39 | | application's "username" field. Typically, this might be the email 40 | | address of the users but you are free to change this value here. 41 | | 42 | | Out of the box, Fortify expects forgot password and reset password 43 | | requests to have a field named 'email'. If the application uses 44 | | another name for the field you may define it below as needed. 45 | | 46 | */ 47 | 48 | 'username' => 'email', 49 | 50 | 'email' => 'email', 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Lowercase Usernames 55 | |-------------------------------------------------------------------------- 56 | | 57 | | This value defines whether usernames should be lowercased before saving 58 | | them in the database, as some database system string fields are case 59 | | sensitive. You may disable this for your application if necessary. 60 | | 61 | */ 62 | 63 | 'lowercase_usernames' => true, 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Home Path 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Here you may configure the path where users will get redirected during 71 | | authentication or password reset when the operations are successful 72 | | and the user is authenticated. You are free to change this value. 73 | | 74 | */ 75 | 76 | 'home' => '/', 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Fortify Routes Prefix / Subdomain 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Here you may specify which prefix Fortify will assign to all the routes 84 | | that it registers with the application. If necessary, you may change 85 | | subdomain under which all of the Fortify routes will be available. 86 | | 87 | */ 88 | 89 | 'prefix' => '', 90 | 91 | 'domain' => null, 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Fortify Routes Middleware 96 | |-------------------------------------------------------------------------- 97 | | 98 | | Here you may specify which middleware Fortify will assign to the routes 99 | | that it registers with the application. If necessary, you may change 100 | | these middleware but typically this provided default is preferred. 101 | | 102 | */ 103 | 104 | 'middleware' => ['web'], 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Rate Limiting 109 | |-------------------------------------------------------------------------- 110 | | 111 | | By default, Fortify will throttle logins to five requests per minute for 112 | | every email and IP address combination. However, if you would like to 113 | | specify a custom rate limiter to call then you may specify it here. 114 | | 115 | */ 116 | 117 | 'limiters' => [ 118 | 'login' => 'login', 119 | 'two-factor' => 'two-factor', 120 | ], 121 | 122 | /* 123 | |-------------------------------------------------------------------------- 124 | | Register View Routes 125 | |-------------------------------------------------------------------------- 126 | | 127 | | Here you may specify if the routes returning views should be disabled as 128 | | you may not need them when building your own application. This may be 129 | | especially true if you're writing a custom single-page application. 130 | | 131 | */ 132 | 133 | 'views' => false, 134 | 135 | /* 136 | |-------------------------------------------------------------------------- 137 | | Features 138 | |-------------------------------------------------------------------------- 139 | | 140 | | Some of the Fortify features are optional. You may disable the features 141 | | by removing them from this array. You're free to only remove some of 142 | | these features or you can even remove all of these if you need to. 143 | | 144 | */ 145 | 146 | 'features' => [ 147 | Features::registration(), 148 | Features::resetPasswords(), 149 | Features::emailVerification(), 150 | Features::updateProfileInformation(), 151 | Features::updatePasswords(), 152 | Features::twoFactorAuthentication([ 153 | 'confirm' => true, 154 | 'confirmPassword' => true, 155 | // 'window' => 0, 156 | ]), 157 | ], 158 | 159 | ]; 160 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Deprecations Log Channel 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This option controls the log channel that should be used to log warnings 29 | | regarding deprecated PHP and library features. This allows you to get 30 | | your application ready for upcoming major versions of dependencies. 31 | | 32 | */ 33 | 34 | 'deprecations' => [ 35 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 36 | 'trace' => env('LOG_DEPRECATIONS_TRACE', false), 37 | ], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Log Channels 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Here you may configure the log channels for your application. Laravel 45 | | utilizes the Monolog PHP logging library, which includes a variety 46 | | of powerful log handlers and formatters that you're free to use. 47 | | 48 | | Available Drivers: "single", "daily", "slack", "syslog", 49 | | "errorlog", "monolog", "custom", "stack" 50 | | 51 | */ 52 | 53 | 'channels' => [ 54 | 55 | 'stack' => [ 56 | 'driver' => 'stack', 57 | 'channels' => explode(',', env('LOG_STACK', 'single')), 58 | 'ignore_exceptions' => false, 59 | ], 60 | 61 | 'single' => [ 62 | 'driver' => 'single', 63 | 'path' => storage_path('logs/laravel.log'), 64 | 'level' => env('LOG_LEVEL', 'debug'), 65 | 'replace_placeholders' => true, 66 | ], 67 | 68 | 'daily' => [ 69 | 'driver' => 'daily', 70 | 'path' => storage_path('logs/laravel.log'), 71 | 'level' => env('LOG_LEVEL', 'debug'), 72 | 'days' => env('LOG_DAILY_DAYS', 14), 73 | 'replace_placeholders' => true, 74 | ], 75 | 76 | 'slack' => [ 77 | 'driver' => 'slack', 78 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 79 | 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), 80 | 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 81 | 'level' => env('LOG_LEVEL', 'critical'), 82 | 'replace_placeholders' => true, 83 | ], 84 | 85 | 'papertrail' => [ 86 | 'driver' => 'monolog', 87 | 'level' => env('LOG_LEVEL', 'debug'), 88 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 89 | 'handler_with' => [ 90 | 'host' => env('PAPERTRAIL_URL'), 91 | 'port' => env('PAPERTRAIL_PORT'), 92 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), 93 | ], 94 | 'processors' => [PsrLogMessageProcessor::class], 95 | ], 96 | 97 | 'stderr' => [ 98 | 'driver' => 'monolog', 99 | 'level' => env('LOG_LEVEL', 'debug'), 100 | 'handler' => StreamHandler::class, 101 | 'formatter' => env('LOG_STDERR_FORMATTER'), 102 | 'with' => [ 103 | 'stream' => 'php://stderr', 104 | ], 105 | 'processors' => [PsrLogMessageProcessor::class], 106 | ], 107 | 108 | 'syslog' => [ 109 | 'driver' => 'syslog', 110 | 'level' => env('LOG_LEVEL', 'debug'), 111 | 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 112 | 'replace_placeholders' => true, 113 | ], 114 | 115 | 'errorlog' => [ 116 | 'driver' => 'errorlog', 117 | 'level' => env('LOG_LEVEL', 'debug'), 118 | 'replace_placeholders' => true, 119 | ], 120 | 121 | 'null' => [ 122 | 'driver' => 'monolog', 123 | 'handler' => NullHandler::class, 124 | ], 125 | 126 | 'emergency' => [ 127 | 'path' => storage_path('logs/laravel.log'), 128 | ], 129 | 130 | ], 131 | 132 | ]; 133 | -------------------------------------------------------------------------------- /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", "log", "array", "failover", "roundrobin" 34 | | 35 | */ 36 | 37 | 'mailers' => [ 38 | 39 | 'smtp' => [ 40 | 'transport' => 'smtp', 41 | 'url' => env('MAIL_URL'), 42 | 'host' => env('MAIL_HOST', '127.0.0.1'), 43 | 'port' => env('MAIL_PORT', 2525), 44 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 45 | 'username' => env('MAIL_USERNAME'), 46 | 'password' => env('MAIL_PASSWORD'), 47 | 'timeout' => null, 48 | 'local_domain' => env('MAIL_EHLO_DOMAIN'), 49 | ], 50 | 51 | 'ses' => [ 52 | 'transport' => 'ses', 53 | ], 54 | 55 | 'postmark' => [ 56 | 'transport' => 'postmark', 57 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 58 | // 'client' => [ 59 | // 'timeout' => 5, 60 | // ], 61 | ], 62 | 63 | 'sendmail' => [ 64 | 'transport' => 'sendmail', 65 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 66 | ], 67 | 68 | 'log' => [ 69 | 'transport' => 'log', 70 | 'channel' => env('MAIL_LOG_CHANNEL'), 71 | ], 72 | 73 | 'array' => [ 74 | 'transport' => 'array', 75 | ], 76 | 77 | 'failover' => [ 78 | 'transport' => 'failover', 79 | 'mailers' => [ 80 | 'smtp', 81 | 'log', 82 | ], 83 | ], 84 | 85 | 'roundrobin' => [ 86 | 'transport' => 'roundrobin', 87 | 'mailers' => [ 88 | 'ses', 89 | 'postmark', 90 | ], 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Global "From" Address 98 | |-------------------------------------------------------------------------- 99 | | 100 | | You may wish for all emails sent by your application to be sent from 101 | | the same address. Here you may specify a name and address that is 102 | | used globally for all emails that are sent by your application. 103 | | 104 | */ 105 | 106 | 'from' => [ 107 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 108 | 'name' => env('MAIL_FROM_NAME', 'Example'), 109 | ], 110 | 111 | ]; 112 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'database'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection options for every queue backend 24 | | used by your application. An example configuration is provided for 25 | | each backend supported by Laravel. You're also free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'connection' => env('DB_QUEUE_CONNECTION'), 40 | 'table' => env('DB_QUEUE_TABLE', 'jobs'), 41 | 'queue' => env('DB_QUEUE', 'default'), 42 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 43 | 'after_commit' => false, 44 | ], 45 | 46 | 'beanstalkd' => [ 47 | 'driver' => 'beanstalkd', 48 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), 49 | 'queue' => env('BEANSTALKD_QUEUE', 'default'), 50 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 51 | 'block_for' => 0, 52 | 'after_commit' => false, 53 | ], 54 | 55 | 'sqs' => [ 56 | 'driver' => 'sqs', 57 | 'key' => env('AWS_ACCESS_KEY_ID'), 58 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 59 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 60 | 'queue' => env('SQS_QUEUE', 'default'), 61 | 'suffix' => env('SQS_SUFFIX'), 62 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 63 | 'after_commit' => false, 64 | ], 65 | 66 | 'redis' => [ 67 | 'driver' => 'redis', 68 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 69 | 'queue' => env('REDIS_QUEUE', 'default'), 70 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 71 | 'block_for' => null, 72 | 'after_commit' => false, 73 | ], 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Job Batching 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The following options configure the database and table that store job 83 | | batching information. These options can be updated to any database 84 | | connection and table which has been defined by your application. 85 | | 86 | */ 87 | 88 | 'batching' => [ 89 | 'database' => env('DB_CONNECTION', 'sqlite'), 90 | 'table' => 'job_batches', 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Failed Queue Jobs 96 | |-------------------------------------------------------------------------- 97 | | 98 | | These options configure the behavior of failed queue job logging so you 99 | | can control how and where failed jobs are stored. Laravel ships with 100 | | support for storing failed jobs in a simple file or in a database. 101 | | 102 | | Supported drivers: "database-uuids", "dynamodb", "file", "null" 103 | | 104 | */ 105 | 106 | 'failed' => [ 107 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 108 | 'database' => env('DB_CONNECTION', 'sqlite'), 109 | 'table' => 'failed_jobs', 110 | ], 111 | 112 | ]; 113 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 19 | '%s%s', 20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 21 | Sanctum::currentApplicationUrlWithPort() 22 | ))), 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Sanctum Guards 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This array contains the authentication guards that will be checked when 30 | | Sanctum is trying to authenticate a request. If none of these guards 31 | | are able to authenticate the request, Sanctum will use the bearer 32 | | token that's present on an incoming request for authentication. 33 | | 34 | */ 35 | 36 | 'guard' => ['web'], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Expiration Minutes 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This value controls the number of minutes until an issued token will be 44 | | considered expired. This will override any values set in the token's 45 | | "expires_at" attribute, but first-party sessions are not affected. 46 | | 47 | */ 48 | 49 | 'expiration' => null, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Token Prefix 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Sanctum can prefix new tokens in order to take advantage of numerous 57 | | security scanning initiatives maintained by open source platforms 58 | | that notify developers if they commit tokens into repositories. 59 | | 60 | | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning 61 | | 62 | */ 63 | 64 | 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Sanctum Middleware 69 | |-------------------------------------------------------------------------- 70 | | 71 | | When authenticating your first-party SPA with Sanctum you may need to 72 | | customize some of the middleware Sanctum uses while processing the 73 | | request. You may change the middleware listed below as required. 74 | | 75 | */ 76 | 77 | 'middleware' => [ 78 | 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 79 | 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, 80 | 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, 81 | ], 82 | 83 | ]; 84 | -------------------------------------------------------------------------------- /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 | 'slack' => [ 28 | 'notifications' => [ 29 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 30 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 31 | ], 32 | ], 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | env('SESSION_DRIVER', 'database'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Session Lifetime 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Here you may specify the number of minutes that you wish the session 29 | | to be allowed to remain idle before it expires. If you want them 30 | | to expire immediately when the browser is closed then you may 31 | | indicate that via the expire_on_close configuration option. 32 | | 33 | */ 34 | 35 | 'lifetime' => env('SESSION_LIFETIME', 120), 36 | 37 | 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Session Encryption 42 | |-------------------------------------------------------------------------- 43 | | 44 | | This option allows you to easily specify that all of your session data 45 | | should be encrypted before it's stored. All encryption is performed 46 | | automatically by Laravel and you may use the session like normal. 47 | | 48 | */ 49 | 50 | 'encrypt' => env('SESSION_ENCRYPT', false), 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Session File Location 55 | |-------------------------------------------------------------------------- 56 | | 57 | | When utilizing the "file" session driver, the session files are placed 58 | | on disk. The default storage location is defined here; however, you 59 | | are free to provide another location where they should be stored. 60 | | 61 | */ 62 | 63 | 'files' => storage_path('framework/sessions'), 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Session Database Connection 68 | |-------------------------------------------------------------------------- 69 | | 70 | | When using the "database" or "redis" session drivers, you may specify a 71 | | connection that should be used to manage these sessions. This should 72 | | correspond to a connection in your database configuration options. 73 | | 74 | */ 75 | 76 | 'connection' => env('SESSION_CONNECTION'), 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Session Database Table 81 | |-------------------------------------------------------------------------- 82 | | 83 | | When using the "database" session driver, you may specify the table to 84 | | be used to store sessions. Of course, a sensible default is defined 85 | | for you; however, you're welcome to change this to another table. 86 | | 87 | */ 88 | 89 | 'table' => env('SESSION_TABLE', 'sessions'), 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Session Cache Store 94 | |-------------------------------------------------------------------------- 95 | | 96 | | When using one of the framework's cache driven session backends, you may 97 | | define the cache store which should be used to store the session data 98 | | between requests. This must match one of your defined cache stores. 99 | | 100 | | Affects: "apc", "dynamodb", "memcached", "redis" 101 | | 102 | */ 103 | 104 | 'store' => env('SESSION_STORE'), 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Session Sweeping Lottery 109 | |-------------------------------------------------------------------------- 110 | | 111 | | Some session drivers must manually sweep their storage location to get 112 | | rid of old sessions from storage. Here are the chances that it will 113 | | happen on a given request. By default, the odds are 2 out of 100. 114 | | 115 | */ 116 | 117 | 'lottery' => [2, 100], 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Session Cookie Name 122 | |-------------------------------------------------------------------------- 123 | | 124 | | Here you may change the name of the session cookie that is created by 125 | | the framework. Typically, you should not need to change this value 126 | | since doing so does not grant a meaningful security improvement. 127 | | 128 | */ 129 | 130 | 'cookie' => env( 131 | 'SESSION_COOKIE', 132 | Str::slug(env('APP_NAME', 'laravel'), '_').'_session' 133 | ), 134 | 135 | /* 136 | |-------------------------------------------------------------------------- 137 | | Session Cookie Path 138 | |-------------------------------------------------------------------------- 139 | | 140 | | The session cookie path determines the path for which the cookie will 141 | | be regarded as available. Typically, this will be the root path of 142 | | your application, but you're free to change this when necessary. 143 | | 144 | */ 145 | 146 | 'path' => env('SESSION_PATH', '/'), 147 | 148 | /* 149 | |-------------------------------------------------------------------------- 150 | | Session Cookie Domain 151 | |-------------------------------------------------------------------------- 152 | | 153 | | This value determines the domain and subdomains the session cookie is 154 | | available to. By default, the cookie will be available to the root 155 | | domain and all subdomains. Typically, this shouldn't be changed. 156 | | 157 | */ 158 | 159 | 'domain' => env('SESSION_DOMAIN'), 160 | 161 | /* 162 | |-------------------------------------------------------------------------- 163 | | HTTPS Only Cookies 164 | |-------------------------------------------------------------------------- 165 | | 166 | | By setting this option to true, session cookies will only be sent back 167 | | to the server if the browser has a HTTPS connection. This will keep 168 | | the cookie from being sent to you when it can't be done securely. 169 | | 170 | */ 171 | 172 | 'secure' => env('SESSION_SECURE_COOKIE'), 173 | 174 | /* 175 | |-------------------------------------------------------------------------- 176 | | HTTP Access Only 177 | |-------------------------------------------------------------------------- 178 | | 179 | | Setting this value to true will prevent JavaScript from accessing the 180 | | value of the cookie and the cookie will only be accessible through 181 | | the HTTP protocol. It's unlikely you should disable this option. 182 | | 183 | */ 184 | 185 | 'http_only' => env('SESSION_HTTP_ONLY', true), 186 | 187 | /* 188 | |-------------------------------------------------------------------------- 189 | | Same-Site Cookies 190 | |-------------------------------------------------------------------------- 191 | | 192 | | This option determines how your cookies behave when cross-site requests 193 | | take place, and can be used to mitigate CSRF attacks. By default, we 194 | | will set this value to "lax" to permit secure cross-site requests. 195 | | 196 | | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value 197 | | 198 | | Supported: "lax", "strict", "none", null 199 | | 200 | */ 201 | 202 | 'same_site' => env('SESSION_SAME_SITE', 'lax'), 203 | 204 | /* 205 | |-------------------------------------------------------------------------- 206 | | Partitioned Cookies 207 | |-------------------------------------------------------------------------- 208 | | 209 | | Setting this value to true will tie the cookie to the top-level site for 210 | | a cross-site context. Partitioned cookies are accepted by the browser 211 | | when flagged "secure" and the Same-Site attribute is set to "none". 212 | | 213 | */ 214 | 215 | 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), 216 | 217 | ]; 218 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/TeamFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class TeamFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition(): array 19 | { 20 | return [ 21 | 'name' => $this->faker->unique()->company(), 22 | 'user_id' => User::factory(), 23 | 'personal_team' => true, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'two_factor_secret' => null, 32 | 'two_factor_recovery_codes' => null, 33 | 'remember_token' => Str::random(10), 34 | 'profile_photo_path' => null, 35 | 'current_team_id' => null, 36 | ]; 37 | } 38 | 39 | /** 40 | * Indicate that the model's email address should be unverified. 41 | */ 42 | public function unverified(): static 43 | { 44 | return $this->state(fn (array $attributes) => [ 45 | 'email_verified_at' => null, 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->foreignId('current_team_id')->nullable(); 22 | $table->string('profile_photo_path', 2048)->nullable(); 23 | $table->timestamps(); 24 | }); 25 | 26 | Schema::create('password_reset_tokens', function (Blueprint $table) { 27 | $table->string('email')->primary(); 28 | $table->string('token'); 29 | $table->timestamp('created_at')->nullable(); 30 | }); 31 | 32 | Schema::create('sessions', function (Blueprint $table) { 33 | $table->string('id')->primary(); 34 | $table->foreignId('user_id')->nullable()->index(); 35 | $table->string('ip_address', 45)->nullable(); 36 | $table->text('user_agent')->nullable(); 37 | $table->longText('payload'); 38 | $table->integer('last_activity')->index(); 39 | }); 40 | } 41 | 42 | /** 43 | * Reverse the migrations. 44 | */ 45 | public function down(): void 46 | { 47 | Schema::dropIfExists('users'); 48 | Schema::dropIfExists('password_reset_tokens'); 49 | Schema::dropIfExists('sessions'); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /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/2024_04_28_125542_add_two_factor_columns_to_users_table.php: -------------------------------------------------------------------------------- 1 | text('two_factor_secret') 17 | ->after('password') 18 | ->nullable(); 19 | 20 | $table->text('two_factor_recovery_codes') 21 | ->after('two_factor_secret') 22 | ->nullable(); 23 | 24 | if (Fortify::confirmsTwoFactorAuthentication()) { 25 | $table->timestamp('two_factor_confirmed_at') 26 | ->after('two_factor_recovery_codes') 27 | ->nullable(); 28 | } 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | */ 35 | public function down(): void 36 | { 37 | Schema::table('users', function (Blueprint $table) { 38 | $table->dropColumn(array_merge([ 39 | 'two_factor_secret', 40 | 'two_factor_recovery_codes', 41 | ], Fortify::confirmsTwoFactorAuthentication() ? [ 42 | 'two_factor_confirmed_at', 43 | ] : [])); 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /database/migrations/2024_04_28_125548_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('tokenable'); 17 | $table->string('name'); 18 | $table->string('token', 64)->unique(); 19 | $table->text('abilities')->nullable(); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('expires_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('personal_access_tokens'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2024_04_28_125548_create_teams_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->index(); 17 | $table->string('name'); 18 | $table->boolean('personal_team'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('teams'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2024_04_28_125549_create_team_user_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('team_id'); 17 | $table->foreignId('user_id'); 18 | $table->string('role')->nullable(); 19 | $table->timestamps(); 20 | 21 | $table->unique(['team_id', 'user_id']); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('team_user'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2024_04_28_125550_create_team_invitations_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('team_id')->constrained()->cascadeOnDelete(); 17 | $table->string('email'); 18 | $table->string('role')->nullable(); 19 | $table->timestamps(); 20 | 21 | $table->unique(['team_id', 'email']); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('team_invitations'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | User::factory()->create([ 19 | 'name' => 'Test User', 20 | 'email' => 'test@example.com', 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": [ 6 | "resources/js/*" 7 | ] 8 | } 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "public" 13 | ] 14 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build" 7 | }, 8 | "devDependencies": { 9 | "@vitejs/plugin-vue": "^5.0.0", 10 | "autoprefixer": "^10.4.16", 11 | "axios": "^1.7.4", 12 | "laravel-vite-plugin": "^1.0", 13 | "postcss": "^8.4.32", 14 | "tailwindcss": "^3.4.0", 15 | "vite": "^5.4", 16 | "vue": "^3.3.13", 17 | "vue-router": "^4.2.5", 18 | "pinia": "^2.1.7" 19 | } 20 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "simplified_null_return": true, 5 | "braces": true 6 | } 7 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /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/tobischulz/vue-laravel-spa/8b00b166691bfaae7ca1d5d5bfcdd209078f0471/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue SPA", 3 | "short_name": "Vue SPA", 4 | "lang": "de-DE", 5 | "start_url": "/", 6 | "display": "standalone" 7 | } 8 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import { useAuthStore } from '@/stores/auth'; 4 | import router from '@/router'; 5 | import App from '@/layouts/App.vue'; 6 | import '../css/app.css'; 7 | import axios from 'axios'; 8 | 9 | window.axios = axios; 10 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 11 | window.axios.defaults.headers.common['Content-Type'] = 'application/json'; 12 | window.axios.defaults.headers.common['Accept'] = 'application/json'; 13 | window.axios.defaults.withCredentials = true; 14 | window.axios.defaults.withXSRFToken = true; 15 | 16 | const pinia = createPinia() 17 | const app = createApp(App) 18 | .use(pinia) 19 | 20 | const userStore = useAuthStore() 21 | userStore.attempt_user() 22 | .catch((error) => { 23 | console.log('Please login.') 24 | }) 25 | .finally(() => { 26 | app.use(router) 27 | .mount('#app'); 28 | }) 29 | -------------------------------------------------------------------------------- /resources/js/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /resources/js/components/Navigation.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 51 | -------------------------------------------------------------------------------- /resources/js/components/user/PasswordUpdate.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 72 | -------------------------------------------------------------------------------- /resources/js/components/user/ProfileUpdate.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 69 | -------------------------------------------------------------------------------- /resources/js/components/user/TwoFactoreAuthentication.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 164 | -------------------------------------------------------------------------------- /resources/js/layouts/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 36 | -------------------------------------------------------------------------------- /resources/js/layouts/Authenticated.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /resources/js/layouts/Guest.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /resources/js/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/pages/User.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 48 | -------------------------------------------------------------------------------- /resources/js/pages/auth/ConfirmPassword.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 52 | -------------------------------------------------------------------------------- /resources/js/pages/auth/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 56 | -------------------------------------------------------------------------------- /resources/js/pages/auth/Login.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 104 | -------------------------------------------------------------------------------- /resources/js/pages/auth/Register.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 107 | -------------------------------------------------------------------------------- /resources/js/pages/auth/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 78 | -------------------------------------------------------------------------------- /resources/js/pages/auth/TwoFactorChallenge.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 67 | -------------------------------------------------------------------------------- /resources/js/pages/auth/VerifyEmail.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 46 | -------------------------------------------------------------------------------- /resources/js/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router" 2 | import { useAuthStore } from '@/stores/auth.js' 3 | 4 | import NotFound from '@/pages/NotFound.vue' 5 | import Guest from '@/layouts/Guest.vue' 6 | import Register from '@/pages/auth/Register.vue' 7 | import Login from '@/pages/auth/Login.vue' 8 | import TwoFactorChallenge from '@/pages/auth/TwoFactorChallenge.vue' 9 | import ForgotPassword from '@/pages/auth/ForgotPassword.vue' 10 | import ResetPassword from '@/pages/auth/ResetPassword.vue' 11 | import VerifyEmail from '@/pages/auth/VerifyEmail.vue' 12 | 13 | import Authenticated from '@/layouts/Authenticated.vue' 14 | import ConfirmPassword from '@/pages/auth/ConfirmPassword.vue' 15 | import Home from '@/pages/Home.vue' 16 | import User from '@/pages/User.vue' 17 | 18 | const router = createRouter({ 19 | history: createWebHistory(), 20 | routes: [ 21 | { 22 | path: '/', 23 | component: Authenticated, 24 | meta: { requiresAuth: true }, 25 | children: [ 26 | { path: "/", name: 'Home', component: Home }, 27 | { path: "/user", name: 'User', component: User }, 28 | { path: "/confirm-password", name: 'ConfirmPassword', component: ConfirmPassword }, 29 | ] 30 | }, 31 | { 32 | path: '/auth', 33 | redirect: "/login", 34 | component: Guest, 35 | meta: { isGuest: true }, 36 | children: [ 37 | { path: "/register", name: 'Register', component: Register }, 38 | { path: "/login", name: 'Login', component: Login }, 39 | { path: "/verify-email", name: 'VerifyEmail', component: VerifyEmail }, 40 | { path: "/two-factor-challenge", name: 'TwoFactorChallenge', component: TwoFactorChallenge }, 41 | { path: "/forgot-password", name: 'ForgotPassword', component: ForgotPassword }, 42 | { path: "/reset-password/:token", name: 'ResetPassword', component: ResetPassword } 43 | ] 44 | }, 45 | { 46 | path: '/:pathMatch(.*)*', 47 | name: '404', 48 | component: NotFound, 49 | } 50 | ], 51 | }); 52 | 53 | router.beforeEach((to, from, next) => { 54 | const authStore = useAuthStore() 55 | 56 | if (to.meta.requiresAuth && !authStore.currentUser) { 57 | next({ name: "Login" }) 58 | } else if (to.meta.isGuest && authStore.currentUser) { 59 | next({ name: "Home" }) 60 | } else { 61 | next(); 62 | } 63 | }); 64 | 65 | export default router; 66 | -------------------------------------------------------------------------------- /resources/js/stores/auth.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useAuthStore = defineStore('auth', { 4 | state: () => ({ 5 | user: null, 6 | }), 7 | actions: { 8 | set_user(user) { 9 | this.user = user 10 | }, 11 | async attempt_user() { 12 | return axios.get('/api/v1/me') 13 | .then((response) => { 14 | this.user = response.data.data 15 | return response 16 | }) 17 | .catch((error) => { 18 | this.user = null 19 | throw error 20 | }) 21 | }, 22 | async logout() { 23 | return axios.post('/logout') 24 | .then((response) => { 25 | this.$reset() 26 | }) 27 | .catch((error) => { 28 | console.error(error) 29 | throw error 30 | }) 31 | }, 32 | }, 33 | getters: { 34 | currentUser: (state) => state.user, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /resources/markdown/policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Edit this file to define the privacy policy for your application. 4 | -------------------------------------------------------------------------------- /resources/markdown/terms.md: -------------------------------------------------------------------------------- 1 | # Terms of Service 2 | 3 | Edit this file to define the terms of service for your application. 4 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ config('app.name', 'Vue Laravel SPA') }} 8 | 9 | 10 | 11 | 12 | @vite(['resources/js/app.js']) 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/views/emails/team-invitation.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | {{ __('You have been invited to join the :team team!', ['team' => $invitation->team->name]) }} 3 | 4 | @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::registration())) 5 | {{ __('If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:') }} 6 | 7 | @component('mail::button', ['url' => route('register')]) 8 | {{ __('Create Account') }} 9 | @endcomponent 10 | 11 | {{ __('If you already have an account, you may accept this invitation by clicking the button below:') }} 12 | 13 | @else 14 | {{ __('You may accept this invitation by clicking the button below:') }} 15 | @endif 16 | 17 | 18 | @component('mail::button', ['url' => $acceptUrl]) 19 | {{ __('Accept Invitation') }} 20 | @endcomponent 21 | 22 | {{ __('If you did not expect to receive an invitation to this team, you may discard this email.') }} 23 | @endcomponent 24 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | user(); 9 | })->middleware('auth:sanctum'); 10 | 11 | Route::prefix('v1') 12 | ->namespace('\App\Http\Controllers\Api\V1') 13 | ->middleware(['auth:sanctum', 'verified']) 14 | ->group(function () { 15 | Route::get('/me', MeController::class); 16 | }); 17 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote')->hourly(); 9 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | view('app'))->name('app'); 6 | 7 | Route::get('/reset-password/{token}', fn () => view('app')) 8 | ->middleware(['guest:'.config('fortify.guard')]) 9 | ->name('password.reset'); 10 | 11 | Route::get('{any}', fn () => view('app'))->where('any', '^((?!api).)*'); 12 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | "./resources/**/*.blade.php", 7 | "./resources/**/*.js", 8 | "./resources/**/*.vue", 9 | ], 10 | 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ['Figtree', ...defaultTheme.fontFamily.sans], 15 | }, 16 | }, 17 | }, 18 | 19 | plugins: [], 20 | }; 21 | -------------------------------------------------------------------------------- /tests/Feature/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | create(); 13 | 14 | $response = $this->postJson('/login', [ 15 | 'email' => $user->email, 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | 21 | $response->assertJson([ 22 | 'two_factor' => false, 23 | ]); 24 | } 25 | 26 | public function test_users_can_not_authenticate_with_invalid_password() 27 | { 28 | $user = User::factory()->create(); 29 | 30 | $this->post('/login', [ 31 | 'email' => $user->email, 32 | 'password' => 'wrong-password', 33 | ]); 34 | 35 | $this->assertGuest(); 36 | } 37 | 38 | public function test_users_get_his_own_data() 39 | { 40 | $user = User::factory()->create(); 41 | 42 | $this->post('/login', [ 43 | 'email' => $user->email, 44 | 'password' => 'password', 45 | ]); 46 | 47 | $this->assertAuthenticated(); 48 | 49 | $response = $this->getJson('/api/v1/me'); 50 | 51 | $response->assertJson([ 52 | 'data' => [ 53 | 'id' => $user->id, 54 | 'email' => $user->email, 55 | 'name' => $user->name, 56 | ], 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Feature/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skip test cause feature is disabled.'); 21 | } 22 | 23 | $user = User::factory()->create([ 24 | 'email_verified_at' => null, 25 | ]); 26 | 27 | Event::fake(); 28 | 29 | $verificationUrl = URL::temporarySignedRoute( 30 | 'verification.verify', 31 | now()->addMinutes(60), 32 | ['id' => $user->id, 'hash' => sha1($user->email)] 33 | ); 34 | 35 | $response = $this->actingAs($user)->get($verificationUrl); 36 | 37 | Event::assertDispatched(Verified::class); 38 | 39 | $this->assertTrue($user->fresh()->hasVerifiedEmail()); 40 | 41 | $response->assertRedirect(config('app.url').'?verified=1'); 42 | } 43 | 44 | public function test_email_is_not_verified_with_invalid_hash() 45 | { 46 | if (! in_array(Features::emailVerification(), config('fortify.features'))) { 47 | $this->markTestSkipped('Skip test cause feature is disabled.'); 48 | } 49 | 50 | $user = User::factory()->create([ 51 | 'email_verified_at' => null, 52 | ]); 53 | 54 | $verificationUrl = URL::temporarySignedRoute( 55 | 'verification.verify', 56 | now()->addMinutes(60), 57 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 58 | ); 59 | 60 | $this->actingAs($user)->get($verificationUrl); 61 | 62 | $this->assertFalse($user->fresh()->hasVerifiedEmail()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 17 | 18 | $response->assertStatus(200); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Feature/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $response = $this->actingAs($user)->postJson('/user/confirm-password', [ 18 | 'password' => 'password', 19 | ]); 20 | 21 | $response->assertCreated(); 22 | $response->assertSessionHasNoErrors(); 23 | } 24 | 25 | public function test_password_is_not_confirmed_with_invalid_password() 26 | { 27 | $user = User::factory()->create(); 28 | 29 | $response = $this->actingAs($user)->post('/user/confirm-password', [ 30 | 'password' => 'wrong-password', 31 | ]); 32 | 33 | $response->assertSessionHasErrors(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Feature/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skip test cause feature is disabled.'); 20 | } 21 | 22 | Notification::fake(); 23 | 24 | $user = User::factory()->create(); 25 | 26 | $this->post('/forgot-password', ['email' => $user->email]); 27 | 28 | Notification::assertSentTo($user, ResetPassword::class); 29 | } 30 | 31 | public function test_password_can_be_reset_with_valid_token() 32 | { 33 | if (! in_array(Features::resetPasswords(), config('fortify.features'))) { 34 | $this->markTestSkipped('Skip test cause feature is disabled.'); 35 | } 36 | 37 | Notification::fake(); 38 | 39 | $user = User::factory()->create(); 40 | 41 | $this->post('/forgot-password', ['email' => $user->email]); 42 | 43 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 44 | $response = $this->post('/reset-password', [ 45 | 'token' => $notification->token, 46 | 'email' => $user->email, 47 | 'password' => 'password', 48 | 'password_confirmation' => 'password', 49 | ]); 50 | 51 | $response->assertSessionHasNoErrors(); 52 | 53 | return true; 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Feature/ProfileInformationTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skip test cause feature is disabled.'); 18 | } 19 | 20 | $this->actingAs($user = User::factory()->create()); 21 | 22 | $response = $this->put('/user/profile-information', [ 23 | 'name' => 'Test Name', 24 | 'email' => 'test@example.com', 25 | ]); 26 | 27 | $this->assertEquals('Test Name', $user->fresh()->name); 28 | $this->assertEquals('test@example.com', $user->fresh()->email); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Feature/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skip test cause feature is disabled.'); 17 | } 18 | 19 | $response = $this->postJson('/register', [ 20 | 'name' => 'Test User', 21 | 'email' => 'test@example.com', 22 | 'password' => 'password', 23 | 'password_confirmation' => 'password', 24 | ]); 25 | 26 | $this->assertAuthenticated(); 27 | 28 | $response->assertCreated(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Feature/TwoFactorAuthenticationTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skip test cause feature is disabled.'); 18 | } 19 | 20 | $this->actingAs($user = User::factory()->create()); 21 | 22 | $this->withSession(['auth.password_confirmed_at' => time()]); 23 | 24 | $this->post('/user/two-factor-authentication'); 25 | 26 | $user = $user->fresh(); 27 | 28 | $this->assertNotNull($user->two_factor_secret); 29 | $this->assertCount(8, $user->recoveryCodes()); 30 | } 31 | 32 | public function test_recovery_codes_can_be_regenerated() 33 | { 34 | if (! in_array(Features::twoFactorAuthentication(), config('fortify.features'))) { 35 | $this->markTestSkipped('Skip test cause feature is disabled.'); 36 | } 37 | 38 | $this->actingAs($user = User::factory()->create()); 39 | 40 | $this->withSession(['auth.password_confirmed_at' => time()]); 41 | 42 | $this->post('/user/two-factor-authentication'); 43 | $this->post('/user/two-factor-recovery-codes'); 44 | 45 | $user = $user->fresh(); 46 | 47 | $this->post('/user/two-factor-recovery-codes'); 48 | 49 | $this->assertCount(8, $user->recoveryCodes()); 50 | $this->assertCount(8, array_diff($user->recoveryCodes(), $user->fresh()->recoveryCodes())); 51 | } 52 | 53 | public function test_two_factor_authentication_can_be_disabled() 54 | { 55 | if (! in_array(Features::twoFactorAuthentication(), config('fortify.features'))) { 56 | $this->markTestSkipped('Skip test cause feature is disabled.'); 57 | } 58 | 59 | $this->actingAs($user = User::factory()->create()); 60 | 61 | $this->withSession(['auth.password_confirmed_at' => time()]); 62 | 63 | $this->post('/user/two-factor-authentication'); 64 | 65 | $this->assertNotNull($user->fresh()->two_factor_secret); 66 | 67 | $this->delete('/user/two-factor-authentication'); 68 | 69 | $this->assertNull($user->fresh()->two_factor_secret); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Feature/UpdatePasswordTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skip test cause feature is disabled.'); 19 | } 20 | 21 | $this->actingAs($user = User::factory()->create()); 22 | 23 | $response = $this->put('/user/password', [ 24 | 'current_password' => 'password', 25 | 'password' => 'new-password', 26 | 'password_confirmation' => 'new-password', 27 | ]); 28 | 29 | $this->assertTrue(Hash::check('new-password', $user->fresh()->password)); 30 | } 31 | 32 | public function test_current_password_must_be_correct() 33 | { 34 | if (! in_array(Features::updatePasswords(), config('fortify.features'))) { 35 | $this->markTestSkipped('Skip test cause feature is disabled.'); 36 | } 37 | 38 | $this->actingAs($user = User::factory()->create()); 39 | 40 | $response = $this->put('/user/password', [ 41 | 'current_password' => 'wrong-password', 42 | 'password' => 'new-password', 43 | 'password_confirmation' => 'new-password', 44 | ]); 45 | 46 | $response->assertSessionHasErrors(); 47 | 48 | $this->assertTrue(Hash::check('password', $user->fresh()->password)); 49 | } 50 | 51 | public function test_new_passwords_must_match() 52 | { 53 | if (! in_array(Features::updatePasswords(), config('fortify.features'))) { 54 | $this->markTestSkipped('Skip test cause feature is disabled.'); 55 | } 56 | 57 | $this->actingAs($user = User::factory()->create()); 58 | 59 | $response = $this->put('/user/password', [ 60 | 'current_password' => 'password', 61 | 'password' => 'new-password', 62 | 'password_confirmation' => 'wrong-password', 63 | ]); 64 | 65 | $response->assertSessionHasErrors(); 66 | 67 | $this->assertTrue(Hash::check('password', $user->fresh()->password)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | export default defineConfig({ 6 | resolve: { 7 | alias: { 8 | '@': '/resources/js', 9 | }, 10 | }, 11 | build: { 12 | sourcemap: true, 13 | }, 14 | plugins: [ 15 | laravel({ 16 | input: [ 17 | 'resources/js/app.js' 18 | ], 19 | refresh: true, 20 | // detectTls: 'vue-laravel-spa.test', 21 | }), 22 | vue({ 23 | template: { 24 | transformAssetUrls: { 25 | // The Vue plugin will re-write asset URLs, when referenced 26 | // in Single File Components, to point to the Laravel web 27 | // server. Setting this to `null` allows the Laravel plugin 28 | // to instead re-write asset URLs to point to the Vite 29 | // server instead. 30 | base: null, 31 | 32 | // The Vue plugin will parse absolute URLs and treat them 33 | // as absolute paths to files on disk. Setting this to 34 | // `false` will leave absolute URLs un-touched so they can 35 | // reference assets in the public directory as expected. 36 | includeAbsolute: false, 37 | }, 38 | }, 39 | }), 40 | ], 41 | }); 42 | --------------------------------------------------------------------------------