Your calm window into the Atmosphere. morgen.blue
rss atproto
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: remove Fortify in preparation for ATProto OAuth

Tear down the password-based auth stack the starter kit installs via
Laravel Fortify so the branch is ready for ATProto OAuth through
Socialite + revolution/laravel-bluesky. The wire-up of the new auth
layer lands in a follow-up.

Removed:
- laravel/fortify from composer.json
- FortifyServiceProvider (+ bootstrap/providers.php registration)
- Fortify actions (CreateNewUser, ResetUserPassword)
- config/fortify.php
- SecurityController + password/2FA request classes
- 2FA migration (add_two_factor_columns_to_users_table)
- Fortify-shaped Inertia auth pages (login, register, forgot-password,
reset-password, verify-email, confirm-password, two-factor-challenge)
and settings/security page
- Two-factor React components + use-two-factor-auth hook + input-otp ui
- All tests/Feature/Auth Pest tests + tests/Feature/Settings/SecurityTest

Cleaned:
- routes/web.php (drop Fortify Features import, canRegister prop,
verified middleware on dashboard)
- routes/settings.php (drop security/password routes)
- User model (drop TwoFactorAuthenticatable trait, 2FA hidden fields,
two_factor_confirmed_at cast)
- tests/TestCase.php (drop skipUnlessFortifyHas helper)
- resources/js/types/auth.ts (drop 2FA types)
- resources/js/pages/welcome.tsx (drop login/register from @/routes,
drop canRegister prop, drop register CTA)
- resources/js/pages/settings/profile.tsx (drop verify-email section)
- resources/js/components/user-menu-content.tsx (swap logout() helper
for plain /logout path so the link survives until 6b adds the route)

Known broken state (to be fixed in Step 6b): /login + /logout routes
no longer exist; user-menu logout link and any /login visit return 404
until the ATProto wire-up adds replacements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+19 -3173
-131
.agents/skills/fortify-development/SKILL.md
··· 1 - --- 2 - name: fortify-development 3 - description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.' 4 - license: MIT 5 - metadata: 6 - author: laravel 7 - --- 8 - 9 - # Laravel Fortify Development 10 - 11 - Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications. 12 - 13 - ## Documentation 14 - 15 - Use `search-docs` for detailed Laravel Fortify patterns and documentation. 16 - 17 - ## Usage 18 - 19 - - **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints 20 - - **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.) 21 - - **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field 22 - - **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.) 23 - - **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc. 24 - 25 - ## Available Features 26 - 27 - Enable in `config/fortify.php` features array: 28 - 29 - - `Features::registration()` - User registration 30 - - `Features::resetPasswords()` - Password reset via email 31 - - `Features::emailVerification()` - Requires User to implement `MustVerifyEmail` 32 - - `Features::updateProfileInformation()` - Profile updates 33 - - `Features::updatePasswords()` - Password changes 34 - - `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes 35 - 36 - > Use `search-docs` for feature configuration options and customization patterns. 37 - 38 - ## Setup Workflows 39 - 40 - ### Two-Factor Authentication Setup 41 - 42 - ``` 43 - - [ ] Add TwoFactorAuthenticatable trait to User model 44 - - [ ] Enable feature in config/fortify.php 45 - - [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate 46 - - [ ] Set up view callbacks in FortifyServiceProvider 47 - - [ ] Create 2FA management UI 48 - - [ ] Test QR code and recovery codes 49 - ``` 50 - 51 - > Use `search-docs` for TOTP implementation and recovery code handling patterns. 52 - 53 - ### Email Verification Setup 54 - 55 - ``` 56 - - [ ] Enable emailVerification feature in config 57 - - [ ] Implement MustVerifyEmail interface on User model 58 - - [ ] Set up verifyEmailView callback 59 - - [ ] Add verified middleware to protected routes 60 - - [ ] Test verification email flow 61 - ``` 62 - 63 - > Use `search-docs` for MustVerifyEmail implementation patterns. 64 - 65 - ### Password Reset Setup 66 - 67 - ``` 68 - - [ ] Enable resetPasswords feature in config 69 - - [ ] Set up requestPasswordResetLinkView callback 70 - - [ ] Set up resetPasswordView callback 71 - - [ ] Define password.reset named route (if views disabled) 72 - - [ ] Test reset email and link flow 73 - ``` 74 - 75 - > Use `search-docs` for custom password reset flow patterns. 76 - 77 - ### SPA Authentication Setup 78 - 79 - ``` 80 - - [ ] Set 'views' => false in config/fortify.php 81 - - [ ] Install and configure Laravel Sanctum for session-based SPA authentication 82 - - [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication) 83 - - [ ] Set up CSRF token handling 84 - - [ ] Test XHR authentication flows 85 - ``` 86 - 87 - > Use `search-docs` for integration and SPA authentication patterns. 88 - 89 - #### Two-Factor Authentication in SPA Mode 90 - 91 - When `views` is set to `false`, Fortify returns JSON responses instead of redirects. 92 - 93 - If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required: 94 - 95 - ```json 96 - { 97 - "two_factor": true 98 - } 99 - ``` 100 - 101 - ## Best Practices 102 - 103 - ### Custom Authentication Logic 104 - 105 - Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects. 106 - 107 - ### Registration Customization 108 - 109 - Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields. 110 - 111 - ### Rate Limiting 112 - 113 - Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination. 114 - 115 - ## Key Endpoints 116 - 117 - | Feature | Method | Endpoint | 118 - |------------------------|----------|---------------------------------------------| 119 - | Login | POST | `/login` | 120 - | Logout | POST | `/logout` | 121 - | Register | POST | `/register` | 122 - | Password Reset Request | POST | `/forgot-password` | 123 - | Password Reset | POST | `/reset-password` | 124 - | Email Verify Notice | GET | `/email/verify` | 125 - | Resend Verification | POST | `/email/verification-notification` | 126 - | Password Confirm | POST | `/user/confirm-password` | 127 - | Enable 2FA | POST | `/user/two-factor-authentication` | 128 - | Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` | 129 - | 2FA Challenge | POST | `/two-factor-challenge` | 130 - | Get QR Code | GET | `/user/two-factor-qr-code` | 131 - | Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` |
-131
.claude/skills/fortify-development/SKILL.md
··· 1 - --- 2 - name: fortify-development 3 - description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.' 4 - license: MIT 5 - metadata: 6 - author: laravel 7 - --- 8 - 9 - # Laravel Fortify Development 10 - 11 - Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications. 12 - 13 - ## Documentation 14 - 15 - Use `search-docs` for detailed Laravel Fortify patterns and documentation. 16 - 17 - ## Usage 18 - 19 - - **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints 20 - - **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.) 21 - - **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field 22 - - **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.) 23 - - **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc. 24 - 25 - ## Available Features 26 - 27 - Enable in `config/fortify.php` features array: 28 - 29 - - `Features::registration()` - User registration 30 - - `Features::resetPasswords()` - Password reset via email 31 - - `Features::emailVerification()` - Requires User to implement `MustVerifyEmail` 32 - - `Features::updateProfileInformation()` - Profile updates 33 - - `Features::updatePasswords()` - Password changes 34 - - `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes 35 - 36 - > Use `search-docs` for feature configuration options and customization patterns. 37 - 38 - ## Setup Workflows 39 - 40 - ### Two-Factor Authentication Setup 41 - 42 - ``` 43 - - [ ] Add TwoFactorAuthenticatable trait to User model 44 - - [ ] Enable feature in config/fortify.php 45 - - [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate 46 - - [ ] Set up view callbacks in FortifyServiceProvider 47 - - [ ] Create 2FA management UI 48 - - [ ] Test QR code and recovery codes 49 - ``` 50 - 51 - > Use `search-docs` for TOTP implementation and recovery code handling patterns. 52 - 53 - ### Email Verification Setup 54 - 55 - ``` 56 - - [ ] Enable emailVerification feature in config 57 - - [ ] Implement MustVerifyEmail interface on User model 58 - - [ ] Set up verifyEmailView callback 59 - - [ ] Add verified middleware to protected routes 60 - - [ ] Test verification email flow 61 - ``` 62 - 63 - > Use `search-docs` for MustVerifyEmail implementation patterns. 64 - 65 - ### Password Reset Setup 66 - 67 - ``` 68 - - [ ] Enable resetPasswords feature in config 69 - - [ ] Set up requestPasswordResetLinkView callback 70 - - [ ] Set up resetPasswordView callback 71 - - [ ] Define password.reset named route (if views disabled) 72 - - [ ] Test reset email and link flow 73 - ``` 74 - 75 - > Use `search-docs` for custom password reset flow patterns. 76 - 77 - ### SPA Authentication Setup 78 - 79 - ``` 80 - - [ ] Set 'views' => false in config/fortify.php 81 - - [ ] Install and configure Laravel Sanctum for session-based SPA authentication 82 - - [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication) 83 - - [ ] Set up CSRF token handling 84 - - [ ] Test XHR authentication flows 85 - ``` 86 - 87 - > Use `search-docs` for integration and SPA authentication patterns. 88 - 89 - #### Two-Factor Authentication in SPA Mode 90 - 91 - When `views` is set to `false`, Fortify returns JSON responses instead of redirects. 92 - 93 - If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required: 94 - 95 - ```json 96 - { 97 - "two_factor": true 98 - } 99 - ``` 100 - 101 - ## Best Practices 102 - 103 - ### Custom Authentication Logic 104 - 105 - Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects. 106 - 107 - ### Registration Customization 108 - 109 - Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields. 110 - 111 - ### Rate Limiting 112 - 113 - Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination. 114 - 115 - ## Key Endpoints 116 - 117 - | Feature | Method | Endpoint | 118 - |------------------------|----------|---------------------------------------------| 119 - | Login | POST | `/login` | 120 - | Logout | POST | `/logout` | 121 - | Register | POST | `/register` | 122 - | Password Reset Request | POST | `/forgot-password` | 123 - | Password Reset | POST | `/reset-password` | 124 - | Email Verify Notice | GET | `/email/verify` | 125 - | Resend Verification | POST | `/email/verification-notification` | 126 - | Password Confirm | POST | `/user/confirm-password` | 127 - | Enable 2FA | POST | `/user/two-factor-authentication` | 128 - | Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` | 129 - | 2FA Challenge | POST | `/two-factor-challenge` | 130 - | Get QR Code | GET | `/user/two-factor-qr-code` | 131 - | Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` |
-2
AGENTS.md
··· 11 11 12 12 - php - 8.4 13 13 - inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3 14 - - laravel/fortify (FORTIFY) - v1 15 14 - laravel/framework (LARAVEL) - v13 16 15 - laravel/prompts (PROMPTS) - v0 17 16 - laravel/wayfinder (WAYFINDER) - v0 ··· 33 32 34 33 This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. 35 34 36 - - `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features. 37 35 - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. 38 36 - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task 39 37 - `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
-2
CLAUDE.md
··· 11 11 12 12 - php - 8.4 13 13 - inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3 14 - - laravel/fortify (FORTIFY) - v1 15 14 - laravel/framework (LARAVEL) - v13 16 15 - laravel/prompts (PROMPTS) - v0 17 16 - laravel/wayfinder (WAYFINDER) - v0 ··· 33 32 34 33 This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. 35 34 36 - - `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features. 37 35 - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. 38 36 - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task 39 37 - `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
-2
GEMINI.md
··· 11 11 12 12 - php - 8.4 13 13 - inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3 14 - - laravel/fortify (FORTIFY) - v1 15 14 - laravel/framework (LARAVEL) - v13 16 15 - laravel/prompts (PROMPTS) - v0 17 16 - laravel/wayfinder (WAYFINDER) - v0 ··· 33 32 34 33 This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. 35 34 36 - - `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features. 37 35 - `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns. 38 36 - `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task 39 37 - `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
-33
app/Actions/Fortify/CreateNewUser.php
··· 1 - <?php 2 - 3 - namespace App\Actions\Fortify; 4 - 5 - use App\Concerns\PasswordValidationRules; 6 - use App\Concerns\ProfileValidationRules; 7 - use App\Models\User; 8 - use Illuminate\Support\Facades\Validator; 9 - use Laravel\Fortify\Contracts\CreatesNewUsers; 10 - 11 - class CreateNewUser implements CreatesNewUsers 12 - { 13 - use PasswordValidationRules, ProfileValidationRules; 14 - 15 - /** 16 - * Validate and create a newly registered user. 17 - * 18 - * @param array<string, string> $input 19 - */ 20 - public function create(array $input): User 21 - { 22 - Validator::make($input, [ 23 - ...$this->profileRules(), 24 - 'password' => $this->passwordRules(), 25 - ])->validate(); 26 - 27 - return User::create([ 28 - 'name' => $input['name'], 29 - 'email' => $input['email'], 30 - 'password' => $input['password'], 31 - ]); 32 - } 33 - }
-29
app/Actions/Fortify/ResetUserPassword.php
··· 1 - <?php 2 - 3 - namespace App\Actions\Fortify; 4 - 5 - use App\Concerns\PasswordValidationRules; 6 - use App\Models\User; 7 - use Illuminate\Support\Facades\Validator; 8 - use Laravel\Fortify\Contracts\ResetsUserPasswords; 9 - 10 - class ResetUserPassword implements ResetsUserPasswords 11 - { 12 - use PasswordValidationRules; 13 - 14 - /** 15 - * Validate and reset the user's forgotten password. 16 - * 17 - * @param array<string, string> $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' => $input['password'], 27 - ])->save(); 28 - } 29 - }
-60
app/Http/Controllers/Settings/SecurityController.php
··· 1 - <?php 2 - 3 - namespace App\Http\Controllers\Settings; 4 - 5 - use App\Http\Controllers\Controller; 6 - use App\Http\Requests\Settings\PasswordUpdateRequest; 7 - use App\Http\Requests\Settings\TwoFactorAuthenticationRequest; 8 - use Illuminate\Http\RedirectResponse; 9 - use Illuminate\Routing\Controllers\HasMiddleware; 10 - use Illuminate\Routing\Controllers\Middleware; 11 - use Inertia\Inertia; 12 - use Inertia\Response; 13 - use Laravel\Fortify\Features; 14 - 15 - class SecurityController extends Controller implements HasMiddleware 16 - { 17 - /** 18 - * Get the middleware that should be assigned to the controller. 19 - */ 20 - public static function middleware(): array 21 - { 22 - return Features::canManageTwoFactorAuthentication() 23 - && Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword') 24 - ? [new Middleware('password.confirm', only: ['edit'])] 25 - : []; 26 - } 27 - 28 - /** 29 - * Show the user's security settings page. 30 - */ 31 - public function edit(TwoFactorAuthenticationRequest $request): Response 32 - { 33 - $props = [ 34 - 'canManageTwoFactor' => Features::canManageTwoFactorAuthentication(), 35 - ]; 36 - 37 - if (Features::canManageTwoFactorAuthentication()) { 38 - $request->ensureStateIsValid(); 39 - 40 - $props['twoFactorEnabled'] = $request->user()->hasEnabledTwoFactorAuthentication(); 41 - $props['requiresConfirmation'] = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'); 42 - } 43 - 44 - return Inertia::render('settings/security', $props); 45 - } 46 - 47 - /** 48 - * Update the user's password. 49 - */ 50 - public function update(PasswordUpdateRequest $request): RedirectResponse 51 - { 52 - $request->user()->update([ 53 - 'password' => $request->password, 54 - ]); 55 - 56 - Inertia::flash('toast', ['type' => 'success', 'message' => __('Password updated.')]); 57 - 58 - return back(); 59 - } 60 - }
-25
app/Http/Requests/Settings/PasswordUpdateRequest.php
··· 1 - <?php 2 - 3 - namespace App\Http\Requests\Settings; 4 - 5 - use App\Concerns\PasswordValidationRules; 6 - use Illuminate\Contracts\Validation\ValidationRule; 7 - use Illuminate\Foundation\Http\FormRequest; 8 - 9 - class PasswordUpdateRequest extends FormRequest 10 - { 11 - use PasswordValidationRules; 12 - 13 - /** 14 - * Get the validation rules that apply to the request. 15 - * 16 - * @return array<string, ValidationRule|array<mixed>|string> 17 - */ 18 - public function rules(): array 19 - { 20 - return [ 21 - 'current_password' => $this->currentPasswordRules(), 22 - 'password' => $this->passwordRules(), 23 - ]; 24 - } 25 - }
-22
app/Http/Requests/Settings/TwoFactorAuthenticationRequest.php
··· 1 - <?php 2 - 3 - namespace App\Http\Requests\Settings; 4 - 5 - use Illuminate\Contracts\Validation\ValidationRule; 6 - use Illuminate\Foundation\Http\FormRequest; 7 - use Laravel\Fortify\InteractsWithTwoFactorState; 8 - 9 - class TwoFactorAuthenticationRequest extends FormRequest 10 - { 11 - use InteractsWithTwoFactorState; 12 - 13 - /** 14 - * Get the validation rules that apply to the request. 15 - * 16 - * @return array<string, ValidationRule|array<mixed>|string> 17 - */ 18 - public function rules(): array 19 - { 20 - return []; 21 - } 22 - }
+2 -4
app/Models/User.php
··· 9 9 use Illuminate\Database\Eloquent\Factories\HasFactory; 10 10 use Illuminate\Foundation\Auth\User as Authenticatable; 11 11 use Illuminate\Notifications\Notifiable; 12 - use Laravel\Fortify\TwoFactorAuthenticatable; 13 12 14 13 #[Fillable(['name', 'email', 'password'])] 15 - #[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])] 14 + #[Hidden(['password', 'remember_token'])] 16 15 class User extends Authenticatable 17 16 { 18 17 /** @use HasFactory<UserFactory> */ 19 - use HasFactory, Notifiable, TwoFactorAuthenticatable; 18 + use HasFactory, Notifiable; 20 19 21 20 /** 22 21 * Get the attributes that should be cast. ··· 28 27 return [ 29 28 'email_verified_at' => 'datetime', 30 29 'password' => 'hashed', 31 - 'two_factor_confirmed_at' => 'datetime', 32 30 ]; 33 31 } 34 32 }
-91
app/Providers/FortifyServiceProvider.php
··· 1 - <?php 2 - 3 - namespace App\Providers; 4 - 5 - use App\Actions\Fortify\CreateNewUser; 6 - use App\Actions\Fortify\ResetUserPassword; 7 - use Illuminate\Cache\RateLimiting\Limit; 8 - use Illuminate\Http\Request; 9 - use Illuminate\Support\Facades\RateLimiter; 10 - use Illuminate\Support\ServiceProvider; 11 - use Illuminate\Support\Str; 12 - use Inertia\Inertia; 13 - use Laravel\Fortify\Features; 14 - use Laravel\Fortify\Fortify; 15 - 16 - class FortifyServiceProvider extends ServiceProvider 17 - { 18 - /** 19 - * Register any application services. 20 - */ 21 - public function register(): void 22 - { 23 - // 24 - } 25 - 26 - /** 27 - * Bootstrap any application services. 28 - */ 29 - public function boot(): void 30 - { 31 - $this->configureActions(); 32 - $this->configureViews(); 33 - $this->configureRateLimiting(); 34 - } 35 - 36 - /** 37 - * Configure Fortify actions. 38 - */ 39 - private function configureActions(): void 40 - { 41 - Fortify::resetUserPasswordsUsing(ResetUserPassword::class); 42 - Fortify::createUsersUsing(CreateNewUser::class); 43 - } 44 - 45 - /** 46 - * Configure Fortify views. 47 - */ 48 - private function configureViews(): void 49 - { 50 - Fortify::loginView(fn (Request $request) => Inertia::render('auth/login', [ 51 - 'canResetPassword' => Features::enabled(Features::resetPasswords()), 52 - 'canRegister' => Features::enabled(Features::registration()), 53 - 'status' => $request->session()->get('status'), 54 - ])); 55 - 56 - Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/reset-password', [ 57 - 'email' => $request->email, 58 - 'token' => $request->route('token'), 59 - ])); 60 - 61 - Fortify::requestPasswordResetLinkView(fn (Request $request) => Inertia::render('auth/forgot-password', [ 62 - 'status' => $request->session()->get('status'), 63 - ])); 64 - 65 - Fortify::verifyEmailView(fn (Request $request) => Inertia::render('auth/verify-email', [ 66 - 'status' => $request->session()->get('status'), 67 - ])); 68 - 69 - Fortify::registerView(fn () => Inertia::render('auth/register')); 70 - 71 - Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge')); 72 - 73 - Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password')); 74 - } 75 - 76 - /** 77 - * Configure rate limiting. 78 - */ 79 - private function configureRateLimiting(): void 80 - { 81 - RateLimiter::for('two-factor', function (Request $request) { 82 - return Limit::perMinute(5)->by($request->session()->get('login.id')); 83 - }); 84 - 85 - RateLimiter::for('login', function (Request $request) { 86 - $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip()); 87 - 88 - return Limit::perMinute(5)->by($throttleKey); 89 - }); 90 - } 91 - }
-1
boost.json
··· 10 10 "nightwatch_mcp": false, 11 11 "sail": false, 12 12 "skills": [ 13 - "fortify-development", 14 13 "laravel-best-practices", 15 14 "wayfinder-development", 16 15 "pest-testing",
-2
bootstrap/providers.php
··· 1 1 <?php 2 2 3 3 use App\Providers\AppServiceProvider; 4 - use App\Providers\FortifyServiceProvider; 5 4 6 5 return [ 7 6 AppServiceProvider::class, 8 - FortifyServiceProvider::class, 9 7 ];
+1 -2
composer.json
··· 11 11 "require": { 12 12 "php": "^8.3", 13 13 "inertiajs/inertia-laravel": "^3.0", 14 - "laravel/fortify": "^1.34", 15 14 "laravel/framework": "^13.0", 16 15 "laravel/tinker": "^3.0", 17 16 "laravel/wayfinder": "^0.1.14" ··· 106 105 } 107 106 }, 108 107 "minimum-stability": "stable" 109 - } 108 + }
+1 -290
composer.lock
··· 4 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 5 "This file is @generated automatically" 6 6 ], 7 - "content-hash": "ae6e8d54a197752ae4ad39a9f0787d8c", 7 + "content-hash": "9da0b67d6e4141bb81c69503f7af01f9", 8 8 "packages": [ 9 - { 10 - "name": "bacon/bacon-qr-code", 11 - "version": "v3.1.1", 12 - "source": { 13 - "type": "git", 14 - "url": "https://github.com/Bacon/BaconQrCode.git", 15 - "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2" 16 - }, 17 - "dist": { 18 - "type": "zip", 19 - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", 20 - "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", 21 - "shasum": "" 22 - }, 23 - "require": { 24 - "dasprid/enum": "^1.0.3", 25 - "ext-iconv": "*", 26 - "php": "^8.1" 27 - }, 28 - "require-dev": { 29 - "phly/keep-a-changelog": "^2.12", 30 - "phpunit/phpunit": "^10.5.11 || ^11.0.4", 31 - "spatie/phpunit-snapshot-assertions": "^5.1.5", 32 - "spatie/pixelmatch-php": "^1.2.0", 33 - "squizlabs/php_codesniffer": "^3.9" 34 - }, 35 - "suggest": { 36 - "ext-imagick": "to generate QR code images" 37 - }, 38 - "type": "library", 39 - "autoload": { 40 - "psr-4": { 41 - "BaconQrCode\\": "src/" 42 - } 43 - }, 44 - "notification-url": "https://packagist.org/downloads/", 45 - "license": [ 46 - "BSD-2-Clause" 47 - ], 48 - "authors": [ 49 - { 50 - "name": "Ben Scholzen 'DASPRiD'", 51 - "email": "mail@dasprids.de", 52 - "homepage": "https://dasprids.de/", 53 - "role": "Developer" 54 - } 55 - ], 56 - "description": "BaconQrCode is a QR code generator for PHP.", 57 - "homepage": "https://github.com/Bacon/BaconQrCode", 58 - "support": { 59 - "issues": "https://github.com/Bacon/BaconQrCode/issues", 60 - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1" 61 - }, 62 - "time": "2026-04-05T21:06:35+00:00" 63 - }, 64 9 { 65 10 "name": "brick/math", 66 11 "version": "0.14.8", ··· 189 134 } 190 135 ], 191 136 "time": "2024-02-09T16:56:22+00:00" 192 - }, 193 - { 194 - "name": "dasprid/enum", 195 - "version": "1.0.7", 196 - "source": { 197 - "type": "git", 198 - "url": "https://github.com/DASPRiD/Enum.git", 199 - "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" 200 - }, 201 - "dist": { 202 - "type": "zip", 203 - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", 204 - "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", 205 - "shasum": "" 206 - }, 207 - "require": { 208 - "php": ">=7.1 <9.0" 209 - }, 210 - "require-dev": { 211 - "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", 212 - "squizlabs/php_codesniffer": "*" 213 - }, 214 - "type": "library", 215 - "autoload": { 216 - "psr-4": { 217 - "DASPRiD\\Enum\\": "src/" 218 - } 219 - }, 220 - "notification-url": "https://packagist.org/downloads/", 221 - "license": [ 222 - "BSD-2-Clause" 223 - ], 224 - "authors": [ 225 - { 226 - "name": "Ben Scholzen 'DASPRiD'", 227 - "email": "mail@dasprids.de", 228 - "homepage": "https://dasprids.de/", 229 - "role": "Developer" 230 - } 231 - ], 232 - "description": "PHP 7.1 enum implementation", 233 - "keywords": [ 234 - "enum", 235 - "map" 236 - ], 237 - "support": { 238 - "issues": "https://github.com/DASPRiD/Enum/issues", 239 - "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" 240 - }, 241 - "time": "2025-09-16T12:23:56+00:00" 242 137 }, 243 138 { 244 139 "name": "dflydev/dot-access-data", ··· 1230 1125 "source": "https://github.com/inertiajs/inertia-laravel/tree/v3.0.6" 1231 1126 }, 1232 1127 "time": "2026-04-10T14:29:45+00:00" 1233 - }, 1234 - { 1235 - "name": "laravel/fortify", 1236 - "version": "v1.36.2", 1237 - "source": { 1238 - "type": "git", 1239 - "url": "https://github.com/laravel/fortify.git", 1240 - "reference": "b36e0782e6f5f6cfbab34327895a63b7c4c031f9" 1241 - }, 1242 - "dist": { 1243 - "type": "zip", 1244 - "url": "https://api.github.com/repos/laravel/fortify/zipball/b36e0782e6f5f6cfbab34327895a63b7c4c031f9", 1245 - "reference": "b36e0782e6f5f6cfbab34327895a63b7c4c031f9", 1246 - "shasum": "" 1247 - }, 1248 - "require": { 1249 - "bacon/bacon-qr-code": "^3.0", 1250 - "ext-json": "*", 1251 - "illuminate/console": "^10.0|^11.0|^12.0|^13.0", 1252 - "illuminate/support": "^10.0|^11.0|^12.0|^13.0", 1253 - "php": "^8.1", 1254 - "pragmarx/google2fa": "^9.0" 1255 - }, 1256 - "require-dev": { 1257 - "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", 1258 - "phpstan/phpstan": "^1.10" 1259 - }, 1260 - "type": "library", 1261 - "extra": { 1262 - "laravel": { 1263 - "providers": [ 1264 - "Laravel\\Fortify\\FortifyServiceProvider" 1265 - ] 1266 - }, 1267 - "branch-alias": { 1268 - "dev-master": "1.x-dev" 1269 - } 1270 - }, 1271 - "autoload": { 1272 - "psr-4": { 1273 - "Laravel\\Fortify\\": "src/" 1274 - } 1275 - }, 1276 - "notification-url": "https://packagist.org/downloads/", 1277 - "license": [ 1278 - "MIT" 1279 - ], 1280 - "authors": [ 1281 - { 1282 - "name": "Taylor Otwell", 1283 - "email": "taylor@laravel.com" 1284 - } 1285 - ], 1286 - "description": "Backend controllers and scaffolding for Laravel authentication.", 1287 - "keywords": [ 1288 - "auth", 1289 - "laravel" 1290 - ], 1291 - "support": { 1292 - "issues": "https://github.com/laravel/fortify/issues", 1293 - "source": "https://github.com/laravel/fortify" 1294 - }, 1295 - "time": "2026-03-20T20:13:51+00:00" 1296 1128 }, 1297 1129 { 1298 1130 "name": "laravel/framework", ··· 2840 2672 "time": "2026-02-16T23:10:27+00:00" 2841 2673 }, 2842 2674 { 2843 - "name": "paragonie/constant_time_encoding", 2844 - "version": "v3.1.3", 2845 - "source": { 2846 - "type": "git", 2847 - "url": "https://github.com/paragonie/constant_time_encoding.git", 2848 - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" 2849 - }, 2850 - "dist": { 2851 - "type": "zip", 2852 - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", 2853 - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", 2854 - "shasum": "" 2855 - }, 2856 - "require": { 2857 - "php": "^8" 2858 - }, 2859 - "require-dev": { 2860 - "infection/infection": "^0", 2861 - "nikic/php-fuzzer": "^0", 2862 - "phpunit/phpunit": "^9|^10|^11", 2863 - "vimeo/psalm": "^4|^5|^6" 2864 - }, 2865 - "type": "library", 2866 - "autoload": { 2867 - "psr-4": { 2868 - "ParagonIE\\ConstantTime\\": "src/" 2869 - } 2870 - }, 2871 - "notification-url": "https://packagist.org/downloads/", 2872 - "license": [ 2873 - "MIT" 2874 - ], 2875 - "authors": [ 2876 - { 2877 - "name": "Paragon Initiative Enterprises", 2878 - "email": "security@paragonie.com", 2879 - "homepage": "https://paragonie.com", 2880 - "role": "Maintainer" 2881 - }, 2882 - { 2883 - "name": "Steve 'Sc00bz' Thomas", 2884 - "email": "steve@tobtu.com", 2885 - "homepage": "https://www.tobtu.com", 2886 - "role": "Original Developer" 2887 - } 2888 - ], 2889 - "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", 2890 - "keywords": [ 2891 - "base16", 2892 - "base32", 2893 - "base32_decode", 2894 - "base32_encode", 2895 - "base64", 2896 - "base64_decode", 2897 - "base64_encode", 2898 - "bin2hex", 2899 - "encoding", 2900 - "hex", 2901 - "hex2bin", 2902 - "rfc4648" 2903 - ], 2904 - "support": { 2905 - "email": "info@paragonie.com", 2906 - "issues": "https://github.com/paragonie/constant_time_encoding/issues", 2907 - "source": "https://github.com/paragonie/constant_time_encoding" 2908 - }, 2909 - "time": "2025-09-24T15:06:41+00:00" 2910 - }, 2911 - { 2912 2675 "name": "phpoption/phpoption", 2913 2676 "version": "1.9.5", 2914 2677 "source": { ··· 3029 2792 "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" 3030 2793 }, 3031 2794 "time": "2026-01-25T14:56:51+00:00" 3032 - }, 3033 - { 3034 - "name": "pragmarx/google2fa", 3035 - "version": "v9.0.0", 3036 - "source": { 3037 - "type": "git", 3038 - "url": "https://github.com/antonioribeiro/google2fa.git", 3039 - "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" 3040 - }, 3041 - "dist": { 3042 - "type": "zip", 3043 - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", 3044 - "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", 3045 - "shasum": "" 3046 - }, 3047 - "require": { 3048 - "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", 3049 - "php": "^7.1|^8.0" 3050 - }, 3051 - "require-dev": { 3052 - "phpstan/phpstan": "^1.9", 3053 - "phpunit/phpunit": "^7.5.15|^8.5|^9.0" 3054 - }, 3055 - "type": "library", 3056 - "autoload": { 3057 - "psr-4": { 3058 - "PragmaRX\\Google2FA\\": "src/" 3059 - } 3060 - }, 3061 - "notification-url": "https://packagist.org/downloads/", 3062 - "license": [ 3063 - "MIT" 3064 - ], 3065 - "authors": [ 3066 - { 3067 - "name": "Antonio Carlos Ribeiro", 3068 - "email": "acr@antoniocarlosribeiro.com", 3069 - "role": "Creator & Designer" 3070 - } 3071 - ], 3072 - "description": "A One Time Password Authentication package, compatible with Google Authenticator.", 3073 - "keywords": [ 3074 - "2fa", 3075 - "Authentication", 3076 - "Two Factor Authentication", 3077 - "google2fa" 3078 - ], 3079 - "support": { 3080 - "issues": "https://github.com/antonioribeiro/google2fa/issues", 3081 - "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" 3082 - }, 3083 - "time": "2025-09-19T22:51:08+00:00" 3084 2795 }, 3085 2796 { 3086 2797 "name": "psr/clock",
-157
config/fortify.php
··· 1 - <?php 2 - 3 - use Laravel\Fortify\Features; 4 - 5 - return [ 6 - 7 - /* 8 - |-------------------------------------------------------------------------- 9 - | Fortify Guard 10 - |-------------------------------------------------------------------------- 11 - | 12 - | Here you may specify which authentication guard Fortify will use while 13 - | authenticating users. This value should correspond with one of your 14 - | guards that is already present in your "auth" configuration file. 15 - | 16 - */ 17 - 18 - 'guard' => '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' => '/dashboard', 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' => true, 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::twoFactorAuthentication([ 151 - 'confirm' => true, 152 - 'confirmPassword' => true, 153 - // 'window' => 0 154 - ]), 155 - ], 156 - 157 - ];
-34
database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php
··· 1 - <?php 2 - 3 - use Illuminate\Database\Migrations\Migration; 4 - use Illuminate\Database\Schema\Blueprint; 5 - use Illuminate\Support\Facades\Schema; 6 - 7 - return new class extends Migration 8 - { 9 - /** 10 - * Run the migrations. 11 - */ 12 - public function up(): void 13 - { 14 - Schema::table('users', function (Blueprint $table) { 15 - $table->text('two_factor_secret')->after('password')->nullable(); 16 - $table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable(); 17 - $table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable(); 18 - }); 19 - } 20 - 21 - /** 22 - * Reverse the migrations. 23 - */ 24 - public function down(): void 25 - { 26 - Schema::table('users', function (Blueprint $table) { 27 - $table->dropColumn([ 28 - 'two_factor_secret', 29 - 'two_factor_recovery_codes', 30 - 'two_factor_confirmed_at', 31 - ]); 32 - }); 33 - } 34 - };
-164
resources/js/components/two-factor-recovery-codes.tsx
··· 1 - import { Form } from '@inertiajs/react'; 2 - import { Eye, EyeOff, LockKeyhole, RefreshCw } from 'lucide-react'; 3 - import { useCallback, useEffect, useRef, useState } from 'react'; 4 - import AlertError from '@/components/alert-error'; 5 - import { Button } from '@/components/ui/button'; 6 - import { 7 - Card, 8 - CardContent, 9 - CardDescription, 10 - CardHeader, 11 - CardTitle, 12 - } from '@/components/ui/card'; 13 - import { regenerateRecoveryCodes } from '@/routes/two-factor'; 14 - 15 - type Props = { 16 - recoveryCodesList: string[]; 17 - fetchRecoveryCodes: () => Promise<void>; 18 - errors: string[]; 19 - }; 20 - 21 - export default function TwoFactorRecoveryCodes({ 22 - recoveryCodesList, 23 - fetchRecoveryCodes, 24 - errors, 25 - }: Props) { 26 - const [codesAreVisible, setCodesAreVisible] = useState<boolean>(false); 27 - const codesSectionRef = useRef<HTMLDivElement | null>(null); 28 - const canRegenerateCodes = recoveryCodesList.length > 0 && codesAreVisible; 29 - 30 - const toggleCodesVisibility = useCallback(async () => { 31 - if (!codesAreVisible && !recoveryCodesList.length) { 32 - await fetchRecoveryCodes(); 33 - } 34 - 35 - setCodesAreVisible(!codesAreVisible); 36 - 37 - if (!codesAreVisible) { 38 - setTimeout(() => { 39 - codesSectionRef.current?.scrollIntoView({ 40 - behavior: 'smooth', 41 - block: 'nearest', 42 - }); 43 - }); 44 - } 45 - }, [codesAreVisible, recoveryCodesList.length, fetchRecoveryCodes]); 46 - 47 - useEffect(() => { 48 - if (!recoveryCodesList.length) { 49 - fetchRecoveryCodes(); 50 - } 51 - }, [recoveryCodesList.length, fetchRecoveryCodes]); 52 - 53 - const RecoveryCodeIconComponent = codesAreVisible ? EyeOff : Eye; 54 - 55 - return ( 56 - <Card> 57 - <CardHeader> 58 - <CardTitle className="flex gap-3"> 59 - <LockKeyhole className="size-4" aria-hidden="true" /> 60 - 2FA recovery codes 61 - </CardTitle> 62 - <CardDescription> 63 - Recovery codes let you regain access if you lose your 2FA 64 - device. Store them in a secure password manager. 65 - </CardDescription> 66 - </CardHeader> 67 - <CardContent> 68 - <div className="flex flex-col gap-3 select-none sm:flex-row sm:items-center sm:justify-between"> 69 - <Button 70 - onClick={toggleCodesVisibility} 71 - className="w-fit" 72 - aria-expanded={codesAreVisible} 73 - aria-controls="recovery-codes-section" 74 - > 75 - <RecoveryCodeIconComponent 76 - className="size-4" 77 - aria-hidden="true" 78 - /> 79 - {codesAreVisible ? 'Hide' : 'View'} recovery codes 80 - </Button> 81 - 82 - {canRegenerateCodes && ( 83 - <Form 84 - {...regenerateRecoveryCodes.form()} 85 - options={{ preserveScroll: true }} 86 - onSuccess={fetchRecoveryCodes} 87 - > 88 - {({ processing }) => ( 89 - <Button 90 - variant="secondary" 91 - type="submit" 92 - disabled={processing} 93 - aria-describedby="regenerate-warning" 94 - > 95 - <RefreshCw /> Regenerate codes 96 - </Button> 97 - )} 98 - </Form> 99 - )} 100 - </div> 101 - <div 102 - id="recovery-codes-section" 103 - className={`relative overflow-hidden transition-all duration-300 ${codesAreVisible ? 'h-auto opacity-100' : 'h-0 opacity-0'}`} 104 - aria-hidden={!codesAreVisible} 105 - > 106 - <div className="mt-3 space-y-3"> 107 - {errors?.length ? ( 108 - <AlertError errors={errors} /> 109 - ) : ( 110 - <> 111 - <div 112 - ref={codesSectionRef} 113 - className="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm" 114 - role="list" 115 - aria-label="Recovery codes" 116 - > 117 - {recoveryCodesList.length ? ( 118 - recoveryCodesList.map((code, index) => ( 119 - <div 120 - key={index} 121 - role="listitem" 122 - className="select-text" 123 - > 124 - {code} 125 - </div> 126 - )) 127 - ) : ( 128 - <div 129 - className="space-y-2" 130 - aria-label="Loading recovery codes" 131 - > 132 - {Array.from( 133 - { length: 8 }, 134 - (_, index) => ( 135 - <div 136 - key={index} 137 - className="h-4 animate-pulse rounded bg-muted-foreground/20" 138 - aria-hidden="true" 139 - /> 140 - ), 141 - )} 142 - </div> 143 - )} 144 - </div> 145 - 146 - <div className="text-xs text-muted-foreground select-none"> 147 - <p id="regenerate-warning"> 148 - Each recovery code can be used once to 149 - access your account and will be removed 150 - after use. If you need more, click{' '} 151 - <span className="font-bold"> 152 - Regenerate codes 153 - </span>{' '} 154 - above. 155 - </p> 156 - </div> 157 - </> 158 - )} 159 - </div> 160 - </div> 161 - </CardContent> 162 - </Card> 163 - ); 164 - }
-350
resources/js/components/two-factor-setup-modal.tsx
··· 1 - import { Form } from '@inertiajs/react'; 2 - import { REGEXP_ONLY_DIGITS } from 'input-otp'; 3 - import { Check, Copy, ScanLine } from 'lucide-react'; 4 - import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 5 - import AlertError from '@/components/alert-error'; 6 - import InputError from '@/components/input-error'; 7 - import { Button } from '@/components/ui/button'; 8 - import { 9 - Dialog, 10 - DialogContent, 11 - DialogDescription, 12 - DialogHeader, 13 - DialogTitle, 14 - } from '@/components/ui/dialog'; 15 - import { 16 - InputOTP, 17 - InputOTPGroup, 18 - InputOTPSlot, 19 - } from '@/components/ui/input-otp'; 20 - import { Spinner } from '@/components/ui/spinner'; 21 - import { useAppearance } from '@/hooks/use-appearance'; 22 - import { useClipboard } from '@/hooks/use-clipboard'; 23 - import { OTP_MAX_LENGTH } from '@/hooks/use-two-factor-auth'; 24 - import { confirm } from '@/routes/two-factor'; 25 - 26 - function GridScanIcon() { 27 - return ( 28 - <div className="mb-3 rounded-full border border-border bg-card p-0.5 shadow-sm"> 29 - <div className="relative overflow-hidden rounded-full border border-border bg-muted p-2.5"> 30 - <div className="absolute inset-0 grid grid-cols-5 opacity-50"> 31 - {Array.from({ length: 5 }, (_, i) => ( 32 - <div 33 - key={`col-${i + 1}`} 34 - className="border-r border-border last:border-r-0" 35 - /> 36 - ))} 37 - </div> 38 - <div className="absolute inset-0 grid grid-rows-5 opacity-50"> 39 - {Array.from({ length: 5 }, (_, i) => ( 40 - <div 41 - key={`row-${i + 1}`} 42 - className="border-b border-border last:border-b-0" 43 - /> 44 - ))} 45 - </div> 46 - <ScanLine className="relative z-20 size-6 text-foreground" /> 47 - </div> 48 - </div> 49 - ); 50 - } 51 - 52 - function TwoFactorSetupStep({ 53 - qrCodeSvg, 54 - manualSetupKey, 55 - buttonText, 56 - onNextStep, 57 - errors, 58 - }: { 59 - qrCodeSvg: string | null; 60 - manualSetupKey: string | null; 61 - buttonText: string; 62 - onNextStep: () => void; 63 - errors: string[]; 64 - }) { 65 - const { resolvedAppearance } = useAppearance(); 66 - const [copiedText, copy] = useClipboard(); 67 - const IconComponent = copiedText === manualSetupKey ? Check : Copy; 68 - 69 - return ( 70 - <> 71 - {errors?.length ? ( 72 - <AlertError errors={errors} /> 73 - ) : ( 74 - <> 75 - <div className="mx-auto flex max-w-md overflow-hidden"> 76 - <div className="mx-auto aspect-square w-64 rounded-lg border border-border"> 77 - <div className="z-10 flex h-full w-full items-center justify-center p-5"> 78 - {qrCodeSvg ? ( 79 - <div 80 - className="aspect-square w-full rounded-lg bg-white p-2 [&_svg]:size-full" 81 - dangerouslySetInnerHTML={{ 82 - __html: qrCodeSvg, 83 - }} 84 - style={{ 85 - filter: 86 - resolvedAppearance === 'dark' 87 - ? 'invert(1) brightness(1.5)' 88 - : undefined, 89 - }} 90 - /> 91 - ) : ( 92 - <Spinner /> 93 - )} 94 - </div> 95 - </div> 96 - </div> 97 - 98 - <div className="flex w-full space-x-5"> 99 - <Button className="w-full" onClick={onNextStep}> 100 - {buttonText} 101 - </Button> 102 - </div> 103 - 104 - <div className="relative flex w-full items-center justify-center"> 105 - <div className="absolute inset-0 top-1/2 h-px w-full bg-border" /> 106 - <span className="relative bg-card px-2 py-1"> 107 - or, enter the code manually 108 - </span> 109 - </div> 110 - 111 - <div className="flex w-full space-x-2"> 112 - <div className="flex w-full items-stretch overflow-hidden rounded-xl border border-border"> 113 - {!manualSetupKey ? ( 114 - <div className="flex h-full w-full items-center justify-center bg-muted p-3"> 115 - <Spinner /> 116 - </div> 117 - ) : ( 118 - <> 119 - <input 120 - type="text" 121 - readOnly 122 - value={manualSetupKey} 123 - className="h-full w-full bg-background p-3 text-foreground outline-none" 124 - /> 125 - <button 126 - onClick={() => copy(manualSetupKey)} 127 - className="border-l border-border px-3 hover:bg-muted" 128 - > 129 - <IconComponent className="w-4" /> 130 - </button> 131 - </> 132 - )} 133 - </div> 134 - </div> 135 - </> 136 - )} 137 - </> 138 - ); 139 - } 140 - 141 - function TwoFactorVerificationStep({ 142 - onClose, 143 - onBack, 144 - }: { 145 - onClose: () => void; 146 - onBack: () => void; 147 - }) { 148 - const [code, setCode] = useState<string>(''); 149 - const pinInputContainerRef = useRef<HTMLDivElement>(null); 150 - 151 - useEffect(() => { 152 - setTimeout(() => { 153 - pinInputContainerRef.current?.querySelector('input')?.focus(); 154 - }, 0); 155 - }, []); 156 - 157 - return ( 158 - <Form 159 - {...confirm.form()} 160 - onSuccess={() => onClose()} 161 - resetOnError 162 - resetOnSuccess 163 - > 164 - {({ 165 - processing, 166 - errors, 167 - }: { 168 - processing: boolean; 169 - errors?: { confirmTwoFactorAuthentication?: { code?: string } }; 170 - }) => ( 171 - <> 172 - <div 173 - ref={pinInputContainerRef} 174 - className="relative w-full space-y-3" 175 - > 176 - <div className="flex w-full flex-col items-center space-y-3 py-2"> 177 - <InputOTP 178 - id="otp" 179 - name="code" 180 - maxLength={OTP_MAX_LENGTH} 181 - onChange={setCode} 182 - disabled={processing} 183 - pattern={REGEXP_ONLY_DIGITS} 184 - > 185 - <InputOTPGroup> 186 - {Array.from( 187 - { length: OTP_MAX_LENGTH }, 188 - (_, index) => ( 189 - <InputOTPSlot 190 - key={index} 191 - index={index} 192 - /> 193 - ), 194 - )} 195 - </InputOTPGroup> 196 - </InputOTP> 197 - <InputError 198 - message={ 199 - errors?.confirmTwoFactorAuthentication?.code 200 - } 201 - /> 202 - </div> 203 - 204 - <div className="flex w-full space-x-5"> 205 - <Button 206 - type="button" 207 - variant="outline" 208 - className="flex-1" 209 - onClick={onBack} 210 - disabled={processing} 211 - > 212 - Back 213 - </Button> 214 - <Button 215 - type="submit" 216 - className="flex-1" 217 - disabled={ 218 - processing || code.length < OTP_MAX_LENGTH 219 - } 220 - > 221 - Confirm 222 - </Button> 223 - </div> 224 - </div> 225 - </> 226 - )} 227 - </Form> 228 - ); 229 - } 230 - 231 - type Props = { 232 - isOpen: boolean; 233 - onClose: () => void; 234 - requiresConfirmation: boolean; 235 - twoFactorEnabled: boolean; 236 - qrCodeSvg: string | null; 237 - manualSetupKey: string | null; 238 - clearSetupData: () => void; 239 - fetchSetupData: () => Promise<void>; 240 - errors: string[]; 241 - }; 242 - 243 - export default function TwoFactorSetupModal({ 244 - isOpen, 245 - onClose, 246 - requiresConfirmation, 247 - twoFactorEnabled, 248 - qrCodeSvg, 249 - manualSetupKey, 250 - clearSetupData, 251 - fetchSetupData, 252 - errors, 253 - }: Props) { 254 - const [showVerificationStep, setShowVerificationStep] = 255 - useState<boolean>(false); 256 - 257 - const modalConfig = useMemo<{ 258 - title: string; 259 - description: string; 260 - buttonText: string; 261 - }>(() => { 262 - if (twoFactorEnabled) { 263 - return { 264 - title: 'Two-factor authentication enabled', 265 - description: 266 - 'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.', 267 - buttonText: 'Close', 268 - }; 269 - } 270 - 271 - if (showVerificationStep) { 272 - return { 273 - title: 'Verify authentication code', 274 - description: 275 - 'Enter the 6-digit code from your authenticator app', 276 - buttonText: 'Continue', 277 - }; 278 - } 279 - 280 - return { 281 - title: 'Enable two-factor authentication', 282 - description: 283 - 'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app', 284 - buttonText: 'Continue', 285 - }; 286 - }, [twoFactorEnabled, showVerificationStep]); 287 - 288 - const resetModalState = useCallback(() => { 289 - setShowVerificationStep(false); 290 - clearSetupData(); 291 - }, [clearSetupData]); 292 - 293 - const handleClose = useCallback(() => { 294 - resetModalState(); 295 - onClose(); 296 - }, [onClose, resetModalState]); 297 - 298 - const handleModalNextStep = useCallback(() => { 299 - if (requiresConfirmation) { 300 - setShowVerificationStep(true); 301 - 302 - return; 303 - } 304 - 305 - handleClose(); 306 - }, [requiresConfirmation, handleClose]); 307 - 308 - const fetchSetupDataRef = useRef(fetchSetupData); 309 - 310 - useEffect(() => { 311 - fetchSetupDataRef.current = fetchSetupData; 312 - }, [fetchSetupData]); 313 - 314 - useEffect(() => { 315 - if (isOpen && !qrCodeSvg) { 316 - fetchSetupDataRef.current(); 317 - } 318 - }, [isOpen, qrCodeSvg]); 319 - 320 - return ( 321 - <Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}> 322 - <DialogContent className="sm:max-w-md"> 323 - <DialogHeader className="flex items-center justify-center"> 324 - <GridScanIcon /> 325 - <DialogTitle>{modalConfig.title}</DialogTitle> 326 - <DialogDescription className="text-center"> 327 - {modalConfig.description} 328 - </DialogDescription> 329 - </DialogHeader> 330 - 331 - <div className="flex flex-col items-center space-y-5"> 332 - {showVerificationStep ? ( 333 - <TwoFactorVerificationStep 334 - onClose={handleClose} 335 - onBack={() => setShowVerificationStep(false)} 336 - /> 337 - ) : ( 338 - <TwoFactorSetupStep 339 - qrCodeSvg={qrCodeSvg} 340 - manualSetupKey={manualSetupKey} 341 - buttonText={modalConfig.buttonText} 342 - onNextStep={handleModalNextStep} 343 - errors={errors} 344 - /> 345 - )} 346 - </div> 347 - </DialogContent> 348 - </Dialog> 349 - ); 350 - }
-69
resources/js/components/ui/input-otp.tsx
··· 1 - import { OTPInput, OTPInputContext } from "input-otp" 2 - import { Minus } from "lucide-react" 3 - import * as React from "react" 4 - 5 - import { cn } from "@/lib/utils" 6 - 7 - const InputOTP = React.forwardRef< 8 - React.ElementRef<typeof OTPInput>, 9 - React.ComponentPropsWithoutRef<typeof OTPInput> 10 - >(({ className, containerClassName, ...props }, ref) => ( 11 - <OTPInput 12 - ref={ref} 13 - containerClassName={cn( 14 - "flex items-center gap-2 has-[:disabled]:opacity-50", 15 - containerClassName 16 - )} 17 - className={cn("disabled:cursor-not-allowed", className)} 18 - {...props} 19 - /> 20 - )) 21 - InputOTP.displayName = "InputOTP" 22 - 23 - const InputOTPGroup = React.forwardRef< 24 - React.ElementRef<"div">, 25 - React.ComponentPropsWithoutRef<"div"> 26 - >(({ className, ...props }, ref) => ( 27 - <div ref={ref} className={cn("flex items-center", className)} {...props} /> 28 - )) 29 - InputOTPGroup.displayName = "InputOTPGroup" 30 - 31 - const InputOTPSlot = React.forwardRef< 32 - React.ElementRef<"div">, 33 - React.ComponentPropsWithoutRef<"div"> & { index: number } 34 - >(({ index, className, ...props }, ref) => { 35 - const inputOTPContext = React.useContext(OTPInputContext) 36 - const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] 37 - 38 - return ( 39 - <div 40 - ref={ref} 41 - className={cn( 42 - "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md", 43 - isActive && "z-10 ring-1 ring-ring", 44 - className 45 - )} 46 - {...props} 47 - > 48 - {char} 49 - {hasFakeCaret && ( 50 - <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> 51 - <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" /> 52 - </div> 53 - )} 54 - </div> 55 - ) 56 - }) 57 - InputOTPSlot.displayName = "InputOTPSlot" 58 - 59 - const InputOTPSeparator = React.forwardRef< 60 - React.ElementRef<"div">, 61 - React.ComponentPropsWithoutRef<"div"> 62 - >(({ ...props }, ref) => ( 63 - <div ref={ref} role="separator" {...props}> 64 - <Minus /> 65 - </div> 66 - )) 67 - InputOTPSeparator.displayName = "InputOTPSeparator" 68 - 69 - export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
+2 -2
resources/js/components/user-menu-content.tsx
··· 8 8 } from '@/components/ui/dropdown-menu'; 9 9 import { UserInfo } from '@/components/user-info'; 10 10 import { useMobileNavigation } from '@/hooks/use-mobile-navigation'; 11 - import { logout } from '@/routes'; 12 11 import { edit } from '@/routes/profile'; 13 12 import type { User } from '@/types'; 14 13 ··· 49 48 <DropdownMenuItem asChild> 50 49 <Link 51 50 className="block w-full cursor-pointer" 52 - href={logout()} 51 + href="/logout" 52 + method="post" 53 53 as="button" 54 54 onClick={handleLogout} 55 55 data-test="logout-button"
-111
resources/js/hooks/use-two-factor-auth.ts
··· 1 - import { useHttp } from '@inertiajs/react'; 2 - import { useCallback, useState } from 'react'; 3 - import { qrCode, recoveryCodes, secretKey } from '@/routes/two-factor'; 4 - 5 - export type UseTwoFactorAuthReturn = { 6 - qrCodeSvg: string | null; 7 - manualSetupKey: string | null; 8 - recoveryCodesList: string[]; 9 - hasSetupData: boolean; 10 - errors: string[]; 11 - clearErrors: () => void; 12 - clearSetupData: () => void; 13 - clearTwoFactorAuthData: () => void; 14 - fetchQrCode: () => Promise<void>; 15 - fetchSetupKey: () => Promise<void>; 16 - fetchSetupData: () => Promise<void>; 17 - fetchRecoveryCodes: () => Promise<void>; 18 - }; 19 - 20 - export const OTP_MAX_LENGTH = 6; 21 - 22 - export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => { 23 - const { submit } = useHttp(); 24 - 25 - const [qrCodeSvg, setQrCodeSvg] = useState<string | null>(null); 26 - const [manualSetupKey, setManualSetupKey] = useState<string | null>(null); 27 - const [recoveryCodesList, setRecoveryCodesList] = useState<string[]>([]); 28 - const [errors, setErrors] = useState<string[]>([]); 29 - 30 - const hasSetupData = qrCodeSvg !== null && manualSetupKey !== null; 31 - 32 - const clearErrors = useCallback((): void => { 33 - setErrors([]); 34 - }, []); 35 - 36 - const clearSetupData = useCallback((): void => { 37 - setManualSetupKey(null); 38 - setQrCodeSvg(null); 39 - setErrors([]); 40 - }, []); 41 - 42 - const clearTwoFactorAuthData = useCallback((): void => { 43 - setManualSetupKey(null); 44 - setQrCodeSvg(null); 45 - setErrors([]); 46 - setRecoveryCodesList([]); 47 - }, []); 48 - 49 - const fetchQrCode = useCallback(async (): Promise<void> => { 50 - try { 51 - const { svg } = (await submit(qrCode())) as { 52 - svg: string; 53 - url: string; 54 - }; 55 - 56 - setQrCodeSvg(svg); 57 - } catch { 58 - setErrors((prev) => [...prev, 'Failed to fetch QR code']); 59 - setQrCodeSvg(null); 60 - } 61 - }, [submit]); 62 - 63 - const fetchSetupKey = useCallback(async (): Promise<void> => { 64 - try { 65 - const { secretKey: key } = (await submit(secretKey())) as { 66 - secretKey: string; 67 - }; 68 - 69 - setManualSetupKey(key); 70 - } catch { 71 - setErrors((prev) => [...prev, 'Failed to fetch a setup key']); 72 - setManualSetupKey(null); 73 - } 74 - }, [submit]); 75 - 76 - const fetchRecoveryCodes = useCallback(async (): Promise<void> => { 77 - try { 78 - setErrors([]); 79 - const codes = (await submit(recoveryCodes())) as string[]; 80 - setRecoveryCodesList(codes); 81 - } catch { 82 - setErrors((prev) => [...prev, 'Failed to fetch recovery codes']); 83 - setRecoveryCodesList([]); 84 - } 85 - }, [submit]); 86 - 87 - const fetchSetupData = useCallback(async (): Promise<void> => { 88 - try { 89 - setErrors([]); 90 - await Promise.all([fetchQrCode(), fetchSetupKey()]); 91 - } catch { 92 - setQrCodeSvg(null); 93 - setManualSetupKey(null); 94 - } 95 - }, [fetchQrCode, fetchSetupKey]); 96 - 97 - return { 98 - qrCodeSvg, 99 - manualSetupKey, 100 - recoveryCodesList, 101 - hasSetupData, 102 - errors, 103 - clearErrors, 104 - clearSetupData, 105 - clearTwoFactorAuthData, 106 - fetchQrCode, 107 - fetchSetupKey, 108 - fetchSetupData, 109 - fetchRecoveryCodes, 110 - }; 111 - };
-51
resources/js/pages/auth/confirm-password.tsx
··· 1 - import { Form, Head } from '@inertiajs/react'; 2 - import InputError from '@/components/input-error'; 3 - import PasswordInput from '@/components/password-input'; 4 - import { Button } from '@/components/ui/button'; 5 - import { Label } from '@/components/ui/label'; 6 - import { Spinner } from '@/components/ui/spinner'; 7 - import { store } from '@/routes/password/confirm'; 8 - 9 - export default function ConfirmPassword() { 10 - return ( 11 - <> 12 - <Head title="Confirm password" /> 13 - 14 - <Form {...store.form()} resetOnSuccess={['password']}> 15 - {({ processing, errors }) => ( 16 - <div className="space-y-6"> 17 - <div className="grid gap-2"> 18 - <Label htmlFor="password">Password</Label> 19 - <PasswordInput 20 - id="password" 21 - name="password" 22 - placeholder="Password" 23 - autoComplete="current-password" 24 - autoFocus 25 - /> 26 - 27 - <InputError message={errors.password} /> 28 - </div> 29 - 30 - <div className="flex items-center"> 31 - <Button 32 - className="w-full" 33 - disabled={processing} 34 - data-test="confirm-password-button" 35 - > 36 - {processing && <Spinner />} 37 - Confirm password 38 - </Button> 39 - </div> 40 - </div> 41 - )} 42 - </Form> 43 - </> 44 - ); 45 - } 46 - 47 - ConfirmPassword.layout = { 48 - title: 'Confirm your password', 49 - description: 50 - 'This is a secure area of the application. Please confirm your password before continuing.', 51 - };
-69
resources/js/pages/auth/forgot-password.tsx
··· 1 - // Components 2 - import { Form, Head } from '@inertiajs/react'; 3 - import { LoaderCircle } from 'lucide-react'; 4 - import InputError from '@/components/input-error'; 5 - import TextLink from '@/components/text-link'; 6 - import { Button } from '@/components/ui/button'; 7 - import { Input } from '@/components/ui/input'; 8 - import { Label } from '@/components/ui/label'; 9 - import { login } from '@/routes'; 10 - import { email } from '@/routes/password'; 11 - 12 - export default function ForgotPassword({ status }: { status?: string }) { 13 - return ( 14 - <> 15 - <Head title="Forgot password" /> 16 - 17 - {status && ( 18 - <div className="mb-4 text-center text-sm font-medium text-green-600"> 19 - {status} 20 - </div> 21 - )} 22 - 23 - <div className="space-y-6"> 24 - <Form {...email.form()}> 25 - {({ processing, errors }) => ( 26 - <> 27 - <div className="grid gap-2"> 28 - <Label htmlFor="email">Email address</Label> 29 - <Input 30 - id="email" 31 - type="email" 32 - name="email" 33 - autoComplete="off" 34 - autoFocus 35 - placeholder="email@example.com" 36 - /> 37 - 38 - <InputError message={errors.email} /> 39 - </div> 40 - 41 - <div className="my-6 flex items-center justify-start"> 42 - <Button 43 - className="w-full" 44 - disabled={processing} 45 - data-test="email-password-reset-link-button" 46 - > 47 - {processing && ( 48 - <LoaderCircle className="h-4 w-4 animate-spin" /> 49 - )} 50 - Email password reset link 51 - </Button> 52 - </div> 53 - </> 54 - )} 55 - </Form> 56 - 57 - <div className="space-x-1 text-center text-sm text-muted-foreground"> 58 - <span>Or, return to</span> 59 - <TextLink href={login()}>log in</TextLink> 60 - </div> 61 - </div> 62 - </> 63 - ); 64 - } 65 - 66 - ForgotPassword.layout = { 67 - title: 'Forgot password', 68 - description: 'Enter your email to receive a password reset link', 69 - };
-121
resources/js/pages/auth/login.tsx
··· 1 - import { Form, Head } from '@inertiajs/react'; 2 - import InputError from '@/components/input-error'; 3 - import PasswordInput from '@/components/password-input'; 4 - import TextLink from '@/components/text-link'; 5 - import { Button } from '@/components/ui/button'; 6 - import { Checkbox } from '@/components/ui/checkbox'; 7 - import { Input } from '@/components/ui/input'; 8 - import { Label } from '@/components/ui/label'; 9 - import { Spinner } from '@/components/ui/spinner'; 10 - import { register } from '@/routes'; 11 - import { store } from '@/routes/login'; 12 - import { request } from '@/routes/password'; 13 - 14 - type Props = { 15 - status?: string; 16 - canResetPassword: boolean; 17 - canRegister: boolean; 18 - }; 19 - 20 - export default function Login({ 21 - status, 22 - canResetPassword, 23 - canRegister, 24 - }: Props) { 25 - return ( 26 - <> 27 - <Head title="Log in" /> 28 - 29 - <Form 30 - {...store.form()} 31 - resetOnSuccess={['password']} 32 - className="flex flex-col gap-6" 33 - > 34 - {({ processing, errors }) => ( 35 - <> 36 - <div className="grid gap-6"> 37 - <div className="grid gap-2"> 38 - <Label htmlFor="email">Email address</Label> 39 - <Input 40 - id="email" 41 - type="email" 42 - name="email" 43 - required 44 - autoFocus 45 - tabIndex={1} 46 - autoComplete="email" 47 - placeholder="email@example.com" 48 - /> 49 - <InputError message={errors.email} /> 50 - </div> 51 - 52 - <div className="grid gap-2"> 53 - <div className="flex items-center"> 54 - <Label htmlFor="password">Password</Label> 55 - {canResetPassword && ( 56 - <TextLink 57 - href={request()} 58 - className="ml-auto text-sm" 59 - tabIndex={5} 60 - > 61 - Forgot password? 62 - </TextLink> 63 - )} 64 - </div> 65 - <PasswordInput 66 - id="password" 67 - name="password" 68 - required 69 - tabIndex={2} 70 - autoComplete="current-password" 71 - placeholder="Password" 72 - /> 73 - <InputError message={errors.password} /> 74 - </div> 75 - 76 - <div className="flex items-center space-x-3"> 77 - <Checkbox 78 - id="remember" 79 - name="remember" 80 - tabIndex={3} 81 - /> 82 - <Label htmlFor="remember">Remember me</Label> 83 - </div> 84 - 85 - <Button 86 - type="submit" 87 - className="mt-4 w-full" 88 - tabIndex={4} 89 - disabled={processing} 90 - data-test="login-button" 91 - > 92 - {processing && <Spinner />} 93 - Log in 94 - </Button> 95 - </div> 96 - 97 - {canRegister && ( 98 - <div className="text-center text-sm text-muted-foreground"> 99 - Don't have an account?{' '} 100 - <TextLink href={register()} tabIndex={5}> 101 - Sign up 102 - </TextLink> 103 - </div> 104 - )} 105 - </> 106 - )} 107 - </Form> 108 - 109 - {status && ( 110 - <div className="mb-4 text-center text-sm font-medium text-green-600"> 111 - {status} 112 - </div> 113 - )} 114 - </> 115 - ); 116 - } 117 - 118 - Login.layout = { 119 - title: 'Log in to your account', 120 - description: 'Enter your email and password below to log in', 121 - };
-114
resources/js/pages/auth/register.tsx
··· 1 - import { Form, Head } from '@inertiajs/react'; 2 - import InputError from '@/components/input-error'; 3 - import PasswordInput from '@/components/password-input'; 4 - import TextLink from '@/components/text-link'; 5 - import { Button } from '@/components/ui/button'; 6 - import { Input } from '@/components/ui/input'; 7 - import { Label } from '@/components/ui/label'; 8 - import { Spinner } from '@/components/ui/spinner'; 9 - import { login } from '@/routes'; 10 - import { store } from '@/routes/register'; 11 - 12 - export default function Register() { 13 - return ( 14 - <> 15 - <Head title="Register" /> 16 - <Form 17 - {...store.form()} 18 - resetOnSuccess={['password', 'password_confirmation']} 19 - disableWhileProcessing 20 - className="flex flex-col gap-6" 21 - > 22 - {({ processing, errors }) => ( 23 - <> 24 - <div className="grid gap-6"> 25 - <div className="grid gap-2"> 26 - <Label htmlFor="name">Name</Label> 27 - <Input 28 - id="name" 29 - type="text" 30 - required 31 - autoFocus 32 - tabIndex={1} 33 - autoComplete="name" 34 - name="name" 35 - placeholder="Full name" 36 - /> 37 - <InputError 38 - message={errors.name} 39 - className="mt-2" 40 - /> 41 - </div> 42 - 43 - <div className="grid gap-2"> 44 - <Label htmlFor="email">Email address</Label> 45 - <Input 46 - id="email" 47 - type="email" 48 - required 49 - tabIndex={2} 50 - autoComplete="email" 51 - name="email" 52 - placeholder="email@example.com" 53 - /> 54 - <InputError message={errors.email} /> 55 - </div> 56 - 57 - <div className="grid gap-2"> 58 - <Label htmlFor="password">Password</Label> 59 - <PasswordInput 60 - id="password" 61 - required 62 - tabIndex={3} 63 - autoComplete="new-password" 64 - name="password" 65 - placeholder="Password" 66 - /> 67 - <InputError message={errors.password} /> 68 - </div> 69 - 70 - <div className="grid gap-2"> 71 - <Label htmlFor="password_confirmation"> 72 - Confirm password 73 - </Label> 74 - <PasswordInput 75 - id="password_confirmation" 76 - required 77 - tabIndex={4} 78 - autoComplete="new-password" 79 - name="password_confirmation" 80 - placeholder="Confirm password" 81 - /> 82 - <InputError 83 - message={errors.password_confirmation} 84 - /> 85 - </div> 86 - 87 - <Button 88 - type="submit" 89 - className="mt-2 w-full" 90 - tabIndex={5} 91 - data-test="register-user-button" 92 - > 93 - {processing && <Spinner />} 94 - Create account 95 - </Button> 96 - </div> 97 - 98 - <div className="text-center text-sm text-muted-foreground"> 99 - Already have an account?{' '} 100 - <TextLink href={login()} tabIndex={6}> 101 - Log in 102 - </TextLink> 103 - </div> 104 - </> 105 - )} 106 - </Form> 107 - </> 108 - ); 109 - } 110 - 111 - Register.layout = { 112 - title: 'Create an account', 113 - description: 'Enter your details below to create your account', 114 - };
-93
resources/js/pages/auth/reset-password.tsx
··· 1 - import { Form, Head } from '@inertiajs/react'; 2 - import InputError from '@/components/input-error'; 3 - import PasswordInput from '@/components/password-input'; 4 - import { Button } from '@/components/ui/button'; 5 - import { Input } from '@/components/ui/input'; 6 - import { Label } from '@/components/ui/label'; 7 - import { Spinner } from '@/components/ui/spinner'; 8 - import { update } from '@/routes/password'; 9 - 10 - type Props = { 11 - token: string; 12 - email: string; 13 - }; 14 - 15 - export default function ResetPassword({ token, email }: Props) { 16 - return ( 17 - <> 18 - <Head title="Reset password" /> 19 - 20 - <Form 21 - {...update.form()} 22 - transform={(data) => ({ ...data, token, email })} 23 - resetOnSuccess={['password', 'password_confirmation']} 24 - > 25 - {({ processing, errors }) => ( 26 - <div className="grid gap-6"> 27 - <div className="grid gap-2"> 28 - <Label htmlFor="email">Email</Label> 29 - <Input 30 - id="email" 31 - type="email" 32 - name="email" 33 - autoComplete="email" 34 - value={email} 35 - className="mt-1 block w-full" 36 - readOnly 37 - /> 38 - <InputError 39 - message={errors.email} 40 - className="mt-2" 41 - /> 42 - </div> 43 - 44 - <div className="grid gap-2"> 45 - <Label htmlFor="password">Password</Label> 46 - <PasswordInput 47 - id="password" 48 - name="password" 49 - autoComplete="new-password" 50 - className="mt-1 block w-full" 51 - autoFocus 52 - placeholder="Password" 53 - /> 54 - <InputError message={errors.password} /> 55 - </div> 56 - 57 - <div className="grid gap-2"> 58 - <Label htmlFor="password_confirmation"> 59 - Confirm password 60 - </Label> 61 - <PasswordInput 62 - id="password_confirmation" 63 - name="password_confirmation" 64 - autoComplete="new-password" 65 - className="mt-1 block w-full" 66 - placeholder="Confirm password" 67 - /> 68 - <InputError 69 - message={errors.password_confirmation} 70 - className="mt-2" 71 - /> 72 - </div> 73 - 74 - <Button 75 - type="submit" 76 - className="mt-4 w-full" 77 - disabled={processing} 78 - data-test="reset-password-button" 79 - > 80 - {processing && <Spinner />} 81 - Reset password 82 - </Button> 83 - </div> 84 - )} 85 - </Form> 86 - </> 87 - ); 88 - } 89 - 90 - ResetPassword.layout = { 91 - title: 'Reset password', 92 - description: 'Please enter your new password below', 93 - };
-132
resources/js/pages/auth/two-factor-challenge.tsx
··· 1 - import { Form, Head, setLayoutProps } from '@inertiajs/react'; 2 - import { REGEXP_ONLY_DIGITS } from 'input-otp'; 3 - import { useMemo, useState } from 'react'; 4 - import InputError from '@/components/input-error'; 5 - import { Button } from '@/components/ui/button'; 6 - import { Input } from '@/components/ui/input'; 7 - import { 8 - InputOTP, 9 - InputOTPGroup, 10 - InputOTPSlot, 11 - } from '@/components/ui/input-otp'; 12 - import { OTP_MAX_LENGTH } from '@/hooks/use-two-factor-auth'; 13 - import { store } from '@/routes/two-factor/login'; 14 - 15 - export default function TwoFactorChallenge() { 16 - const [showRecoveryInput, setShowRecoveryInput] = useState<boolean>(false); 17 - const [code, setCode] = useState<string>(''); 18 - 19 - const authConfigContent = useMemo<{ 20 - title: string; 21 - description: string; 22 - toggleText: string; 23 - }>(() => { 24 - if (showRecoveryInput) { 25 - return { 26 - title: 'Recovery code', 27 - description: 28 - 'Please confirm access to your account by entering one of your emergency recovery codes.', 29 - toggleText: 'login using an authentication code', 30 - }; 31 - } 32 - 33 - return { 34 - title: 'Authentication code', 35 - description: 36 - 'Enter the authentication code provided by your authenticator application.', 37 - toggleText: 'login using a recovery code', 38 - }; 39 - }, [showRecoveryInput]); 40 - 41 - setLayoutProps({ 42 - title: authConfigContent.title, 43 - description: authConfigContent.description, 44 - }); 45 - 46 - const toggleRecoveryMode = (clearErrors: () => void): void => { 47 - setShowRecoveryInput(!showRecoveryInput); 48 - clearErrors(); 49 - setCode(''); 50 - }; 51 - 52 - return ( 53 - <> 54 - <Head title="Two-factor authentication" /> 55 - 56 - <div className="space-y-6"> 57 - <Form 58 - {...store.form()} 59 - className="space-y-4" 60 - resetOnError 61 - resetOnSuccess={!showRecoveryInput} 62 - > 63 - {({ errors, processing, clearErrors }) => ( 64 - <> 65 - {showRecoveryInput ? ( 66 - <> 67 - <Input 68 - name="recovery_code" 69 - type="text" 70 - placeholder="Enter recovery code" 71 - autoFocus={showRecoveryInput} 72 - required 73 - /> 74 - <InputError 75 - message={errors.recovery_code} 76 - /> 77 - </> 78 - ) : ( 79 - <div className="flex flex-col items-center justify-center space-y-3 text-center"> 80 - <div className="flex w-full items-center justify-center"> 81 - <InputOTP 82 - name="code" 83 - maxLength={OTP_MAX_LENGTH} 84 - value={code} 85 - onChange={(value) => setCode(value)} 86 - disabled={processing} 87 - pattern={REGEXP_ONLY_DIGITS} 88 - > 89 - <InputOTPGroup> 90 - {Array.from( 91 - { length: OTP_MAX_LENGTH }, 92 - (_, index) => ( 93 - <InputOTPSlot 94 - key={index} 95 - index={index} 96 - /> 97 - ), 98 - )} 99 - </InputOTPGroup> 100 - </InputOTP> 101 - </div> 102 - <InputError message={errors.code} /> 103 - </div> 104 - )} 105 - 106 - <Button 107 - type="submit" 108 - className="w-full" 109 - disabled={processing} 110 - > 111 - Continue 112 - </Button> 113 - 114 - <div className="text-center text-sm text-muted-foreground"> 115 - <span>or you can </span> 116 - <button 117 - type="button" 118 - className="cursor-pointer text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500" 119 - onClick={() => 120 - toggleRecoveryMode(clearErrors) 121 - } 122 - > 123 - {authConfigContent.toggleText} 124 - </button> 125 - </div> 126 - </> 127 - )} 128 - </Form> 129 - </div> 130 - </> 131 - ); 132 - }
-46
resources/js/pages/auth/verify-email.tsx
··· 1 - // Components 2 - import { Form, Head } from '@inertiajs/react'; 3 - import TextLink from '@/components/text-link'; 4 - import { Button } from '@/components/ui/button'; 5 - import { Spinner } from '@/components/ui/spinner'; 6 - import { logout } from '@/routes'; 7 - import { send } from '@/routes/verification'; 8 - 9 - export default function VerifyEmail({ status }: { status?: string }) { 10 - return ( 11 - <> 12 - <Head title="Email verification" /> 13 - 14 - {status === 'verification-link-sent' && ( 15 - <div className="mb-4 text-center text-sm font-medium text-green-600"> 16 - A new verification link has been sent to the email address 17 - you provided during registration. 18 - </div> 19 - )} 20 - 21 - <Form {...send.form()} className="space-y-6 text-center"> 22 - {({ processing }) => ( 23 - <> 24 - <Button disabled={processing} variant="secondary"> 25 - {processing && <Spinner />} 26 - Resend verification email 27 - </Button> 28 - 29 - <TextLink 30 - href={logout()} 31 - className="mx-auto block text-sm" 32 - > 33 - Log out 34 - </TextLink> 35 - </> 36 - )} 37 - </Form> 38 - </> 39 - ); 40 - } 41 - 42 - VerifyEmail.layout = { 43 - title: 'Verify email', 44 - description: 45 - 'Please verify your email address by clicking on the link we just emailed to you.', 46 - };
+2 -34
resources/js/pages/settings/profile.tsx
··· 1 - import { Form, Head, Link, usePage } from '@inertiajs/react'; 1 + import { Form, Head, usePage } from '@inertiajs/react'; 2 2 import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController'; 3 3 import DeleteUser from '@/components/delete-user'; 4 4 import Heading from '@/components/heading'; ··· 7 7 import { Input } from '@/components/ui/input'; 8 8 import { Label } from '@/components/ui/label'; 9 9 import { edit } from '@/routes/profile'; 10 - import { send } from '@/routes/verification'; 11 10 12 - export default function Profile({ 13 - mustVerifyEmail, 14 - status, 15 - }: { 16 - mustVerifyEmail: boolean; 17 - status?: string; 18 - }) { 11 + export default function Profile() { 19 12 const { auth } = usePage().props; 20 13 21 14 return ( ··· 78 71 message={errors.email} 79 72 /> 80 73 </div> 81 - 82 - {mustVerifyEmail && 83 - auth.user.email_verified_at === null && ( 84 - <div> 85 - <p className="-mt-4 text-sm text-muted-foreground"> 86 - Your email address is unverified.{' '} 87 - <Link 88 - href={send()} 89 - as="button" 90 - className="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500" 91 - > 92 - Click here to resend the 93 - verification email. 94 - </Link> 95 - </p> 96 - 97 - {status === 98 - 'verification-link-sent' && ( 99 - <div className="mt-2 text-sm font-medium text-green-600"> 100 - A new verification link has been 101 - sent to your email address. 102 - </div> 103 - )} 104 - </div> 105 - )} 106 74 107 75 <div className="flex items-center gap-4"> 108 76 <Button
-249
resources/js/pages/settings/security.tsx
··· 1 - import { Form, Head } from '@inertiajs/react'; 2 - import { ShieldCheck } from 'lucide-react'; 3 - import { useEffect, useRef, useState } from 'react'; 4 - import SecurityController from '@/actions/App/Http/Controllers/Settings/SecurityController'; 5 - import Heading from '@/components/heading'; 6 - import InputError from '@/components/input-error'; 7 - import PasswordInput from '@/components/password-input'; 8 - import TwoFactorRecoveryCodes from '@/components/two-factor-recovery-codes'; 9 - import TwoFactorSetupModal from '@/components/two-factor-setup-modal'; 10 - import { Button } from '@/components/ui/button'; 11 - import { Label } from '@/components/ui/label'; 12 - import { useTwoFactorAuth } from '@/hooks/use-two-factor-auth'; 13 - import { edit } from '@/routes/security'; 14 - import { disable, enable } from '@/routes/two-factor'; 15 - 16 - type Props = { 17 - canManageTwoFactor?: boolean; 18 - requiresConfirmation?: boolean; 19 - twoFactorEnabled?: boolean; 20 - }; 21 - 22 - export default function Security({ 23 - canManageTwoFactor = false, 24 - requiresConfirmation = false, 25 - twoFactorEnabled = false, 26 - }: Props) { 27 - const passwordInput = useRef<HTMLInputElement>(null); 28 - const currentPasswordInput = useRef<HTMLInputElement>(null); 29 - 30 - const { 31 - qrCodeSvg, 32 - hasSetupData, 33 - manualSetupKey, 34 - clearSetupData, 35 - clearTwoFactorAuthData, 36 - fetchSetupData, 37 - recoveryCodesList, 38 - fetchRecoveryCodes, 39 - errors, 40 - } = useTwoFactorAuth(); 41 - const [showSetupModal, setShowSetupModal] = useState<boolean>(false); 42 - const prevTwoFactorEnabled = useRef(twoFactorEnabled); 43 - 44 - useEffect(() => { 45 - if (prevTwoFactorEnabled.current && !twoFactorEnabled) { 46 - clearTwoFactorAuthData(); 47 - } 48 - 49 - prevTwoFactorEnabled.current = twoFactorEnabled; 50 - }, [twoFactorEnabled, clearTwoFactorAuthData]); 51 - 52 - return ( 53 - <> 54 - <Head title="Security settings" /> 55 - 56 - <h1 className="sr-only">Security settings</h1> 57 - 58 - <div className="space-y-6"> 59 - <Heading 60 - variant="small" 61 - title="Update password" 62 - description="Ensure your account is using a long, random password to stay secure" 63 - /> 64 - 65 - <Form 66 - {...SecurityController.update.form()} 67 - options={{ 68 - preserveScroll: true, 69 - }} 70 - resetOnError={[ 71 - 'password', 72 - 'password_confirmation', 73 - 'current_password', 74 - ]} 75 - resetOnSuccess 76 - onError={(errors) => { 77 - if (errors.password) { 78 - passwordInput.current?.focus(); 79 - } 80 - 81 - if (errors.current_password) { 82 - currentPasswordInput.current?.focus(); 83 - } 84 - }} 85 - className="space-y-6" 86 - > 87 - {({ errors, processing }) => ( 88 - <> 89 - <div className="grid gap-2"> 90 - <Label htmlFor="current_password"> 91 - Current password 92 - </Label> 93 - 94 - <PasswordInput 95 - id="current_password" 96 - ref={currentPasswordInput} 97 - name="current_password" 98 - className="mt-1 block w-full" 99 - autoComplete="current-password" 100 - placeholder="Current password" 101 - /> 102 - 103 - <InputError message={errors.current_password} /> 104 - </div> 105 - 106 - <div className="grid gap-2"> 107 - <Label htmlFor="password">New password</Label> 108 - 109 - <PasswordInput 110 - id="password" 111 - ref={passwordInput} 112 - name="password" 113 - className="mt-1 block w-full" 114 - autoComplete="new-password" 115 - placeholder="New password" 116 - /> 117 - 118 - <InputError message={errors.password} /> 119 - </div> 120 - 121 - <div className="grid gap-2"> 122 - <Label htmlFor="password_confirmation"> 123 - Confirm password 124 - </Label> 125 - 126 - <PasswordInput 127 - id="password_confirmation" 128 - name="password_confirmation" 129 - className="mt-1 block w-full" 130 - autoComplete="new-password" 131 - placeholder="Confirm password" 132 - /> 133 - 134 - <InputError 135 - message={errors.password_confirmation} 136 - /> 137 - </div> 138 - 139 - <div className="flex items-center gap-4"> 140 - <Button 141 - disabled={processing} 142 - data-test="update-password-button" 143 - > 144 - Save password 145 - </Button> 146 - </div> 147 - </> 148 - )} 149 - </Form> 150 - </div> 151 - 152 - {canManageTwoFactor && ( 153 - <div className="space-y-6"> 154 - <Heading 155 - variant="small" 156 - title="Two-factor authentication" 157 - description="Manage your two-factor authentication settings" 158 - /> 159 - {twoFactorEnabled ? ( 160 - <div className="flex flex-col items-start justify-start space-y-4"> 161 - <p className="text-sm text-muted-foreground"> 162 - You will be prompted for a secure, random pin 163 - during login, which you can retrieve from the 164 - TOTP-supported application on your phone. 165 - </p> 166 - 167 - <div className="relative inline"> 168 - <Form {...disable.form()}> 169 - {({ processing }) => ( 170 - <Button 171 - variant="destructive" 172 - type="submit" 173 - disabled={processing} 174 - > 175 - Disable 2FA 176 - </Button> 177 - )} 178 - </Form> 179 - </div> 180 - 181 - <TwoFactorRecoveryCodes 182 - recoveryCodesList={recoveryCodesList} 183 - fetchRecoveryCodes={fetchRecoveryCodes} 184 - errors={errors} 185 - /> 186 - </div> 187 - ) : ( 188 - <div className="flex flex-col items-start justify-start space-y-4"> 189 - <p className="text-sm text-muted-foreground"> 190 - When you enable two-factor authentication, you 191 - will be prompted for a secure pin during login. 192 - This pin can be retrieved from a TOTP-supported 193 - application on your phone. 194 - </p> 195 - 196 - <div> 197 - {hasSetupData ? ( 198 - <Button 199 - onClick={() => setShowSetupModal(true)} 200 - > 201 - <ShieldCheck /> 202 - Continue setup 203 - </Button> 204 - ) : ( 205 - <Form 206 - {...enable.form()} 207 - onSuccess={() => 208 - setShowSetupModal(true) 209 - } 210 - > 211 - {({ processing }) => ( 212 - <Button 213 - type="submit" 214 - disabled={processing} 215 - > 216 - Enable 2FA 217 - </Button> 218 - )} 219 - </Form> 220 - )} 221 - </div> 222 - </div> 223 - )} 224 - 225 - <TwoFactorSetupModal 226 - isOpen={showSetupModal} 227 - onClose={() => setShowSetupModal(false)} 228 - requiresConfirmation={requiresConfirmation} 229 - twoFactorEnabled={twoFactorEnabled} 230 - qrCodeSvg={qrCodeSvg} 231 - manualSetupKey={manualSetupKey} 232 - clearSetupData={clearSetupData} 233 - fetchSetupData={fetchSetupData} 234 - errors={errors} 235 - /> 236 - </div> 237 - )} 238 - </> 239 - ); 240 - } 241 - 242 - Security.layout = { 243 - breadcrumbs: [ 244 - { 245 - title: 'Security settings', 246 - href: edit(), 247 - }, 248 - ], 249 - };
+8 -22
resources/js/pages/welcome.tsx
··· 1 1 import { Head, Link, usePage } from '@inertiajs/react'; 2 - import { dashboard, login, register } from '@/routes'; 2 + import { dashboard } from '@/routes'; 3 3 4 - export default function Welcome({ 5 - canRegister = true, 6 - }: { 7 - canRegister?: boolean; 8 - }) { 4 + export default function Welcome() { 9 5 const { auth } = usePage().props; 10 6 11 7 return ( ··· 28 24 Dashboard 29 25 </Link> 30 26 ) : ( 31 - <> 32 - <Link 33 - href={login()} 34 - className="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]" 35 - > 36 - Log in 37 - </Link> 38 - {canRegister && ( 39 - <Link 40 - href={register()} 41 - className="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]" 42 - > 43 - Register 44 - </Link> 45 - )} 46 - </> 27 + <Link 28 + href="/login" 29 + className="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]" 30 + > 31 + Log in 32 + </Link> 47 33 )} 48 34 </nav> 49 35 </header>
-10
resources/js/types/auth.ts
··· 4 4 email: string; 5 5 avatar?: string; 6 6 email_verified_at: string | null; 7 - two_factor_enabled?: boolean; 8 7 created_at: string; 9 8 updated_at: string; 10 9 [key: string]: unknown; ··· 13 12 export type Auth = { 14 13 user: User; 15 14 }; 16 - 17 - export type TwoFactorSetupData = { 18 - svg: string; 19 - url: string; 20 - }; 21 - 22 - export type TwoFactorSecretKey = { 23 - secretKey: string; 24 - };
-10
routes/settings.php
··· 1 1 <?php 2 2 3 3 use App\Http\Controllers\Settings\ProfileController; 4 - use App\Http\Controllers\Settings\SecurityController; 5 4 use Illuminate\Support\Facades\Route; 6 5 7 6 Route::middleware(['auth'])->group(function () { ··· 9 8 10 9 Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit'); 11 10 Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update'); 12 - }); 13 - 14 - Route::middleware(['auth', 'verified'])->group(function () { 15 11 Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 16 - 17 - Route::get('settings/security', [SecurityController::class, 'edit'])->name('security.edit'); 18 - 19 - Route::put('settings/password', [SecurityController::class, 'update']) 20 - ->middleware('throttle:6,1') 21 - ->name('user-password.update'); 22 12 23 13 Route::inertia('settings/appearance', 'settings/appearance')->name('appearance.edit'); 24 14 });
+2 -5
routes/web.php
··· 1 1 <?php 2 2 3 3 use Illuminate\Support\Facades\Route; 4 - use Laravel\Fortify\Features; 5 4 6 - Route::inertia('/', 'welcome', [ 7 - 'canRegister' => Features::enabled(Features::registration()), 8 - ])->name('home'); 5 + Route::inertia('/', 'welcome')->name('home'); 9 6 10 - Route::middleware(['auth', 'verified'])->group(function () { 7 + Route::middleware(['auth'])->group(function () { 11 8 Route::inertia('dashboard', 'dashboard')->name('dashboard'); 12 9 }); 13 10
-82
tests/Feature/Auth/AuthenticationTest.php
··· 1 - <?php 2 - 3 - use App\Models\User; 4 - use Illuminate\Support\Facades\RateLimiter; 5 - use Laravel\Fortify\Features; 6 - 7 - test('login screen can be rendered', function () { 8 - $response = $this->get(route('login')); 9 - 10 - $response->assertOk(); 11 - }); 12 - 13 - test('users can authenticate using the login screen', function () { 14 - $user = User::factory()->create(); 15 - 16 - $response = $this->post(route('login.store'), [ 17 - 'email' => $user->email, 18 - 'password' => 'password', 19 - ]); 20 - 21 - $this->assertAuthenticated(); 22 - $response->assertRedirect(route('dashboard', absolute: false)); 23 - }); 24 - 25 - test('users with two factor enabled are redirected to two factor challenge', function () { 26 - $this->skipUnlessFortifyHas(Features::twoFactorAuthentication()); 27 - 28 - Features::twoFactorAuthentication([ 29 - 'confirm' => true, 30 - 'confirmPassword' => true, 31 - ]); 32 - 33 - $user = User::factory()->create(); 34 - 35 - $user->forceFill([ 36 - 'two_factor_secret' => encrypt('test-secret'), 37 - 'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])), 38 - 'two_factor_confirmed_at' => now(), 39 - ])->save(); 40 - 41 - $response = $this->post(route('login'), [ 42 - 'email' => $user->email, 43 - 'password' => 'password', 44 - ]); 45 - 46 - $response->assertRedirect(route('two-factor.login')); 47 - $response->assertSessionHas('login.id', $user->id); 48 - $this->assertGuest(); 49 - }); 50 - 51 - test('users can not authenticate with invalid password', function () { 52 - $user = User::factory()->create(); 53 - 54 - $this->post(route('login.store'), [ 55 - 'email' => $user->email, 56 - 'password' => 'wrong-password', 57 - ]); 58 - 59 - $this->assertGuest(); 60 - }); 61 - 62 - test('users can logout', function () { 63 - $user = User::factory()->create(); 64 - 65 - $response = $this->actingAs($user)->post(route('logout')); 66 - 67 - $this->assertGuest(); 68 - $response->assertRedirect(route('home')); 69 - }); 70 - 71 - test('users are rate limited', function () { 72 - $user = User::factory()->create(); 73 - 74 - RateLimiter::increment(md5('login'.implode('|', [$user->email, '127.0.0.1'])), amount: 5); 75 - 76 - $response = $this->post(route('login.store'), [ 77 - 'email' => $user->email, 78 - 'password' => 'wrong-password', 79 - ]); 80 - 81 - $response->assertTooManyRequests(); 82 - });
-100
tests/Feature/Auth/EmailVerificationTest.php
··· 1 - <?php 2 - 3 - use App\Models\User; 4 - use Illuminate\Auth\Events\Verified; 5 - use Illuminate\Support\Facades\Event; 6 - use Illuminate\Support\Facades\URL; 7 - use Laravel\Fortify\Features; 8 - 9 - beforeEach(function () { 10 - $this->skipUnlessFortifyHas(Features::emailVerification()); 11 - }); 12 - 13 - test('email verification screen can be rendered', function () { 14 - $user = User::factory()->unverified()->create(); 15 - 16 - $response = $this->actingAs($user)->get(route('verification.notice')); 17 - 18 - $response->assertOk(); 19 - }); 20 - 21 - test('email can be verified', function () { 22 - $user = User::factory()->unverified()->create(); 23 - 24 - Event::fake(); 25 - 26 - $verificationUrl = URL::temporarySignedRoute( 27 - 'verification.verify', 28 - now()->addMinutes(60), 29 - ['id' => $user->id, 'hash' => sha1($user->email)], 30 - ); 31 - 32 - $response = $this->actingAs($user)->get($verificationUrl); 33 - 34 - Event::assertDispatched(Verified::class); 35 - expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 36 - $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 37 - }); 38 - 39 - test('email is not verified with invalid hash', function () { 40 - $user = User::factory()->unverified()->create(); 41 - 42 - Event::fake(); 43 - 44 - $verificationUrl = URL::temporarySignedRoute( 45 - 'verification.verify', 46 - now()->addMinutes(60), 47 - ['id' => $user->id, 'hash' => sha1('wrong-email')], 48 - ); 49 - 50 - $this->actingAs($user)->get($verificationUrl); 51 - 52 - Event::assertNotDispatched(Verified::class); 53 - expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 54 - }); 55 - 56 - test('email is not verified with invalid user id', function () { 57 - $user = User::factory()->unverified()->create(); 58 - 59 - Event::fake(); 60 - 61 - $verificationUrl = URL::temporarySignedRoute( 62 - 'verification.verify', 63 - now()->addMinutes(60), 64 - ['id' => 123, 'hash' => sha1($user->email)], 65 - ); 66 - 67 - $this->actingAs($user)->get($verificationUrl); 68 - 69 - Event::assertNotDispatched(Verified::class); 70 - expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 71 - }); 72 - 73 - test('verified user is redirected to dashboard from verification prompt', function () { 74 - $user = User::factory()->create(); 75 - 76 - Event::fake(); 77 - 78 - $response = $this->actingAs($user)->get(route('verification.notice')); 79 - 80 - Event::assertNotDispatched(Verified::class); 81 - $response->assertRedirect(route('dashboard', absolute: false)); 82 - }); 83 - 84 - test('already verified user visiting verification link is redirected without firing event again', function () { 85 - $user = User::factory()->create(); 86 - 87 - Event::fake(); 88 - 89 - $verificationUrl = URL::temporarySignedRoute( 90 - 'verification.verify', 91 - now()->addMinutes(60), 92 - ['id' => $user->id, 'hash' => sha1($user->email)], 93 - ); 94 - 95 - $this->actingAs($user)->get($verificationUrl) 96 - ->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 97 - 98 - Event::assertNotDispatched(Verified::class); 99 - expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 100 - });
-22
tests/Feature/Auth/PasswordConfirmationTest.php
··· 1 - <?php 2 - 3 - use App\Models\User; 4 - use Inertia\Testing\AssertableInertia as Assert; 5 - 6 - test('confirm password screen can be rendered', function () { 7 - $user = User::factory()->create(); 8 - 9 - $response = $this->actingAs($user)->get(route('password.confirm')); 10 - 11 - $response->assertOk(); 12 - 13 - $response->assertInertia(fn (Assert $page) => $page 14 - ->component('auth/confirm-password'), 15 - ); 16 - }); 17 - 18 - test('password confirmation requires authentication', function () { 19 - $response = $this->get(route('password.confirm')); 20 - 21 - $response->assertRedirect(route('login')); 22 - });
-78
tests/Feature/Auth/PasswordResetTest.php
··· 1 - <?php 2 - 3 - use App\Models\User; 4 - use Illuminate\Auth\Notifications\ResetPassword; 5 - use Illuminate\Support\Facades\Notification; 6 - use Laravel\Fortify\Features; 7 - 8 - beforeEach(function () { 9 - $this->skipUnlessFortifyHas(Features::resetPasswords()); 10 - }); 11 - 12 - test('reset password link screen can be rendered', function () { 13 - $response = $this->get(route('password.request')); 14 - 15 - $response->assertOk(); 16 - }); 17 - 18 - test('reset password link can be requested', function () { 19 - Notification::fake(); 20 - 21 - $user = User::factory()->create(); 22 - 23 - $this->post(route('password.email'), ['email' => $user->email]); 24 - 25 - Notification::assertSentTo($user, ResetPassword::class); 26 - }); 27 - 28 - test('reset password screen can be rendered', function () { 29 - Notification::fake(); 30 - 31 - $user = User::factory()->create(); 32 - 33 - $this->post(route('password.email'), ['email' => $user->email]); 34 - 35 - Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 36 - $response = $this->get(route('password.reset', $notification->token)); 37 - 38 - $response->assertOk(); 39 - 40 - return true; 41 - }); 42 - }); 43 - 44 - test('password can be reset with valid token', function () { 45 - Notification::fake(); 46 - 47 - $user = User::factory()->create(); 48 - 49 - $this->post(route('password.email'), ['email' => $user->email]); 50 - 51 - Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 52 - $response = $this->post(route('password.update'), [ 53 - 'token' => $notification->token, 54 - 'email' => $user->email, 55 - 'password' => 'password', 56 - 'password_confirmation' => 'password', 57 - ]); 58 - 59 - $response 60 - ->assertSessionHasNoErrors() 61 - ->assertRedirect(route('login')); 62 - 63 - return true; 64 - }); 65 - }); 66 - 67 - test('password cannot be reset with invalid token', function () { 68 - $user = User::factory()->create(); 69 - 70 - $response = $this->post(route('password.update'), [ 71 - 'token' => 'invalid-token', 72 - 'email' => $user->email, 73 - 'password' => 'newpassword123', 74 - 'password_confirmation' => 'newpassword123', 75 - ]); 76 - 77 - $response->assertSessionHasErrors('email'); 78 - });
-25
tests/Feature/Auth/RegistrationTest.php
··· 1 - <?php 2 - 3 - use Laravel\Fortify\Features; 4 - 5 - beforeEach(function () { 6 - $this->skipUnlessFortifyHas(Features::registration()); 7 - }); 8 - 9 - test('registration screen can be rendered', function () { 10 - $response = $this->get(route('register')); 11 - 12 - $response->assertOk(); 13 - }); 14 - 15 - test('new users can register', function () { 16 - $response = $this->post(route('register.store'), [ 17 - 'name' => 'Test User', 18 - 'email' => 'test@example.com', 19 - 'password' => 'password', 20 - 'password_confirmation' => 'password', 21 - ]); 22 - 23 - $this->assertAuthenticated(); 24 - $response->assertRedirect(route('dashboard', absolute: false)); 25 - });
-41
tests/Feature/Auth/TwoFactorChallengeTest.php
··· 1 - <?php 2 - 3 - use App\Models\User; 4 - use Inertia\Testing\AssertableInertia as Assert; 5 - use Laravel\Fortify\Features; 6 - 7 - beforeEach(function () { 8 - $this->skipUnlessFortifyHas(Features::twoFactorAuthentication()); 9 - }); 10 - 11 - test('two factor challenge redirects to login when not authenticated', function () { 12 - $response = $this->get(route('two-factor.login')); 13 - 14 - $response->assertRedirect(route('login')); 15 - }); 16 - 17 - test('two factor challenge can be rendered', function () { 18 - Features::twoFactorAuthentication([ 19 - 'confirm' => true, 20 - 'confirmPassword' => true, 21 - ]); 22 - 23 - $user = User::factory()->create(); 24 - 25 - $user->forceFill([ 26 - 'two_factor_secret' => encrypt('test-secret'), 27 - 'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])), 28 - 'two_factor_confirmed_at' => now(), 29 - ])->save(); 30 - 31 - $this->post(route('login'), [ 32 - 'email' => $user->email, 33 - 'password' => 'password', 34 - ]); 35 - 36 - $this->get(route('two-factor.login')) 37 - ->assertOk() 38 - ->assertInertia(fn (Assert $page) => $page 39 - ->component('auth/two-factor-challenge'), 40 - ); 41 - });
-34
tests/Feature/Auth/VerificationNotificationTest.php
··· 1 - <?php 2 - 3 - use App\Models\User; 4 - use Illuminate\Auth\Notifications\VerifyEmail; 5 - use Illuminate\Support\Facades\Notification; 6 - use Laravel\Fortify\Features; 7 - 8 - beforeEach(function () { 9 - $this->skipUnlessFortifyHas(Features::emailVerification()); 10 - }); 11 - 12 - test('sends verification notification', function () { 13 - Notification::fake(); 14 - 15 - $user = User::factory()->unverified()->create(); 16 - 17 - $this->actingAs($user) 18 - ->post(route('verification.send')) 19 - ->assertRedirect(route('home')); 20 - 21 - Notification::assertSentTo($user, VerifyEmail::class); 22 - }); 23 - 24 - test('does not send verification notification if email is verified', function () { 25 - Notification::fake(); 26 - 27 - $user = User::factory()->create(); 28 - 29 - $this->actingAs($user) 30 - ->post(route('verification.send')) 31 - ->assertRedirect(route('dashboard', absolute: false)); 32 - 33 - Notification::assertNothingSent(); 34 - });
-114
tests/Feature/Settings/SecurityTest.php
··· 1 - <?php 2 - 3 - use App\Models\User; 4 - use Illuminate\Support\Facades\Hash; 5 - use Inertia\Testing\AssertableInertia as Assert; 6 - use Laravel\Fortify\Features; 7 - 8 - test('security page is displayed', function () { 9 - $this->skipUnlessFortifyHas(Features::twoFactorAuthentication()); 10 - 11 - Features::twoFactorAuthentication([ 12 - 'confirm' => true, 13 - 'confirmPassword' => true, 14 - ]); 15 - 16 - $user = User::factory()->create(); 17 - 18 - $this->actingAs($user) 19 - ->withSession(['auth.password_confirmed_at' => time()]) 20 - ->get(route('security.edit')) 21 - ->assertInertia(fn (Assert $page) => $page 22 - ->component('settings/security') 23 - ->where('canManageTwoFactor', true) 24 - ->where('twoFactorEnabled', false), 25 - ); 26 - }); 27 - 28 - test('security page requires password confirmation when enabled', function () { 29 - $this->skipUnlessFortifyHas(Features::twoFactorAuthentication()); 30 - 31 - $user = User::factory()->create(); 32 - 33 - Features::twoFactorAuthentication([ 34 - 'confirm' => true, 35 - 'confirmPassword' => true, 36 - ]); 37 - 38 - $response = $this->actingAs($user) 39 - ->get(route('security.edit')); 40 - 41 - $response->assertRedirect(route('password.confirm')); 42 - }); 43 - 44 - test('security page does not require password confirmation when disabled', function () { 45 - $this->skipUnlessFortifyHas(Features::twoFactorAuthentication()); 46 - 47 - $user = User::factory()->create(); 48 - 49 - Features::twoFactorAuthentication([ 50 - 'confirm' => true, 51 - 'confirmPassword' => false, 52 - ]); 53 - 54 - $this->actingAs($user) 55 - ->get(route('security.edit')) 56 - ->assertOk() 57 - ->assertInertia(fn (Assert $page) => $page 58 - ->component('settings/security'), 59 - ); 60 - }); 61 - 62 - test('security page renders without two factor when feature is disabled', function () { 63 - $this->skipUnlessFortifyHas(Features::twoFactorAuthentication()); 64 - 65 - config(['fortify.features' => []]); 66 - 67 - $user = User::factory()->create(); 68 - 69 - $this->actingAs($user) 70 - ->get(route('security.edit')) 71 - ->assertOk() 72 - ->assertInertia(fn (Assert $page) => $page 73 - ->component('settings/security') 74 - ->where('canManageTwoFactor', false) 75 - ->missing('twoFactorEnabled') 76 - ->missing('requiresConfirmation'), 77 - ); 78 - }); 79 - 80 - test('password can be updated', function () { 81 - $user = User::factory()->create(); 82 - 83 - $response = $this 84 - ->actingAs($user) 85 - ->from(route('security.edit')) 86 - ->put(route('user-password.update'), [ 87 - 'current_password' => 'password', 88 - 'password' => 'new-password', 89 - 'password_confirmation' => 'new-password', 90 - ]); 91 - 92 - $response 93 - ->assertSessionHasNoErrors() 94 - ->assertRedirect(route('security.edit')); 95 - 96 - expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue(); 97 - }); 98 - 99 - test('correct password must be provided to update password', function () { 100 - $user = User::factory()->create(); 101 - 102 - $response = $this 103 - ->actingAs($user) 104 - ->from(route('security.edit')) 105 - ->put(route('user-password.update'), [ 106 - 'current_password' => 'wrong-password', 107 - 'password' => 'new-password', 108 - 'password_confirmation' => 'new-password', 109 - ]); 110 - 111 - $response 112 - ->assertSessionHasErrors('current_password') 113 - ->assertRedirect(route('security.edit')); 114 - });
+1 -7
tests/TestCase.php
··· 3 3 namespace Tests; 4 4 5 5 use Illuminate\Foundation\Testing\TestCase as BaseTestCase; 6 - use Laravel\Fortify\Features; 7 6 8 7 abstract class TestCase extends BaseTestCase 9 8 { 10 - protected function skipUnlessFortifyHas(string $feature, ?string $message = null): void 11 - { 12 - if (! Features::enabled($feature)) { 13 - $this->markTestSkipped($message ?? "Fortify feature [{$feature}] is not enabled."); 14 - } 15 - } 9 + // 16 10 }