feat(demo): gate demo account access behind a config flag (#580)
## 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.
This commit is contained in:
parent
5db6cdc6d2
commit
a346566fd0
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ return [
|
|||
],
|
||||
|
||||
'demo' => [
|
||||
'enabled' => env('DEMO_ENABLED', true),
|
||||
'email' => env('DEMO_EMAIL', 'demo@whisper.money'),
|
||||
'password' => env('DEMO_PASSWORD', 'demo'),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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<SharedData>().props;
|
||||
const planEntries = Object.entries(pricing.plans);
|
||||
const { isMobile } = usePwaInstall();
|
||||
|
|
@ -2269,15 +2269,17 @@ export default function Welcome({
|
|||
{__('Get Started')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/login?demo=1">
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
size={'lg'}
|
||||
className="h-14"
|
||||
>
|
||||
{__('Check Demo')}
|
||||
</Button>
|
||||
</Link>
|
||||
{demoEnabled && (
|
||||
<Link href="/login?demo=1">
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
size={'lg'}
|
||||
className="h-14"
|
||||
>
|
||||
{__('Check Demo')}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export interface SharedData {
|
|||
includeLoansInNetWorthChart: boolean;
|
||||
includeRealEstateInNetWorthChart: boolean;
|
||||
subscriptionsEnabled: boolean;
|
||||
demoEnabled: boolean;
|
||||
aiCategorizationUpsellRate: number;
|
||||
subscriptionPaymentIssue: SubscriptionPaymentIssueNotification | null;
|
||||
pricing: PricingConfig;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue