From ddcf0c80922fa7de5beabc25be68fbb4dd8f3f49 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 26 Jun 2024 14:58:25 +0800 Subject: [PATCH] Code Improvements (#234) * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Apply fixes from StyleCI * wip Signed-off-by: Mior Muhammad Zaki * Update NovaServiceProvider.php * Update NovaServiceProvider.php * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: StyleCI Bot --- app/Providers/NovaServiceProvider.php | 7 +- ...vaWithoutAuthenticationServiceProvider.php | 26 +++++++ config/nova.php | 1 + ..._add_two_factor_columns_to_users_table.php | 46 +++++++++++ lang/vendor/nova/en.json | 7 ++ tests/Browser/AuthenticatesUserTest.php | 25 +----- tests/Browser/CustomAuthenticatesUserTest.php | 78 ++++--------------- .../Browser/CustomForgotUserPasswordTest.php | 52 +++++++++++++ tests/Browser/ForgotUserPasswordTest.php | 34 ++++++++ tests/Browser/ResetPasswordTest.php | 3 +- tests/DuskTestCase.php | 18 ++++- 11 files changed, 209 insertions(+), 88 deletions(-) create mode 100644 app/Providers/NovaWithoutAuthenticationServiceProvider.php create mode 100644 database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php create mode 100644 tests/Browser/CustomForgotUserPasswordTest.php create mode 100644 tests/Browser/ForgotUserPasswordTest.php diff --git a/app/Providers/NovaServiceProvider.php b/app/Providers/NovaServiceProvider.php index ae860dee..328915a1 100644 --- a/app/Providers/NovaServiceProvider.php +++ b/app/Providers/NovaServiceProvider.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules\Password; +use Laravel\Fortify\Features; use Laravel\Nova\Events\ServingNova; use Laravel\Nova\Events\StartedImpersonating; use Laravel\Nova\Events\StoppedImpersonating; @@ -236,9 +237,13 @@ public function registerFieldMacros() protected function routes() { Nova::routes() + ->withFortifyFeatures([ + Features::emailVerification(), + Features::twoFactorAuthentication(['confirm' => true, 'confirmPassword' => true]), + ]) ->withAuthenticationRoutes() ->withPasswordResetRoutes() - ->register(); + ->register(fortify: false); } /** diff --git a/app/Providers/NovaWithoutAuthenticationServiceProvider.php b/app/Providers/NovaWithoutAuthenticationServiceProvider.php new file mode 100644 index 00000000..2e592cbd --- /dev/null +++ b/app/Providers/NovaWithoutAuthenticationServiceProvider.php @@ -0,0 +1,26 @@ + '/login', + 'nova.routes.logout' => '/logout', + ]); + + Nova::routes() + ->withoutAuthenticationRoutes() + ->withPasswordResetRoutes() + ->register(fortify: false); + } +} diff --git a/config/nova.php b/config/nova.php index 8c9679b7..66f3255d 100644 --- a/config/nova.php +++ b/config/nova.php @@ -109,6 +109,7 @@ 'nova', Authenticate::class, Authorize::class, + // 'nova.verified', ], /* diff --git a/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php b/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php new file mode 100644 index 00000000..b490e24f --- /dev/null +++ b/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php @@ -0,0 +1,46 @@ +text('two_factor_secret') + ->after('password') + ->nullable(); + + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + + if (Fortify::confirmsTwoFactorAuthentication()) { + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(array_merge([ + 'two_factor_secret', + 'two_factor_recovery_codes', + ], Fortify::confirmsTwoFactorAuthentication() ? [ + 'two_factor_confirmed_at', + ] : [])); + }); + } +}; diff --git a/lang/vendor/nova/en.json b/lang/vendor/nova/en.json index e50298d6..55f4a7cf 100644 --- a/lang/vendor/nova/en.json +++ b/lang/vendor/nova/en.json @@ -5,17 +5,23 @@ "Reset Password": "Reset Password", "Sorry! You are not authorized to perform this action.": "Sorry! You are not authorized to perform this action.", "You are receiving this email because we received a password reset request for your account.": "You are receiving this email because we received a password reset request for your account.", + "Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.": "Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.", + "A new verification link has been sent to the email address you provided in your profile settings.": "A new verification link has been sent to the email address you provided in your profile settings.", + "This is a secure area of the application. Please confirm your password before continuing.": "This is a secure area of the application. Please confirm your password before continuing.", "Error": "Error", "Confirm Password": "Confirm Password", "We have emailed your password reset link!": "We have emailed your password reset link!", "Dashboard": "Dashboard", "Email Address": "Email Address", + "Username": "Username", "Forgot Password": "Forgot Password", "Forgot your password?": "Forgot your password?", "Log In": "Log In", "Logout": "Logout", "Password": "Password", "Remember me": "Remember me", + "Email Verification": "Email Verification", + "Secure Area": "Secure Area", "Resources": "Resources", "Send Password Reset Link": "Send Password Reset Link", "Welcome Back!": "Welcome Back!", @@ -32,6 +38,7 @@ "Restore": "Restore", "Force Delete Resource": "Force Delete Resource", "Force Delete": "Force Delete", + "Confirm": "Confirm", "Are you sure you want to delete this resource?": "Are you sure you want to delete this resource?", "Are you sure you want to delete the selected resources?": "Are you sure you want to delete the selected resources?", "Are you sure you want to detach this resource?": "Are you sure you want to detach this resource?", diff --git a/tests/Browser/AuthenticatesUserTest.php b/tests/Browser/AuthenticatesUserTest.php index 4e30f05a..589f1530 100644 --- a/tests/Browser/AuthenticatesUserTest.php +++ b/tests/Browser/AuthenticatesUserTest.php @@ -2,7 +2,6 @@ namespace Laravel\Nova\Tests\Browser; -use Database\Factories\UserFactory; use Laravel\Dusk\Browser; use Laravel\Nova\Nova; use Laravel\Nova\Testing\Browser\Components\SidebarComponent; @@ -10,7 +9,9 @@ use Laravel\Nova\Testing\Browser\Pages\Login; use Laravel\Nova\Tests\DuskTestCase; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +#[Group('auth')] class AuthenticatesUserTest extends DuskTestCase { #[DataProvider('intendedUrlDataProvider')] @@ -24,7 +25,8 @@ public function test_it_redirect_to_intended_url_after_login($targetUrl, $expect ->type('email', 'nova@laravel.com') ->type('password', 'password') ->clickAndWaitForReload('button[type="submit"]') - ->assertPathIs(Nova::url($expectedUrl)); + ->assertPathIs(Nova::url($expectedUrl)) + ->assertAuthenticated(); $browser->blank(); }); @@ -112,25 +114,6 @@ public function test_redirect_outside_of_nova_after_login() }); } - public function test_it_redirect_to_login_after_password_reset() - { - $this->browse(function (Browser $browser) { - $user = UserFactory::new()->create(); - - $browser->logout() - ->assertGuest() - ->visit(Nova::url('password/reset')) - ->waitForText('Forgot your password?') - ->type('input[id="email"]', $user->email) - ->click('button[type="submit"]') - ->waitForText(__('passwords.sent')) - ->pause(5000) - ->waitForLocation(Nova::url('login')); - - $browser->blank(); - }); - } - public static function intendedUrlDataProvider() { yield ['/resources/users/3', '/resources/users/3']; diff --git a/tests/Browser/CustomAuthenticatesUserTest.php b/tests/Browser/CustomAuthenticatesUserTest.php index 5a17e1c9..3f41e82c 100644 --- a/tests/Browser/CustomAuthenticatesUserTest.php +++ b/tests/Browser/CustomAuthenticatesUserTest.php @@ -2,7 +2,8 @@ namespace Laravel\Nova\Tests\Browser; -use Database\Factories\UserFactory; +use App\Providers\NovaServiceProvider; +use App\Providers\NovaWithoutAuthenticationServiceProvider; use Laravel\Dusk\Browser; use Laravel\Nova\Nova; use Laravel\Nova\Testing\Browser\Components\SidebarComponent; @@ -11,19 +12,27 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +#[Group('auth')] #[Group('internal-server')] class CustomAuthenticatesUserTest extends DuskTestCase { + /** + * Get package providers. + * + * @param \Illuminate\Foundation\Application $app + * @return array + */ + protected function getPackageProviders($app) + { + return collect(parent::getPackageProviders($app)) + ->replace([ + NovaServiceProvider::class => NovaWithoutAuthenticationServiceProvider::class, + ])->all(); + } + #[DataProvider('intendedUrlDataProvider')] public function test_it_redirect_to_intended_url_after_login($targetUrl, $expectedUrl) { - $this->beforeServingApplication(function ($app, $config) { - $config->set('nova.routes.login', '/login'); - $config->set('nova.routes.logout', '/logout'); - - Nova::$withAuthentication = false; - }); - $this->browse(function (Browser $browser) use ($targetUrl, $expectedUrl) { $browser->logout() ->assertGuest() @@ -40,13 +49,6 @@ public function test_it_redirect_to_intended_url_after_login($targetUrl, $expect public function test_it_redirect_to_login_after_logout() { - $this->beforeServingApplication(function ($app, $config) { - $config->set('nova.routes.login', '/login'); - $config->set('nova.routes.logout', '/logout'); - - Nova::$withAuthentication = false; - }); - $this->browse(function (Browser $browser) { $browser->loginAs(1) ->visit(new Dashboard()) @@ -64,13 +66,6 @@ public function test_it_redirect_to_login_after_logout() public function test_it_clear_user_association_after_logout() { - $this->beforeServingApplication(function ($app, $config) { - $config->set('nova.routes.login', '/login'); - $config->set('nova.routes.logout', '/logout'); - - Nova::$withAuthentication = false; - }); - $this->browse(function (Browser $browser) { $browser->loginAs(1) ->visit(new Dashboard()) @@ -85,13 +80,6 @@ public function test_it_clear_user_association_after_logout() public function test_it_clear_user_association_after_session_timeout() { - $this->beforeServingApplication(function ($app, $config) { - $config->set('nova.routes.login', '/login'); - $config->set('nova.routes.logout', '/logout'); - - Nova::$withAuthentication = false; - }); - $this->browse(function (Browser $browser) { $browser->loginAs(1)->visit(new Dashboard()); @@ -108,13 +96,6 @@ public function test_it_clear_user_association_after_session_timeout() public function test_it_can_relogin_after_session_timeout() { - $this->beforeServingApplication(function ($app, $config) { - $config->set('nova.routes.login', '/login'); - $config->set('nova.routes.logout', '/logout'); - - Nova::$withAuthentication = false; - }); - $this->browse(function (Browser $browser) { $browser->loginAs(1)->visit(new Dashboard()); @@ -131,31 +112,6 @@ public function test_it_can_relogin_after_session_timeout() }); } - public function test_it_redirect_to_login_after_password_reset() - { - $this->beforeServingApplication(function ($app, $config) { - $config->set('mail.default', 'log'); - $config->set('nova.routes.login', '/login'); - $config->set('nova.routes.logout', '/logout'); - - Nova::$withAuthentication = false; - }); - - $this->browse(function (Browser $browser) { - $user = UserFactory::new()->create(); - - $browser->logout() - ->assertGuest() - ->visit(Nova::url('password/reset')) - ->waitForText('Forgot your password?') - ->type('input[id="email"]', $user->email) - ->clickAndWaitForReload('button[type="submit"]', 40) - ->assertPathIs('/login'); - - $browser->blank(); - }); - } - public static function intendedUrlDataProvider() { yield ['/resources/users/3', '/resources/users/3']; diff --git a/tests/Browser/CustomForgotUserPasswordTest.php b/tests/Browser/CustomForgotUserPasswordTest.php new file mode 100644 index 00000000..e07c2648 --- /dev/null +++ b/tests/Browser/CustomForgotUserPasswordTest.php @@ -0,0 +1,52 @@ + + */ + protected function getPackageProviders($app) + { + return collect(parent::getPackageProviders($app)) + ->replace([ + NovaServiceProvider::class => NovaWithoutAuthenticationServiceProvider::class, + ])->all(); + } + + public function test_it_redirect_to_login_after_password_reset() + { + $this->beforeServingApplication(function ($app, $config) { + $config->set('mail.default', 'log'); + }); + + $this->browse(function (Browser $browser) { + $user = UserFactory::new()->create(); + + $browser->logout() + ->assertGuest() + ->visit(Nova::url('password/reset')) + ->waitForText('Forgot your password?') + ->type('input[id="email"]', $user->email) + ->clickAndWaitForReload('button[type="submit"]', 40) + ->assertPathIs('/login'); + + $browser->blank(); + }); + } +} diff --git a/tests/Browser/ForgotUserPasswordTest.php b/tests/Browser/ForgotUserPasswordTest.php new file mode 100644 index 00000000..ce2778ae --- /dev/null +++ b/tests/Browser/ForgotUserPasswordTest.php @@ -0,0 +1,34 @@ +browse(function (Browser $browser) { + $user = UserFactory::new()->create(); + + $browser->logout() + ->assertGuest() + ->visit(Nova::url('password/reset')) + ->waitForText('Forgot your password?') + ->type('input[id="email"]', $user->email) + ->click('button[type="submit"]') + ->waitForText(__('passwords.sent')) + ->pause(5000) + ->waitForLocation(Nova::url('login')); + + $browser->blank(); + }); + } +} diff --git a/tests/Browser/ResetPasswordTest.php b/tests/Browser/ResetPasswordTest.php index 47bde8e7..aace328e 100644 --- a/tests/Browser/ResetPasswordTest.php +++ b/tests/Browser/ResetPasswordTest.php @@ -21,7 +21,8 @@ public function test_it_can_reset_user_password_and_navigate_to_nova() ->type('password', 'password!!') ->type('password_confirmation', 'password!!') ->clickAndWaitForReload('button[type="submit"]') - ->assertPathIs(Nova::url('/dashboards/main')); + ->assertPathIs(Nova::url('/login')) + ->assertGuest(); $browser->blank(); diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php index 3430f62c..8dec02f3 100644 --- a/tests/DuskTestCase.php +++ b/tests/DuskTestCase.php @@ -3,7 +3,14 @@ namespace Laravel\Nova\Tests; use Illuminate\Support\Arr; +use Inertia\ServiceProvider as InertiaServiceProvider; use Laravel\Dusk\Browser; +use Laravel\Fortify\FortifyServiceProvider; +use Laravel\Nova\NovaCoreServiceProvider; +use Laravel\Nova\NovaServiceProvider; +use Laravel\Scout\ScoutServiceProvider; + +use function Orchestra\Testbench\package_path; abstract class DuskTestCase extends \Orchestra\Testbench\Dusk\TestCase { @@ -19,9 +26,10 @@ abstract class DuskTestCase extends \Orchestra\Testbench\Dusk\TestCase protected $loadEnvironmentVariables = true; /** {@inheritDoc} */ + #[\Override] public static function applicationBasePath() { - return realpath(__DIR__.'/../'); + return package_path(['vendor', 'laravel', 'nova-dusk-suite']); } /** @@ -51,9 +59,11 @@ protected function setUpDuskServer(): void protected function getPackageProviders($app) { return [ - 'Inertia\ServiceProvider', - 'Laravel\Nova\NovaCoreServiceProvider', - 'Carbon\Laravel\ServiceProvider', + FortifyServiceProvider::class, + InertiaServiceProvider::class, + NovaCoreServiceProvider::class, + NovaServiceProvider::class, + ScoutServiceProvider::class, ]; }