From a346566fd0a6398bb7e9b146617f88d0d218a35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 22 Jun 2026 13:01:27 +0200 Subject: [PATCH] feat(demo): gate demo account access behind a config flag (#580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds a `DEMO_ENABLED` env var (`config('app.demo.enabled')`, default `true`) to fully toggle the demo account. Setting `DEMO_ENABLED=false` in production blocks it without code changes. When disabled: - **Login is blocked** — `Fortify::authenticateUsing` rejects the demo account with a generic credentials error (doesn't reveal the demo is off). Regular users and 2FA are unaffected. - **Landing link hidden** — the "Check Demo" button on the landing page is removed (shared `demoEnabled` prop). - **No credential prefill** — `demoCredentials` is only shared when enabled, so `/login?demo=1` no longer autofills. ## Why The demo account is publicly shared and gets abused (e.g. duplicate votes on integration requests). This gives us a kill switch. ## Tests Added to `DemoAccountRestrictionsTest`: - demo account cannot log in when disabled - demo account can log in when enabled - regular user can still log in when demo is disabled Existing auth + 2FA tests still pass. --- app/Http/Middleware/HandleInertiaRequests.php | 3 +- app/Providers/FortifyServiceProvider.php | 15 ++++++ config/app.php | 1 + resources/js/pages/welcome.tsx | 22 ++++---- resources/js/types/index.d.ts | 1 + tests/Feature/DemoAccountRestrictionsTest.php | 51 +++++++++++++++++++ 6 files changed, 82 insertions(+), 11 deletions(-) diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 47a66ee0..2d969681 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -86,7 +86,8 @@ class HandleInertiaRequests extends Middleware 'status' => 'past_due', 'action_url' => route('settings.billing.portal'), ] : null, - 'demoCredentials' => ($isDemoQuery || $isDemoAccount) ? [ + 'demoEnabled' => (bool) config('app.demo.enabled'), + 'demoCredentials' => config('app.demo.enabled') && ($isDemoQuery || $isDemoAccount) ? [ 'email' => config('app.demo.email'), 'password' => config('app.demo.password'), ] : null, diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index f7133c51..a025d37b 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -14,6 +14,7 @@ use Illuminate\Auth\Events\Registered; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; @@ -52,6 +53,20 @@ class FortifyServiceProvider extends ServiceProvider { Fortify::resetUserPasswordsUsing(ResetUserPassword::class); Fortify::createUsersUsing(CreateNewUser::class); + + Fortify::authenticateUsing(function (Request $request): ?User { + $user = User::query()->where('email', $request->email)->first(); + + if (! $user || ! Hash::check($request->password, $user->password)) { + return null; + } + + if ($user->isDemoAccount() && ! config('app.demo.enabled')) { + return null; + } + + return $user; + }); } /** diff --git a/config/app.php b/config/app.php index e57dc652..35fa0297 100644 --- a/config/app.php +++ b/config/app.php @@ -124,6 +124,7 @@ return [ ], 'demo' => [ + 'enabled' => env('DEMO_ENABLED', true), 'email' => env('DEMO_EMAIL', 'demo@whisper.money'), 'password' => env('DEMO_PASSWORD', 'demo'), ], diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 46b0acee..37dd40f1 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -1938,7 +1938,7 @@ export default function Welcome({ hideAuthButtons?: boolean; popularBanks: PopularBank[]; }) { - const { appUrl, auth, subscriptionsEnabled, pricing, locale } = + const { appUrl, auth, subscriptionsEnabled, demoEnabled, pricing, locale } = usePage().props; const planEntries = Object.entries(pricing.plans); const { isMobile } = usePwaInstall(); @@ -2269,15 +2269,17 @@ export default function Welcome({ {__('Get Started')} - - - + {demoEnabled && ( + + + + )} )}

diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 87c4fd41..c6a7e3e9 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -77,6 +77,7 @@ export interface SharedData { includeLoansInNetWorthChart: boolean; includeRealEstateInNetWorthChart: boolean; subscriptionsEnabled: boolean; + demoEnabled: boolean; aiCategorizationUpsellRate: number; subscriptionPaymentIssue: SubscriptionPaymentIssueNotification | null; pricing: PricingConfig; diff --git a/tests/Feature/DemoAccountRestrictionsTest.php b/tests/Feature/DemoAccountRestrictionsTest.php index d5510e8f..1f600568 100644 --- a/tests/Feature/DemoAccountRestrictionsTest.php +++ b/tests/Feature/DemoAccountRestrictionsTest.php @@ -37,6 +37,57 @@ test('demo account has restricted actions', function () { expect($demoUser->isDemoAccount())->toBeTrue(); }); +test('demo account cannot log in when demo is disabled', function () { + config(['app.demo.email' => 'demo@whisper.money']); + config(['app.demo.enabled' => false]); + + User::factory()->withoutTwoFactor()->create([ + 'email' => 'demo@whisper.money', + 'password' => 'demo', + ]); + + $this->post(route('login.store'), [ + 'email' => 'demo@whisper.money', + 'password' => 'demo', + ])->assertSessionHasErrors('email'); + + $this->assertGuest(); +}); + +test('demo account can log in when demo is enabled', function () { + config(['app.demo.email' => 'demo@whisper.money']); + config(['app.demo.enabled' => true]); + + User::factory()->withoutTwoFactor()->create([ + 'email' => 'demo@whisper.money', + 'password' => 'demo', + ]); + + $this->post(route('login.store'), [ + 'email' => 'demo@whisper.money', + 'password' => 'demo', + ]); + + $this->assertAuthenticated(); +}); + +test('regular user can log in when demo is disabled', function () { + config(['app.demo.email' => 'demo@whisper.money']); + config(['app.demo.enabled' => false]); + + User::factory()->withoutTwoFactor()->create([ + 'email' => 'real@whisper.money', + 'password' => 'password123', + ]); + + $this->post(route('login.store'), [ + 'email' => 'real@whisper.money', + 'password' => 'password123', + ]); + + $this->assertAuthenticated(); +}); + test('regular user is not restricted', function () { $user = User::factory()->create([ 'password' => 'password123',