From 3fe1a64f5e8147e260f1a4101d1dd4913d163b58 Mon Sep 17 00:00:00 2001 From: Shivanath Devinarayanan Date: Fri, 23 Oct 2026 01:58:37 +0400 Subject: [PATCH] feat(onboard): add ONBOARD_ALLOW_REGISTRATION to close self-service sign-up POST /int/v1/onboard/create-account creates a new organization on every call. should-onboard only hides the console's setup wizard, so a self-hosted install stays open to sign-ups from anyone who can reach it. Add fleetbase.onboard.allow_registration (ONBOARD_ALLOW_REGISTRATION, default true so nothing changes unless an operator opts in). When it is false and an organization already exists, createAccount answers 403 with code registration_disabled, the same shape OAuth uses for OAUTH_ALLOW_REGISTRATION. The first organization can still be created. Co-Authored-By: Claude Opus 5.5 (1M context) --- config/fleetbase.php | 17 +++++++++++ .../Internal/v1/OnboardController.php | 5 ++++ tests/Unit/Http/OnboardControllerTest.php | 30 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/config/fleetbase.php b/config/fleetbase.php index d93a9a97..4f8a5523 100644 --- a/config/fleetbase.php +++ b/config/fleetbase.php @@ -51,6 +51,23 @@ */ 'sms_auth_bypass_code' => env('SMS_AUTH_BYPASS_CODE'), + /* + |-------------------------------------------------------------------------- + | Self-service Sign-up + |-------------------------------------------------------------------------- + | + | When false, the onboarding endpoint only creates the first organization. + | Once an organization exists, new sign-ups are refused, so a self-hosted + | install can stay invite-only. OAUTH_ALLOW_REGISTRATION covers sign-ups + | that start from an OAuth provider. + | + | Environment variable: ONBOARD_ALLOW_REGISTRATION + | + */ + 'onboard' => [ + 'allow_registration' => env('ONBOARD_ALLOW_REGISTRATION', true), + ], + 'user_cache' => [ 'enabled' => env('USER_CACHE_ENABLED', true), 'server_ttl' => (int) env('USER_CACHE_SERVER_TTL', 900), // 15 minutes diff --git a/src/Http/Controllers/Internal/v1/OnboardController.php b/src/Http/Controllers/Internal/v1/OnboardController.php index baa6b4b7..1398a8d5 100644 --- a/src/Http/Controllers/Internal/v1/OnboardController.php +++ b/src/Http/Controllers/Internal/v1/OnboardController.php @@ -36,6 +36,11 @@ public function shouldOnboard() */ public function createAccount(OnboardRequest $request) { + // A self-hosted install can close sign-up once its first organization exists + if (!config('fleetbase.onboard.allow_registration', true) && Company::exists()) { + return response()->error('Sign-ups are not available.', 403, ['code' => 'registration_disabled']); + } + // if first user make admin $isAdmin = !User::exists(); diff --git a/tests/Unit/Http/OnboardControllerTest.php b/tests/Unit/Http/OnboardControllerTest.php index ed1cac69..54d8c418 100644 --- a/tests/Unit/Http/OnboardControllerTest.php +++ b/tests/Unit/Http/OnboardControllerTest.php @@ -136,6 +136,7 @@ function onboard_controller_database(): Capsule 'database.default' => 'mysql', 'database.connections.mysql' => $connection, 'fleetbase.connection.db' => 'mysql', + 'fleetbase.onboard.allow_registration' => true, 'permission.models.permission' => Fleetbase\Models\Permission::class, 'permission.models.role' => Fleetbase\Models\Role::class, 'permission.cache.key' => 'spatie.permission.cache', @@ -447,6 +448,35 @@ function onboard_create_account_request(array $input = []): OnboardRequest ->and($capsule->getConnection('mysql')->table('personal_access_tokens')->count())->toBe(1); }); +test('onboard controller refuses new sign-ups once an organization exists when registration is closed', function () { + $capsule = onboard_controller_database(); + onboard_controller_seed_user($capsule); + config(['fleetbase.onboard.allow_registration' => false]); + + $response = onboard_controller()->createAccount(onboard_create_account_request([ + 'name' => 'Katherine Johnson', + 'email' => 'katherine@example.test', + 'organization_name' => 'Orbital Logistics', + ])); + + expect($response->getStatusCode())->toBe(403) + ->and($response->getData(true)['code'])->toBe('registration_disabled') + ->and(User::where('email', 'katherine@example.test')->exists())->toBeFalse() + ->and(Company::where('name', 'Orbital Logistics')->exists())->toBeFalse() + ->and($capsule->getConnection('mysql')->table('companies')->count())->toBe(1); +}); + +test('onboard controller still creates the first organization when registration is closed', function () { + onboard_controller_database(); + config(['fleetbase.onboard.allow_registration' => false]); + + $response = onboard_controller()->createAccount(onboard_create_account_request()); + + expect($response->getStatusCode())->toBe(200) + ->and(User::where('email', 'grace@example.test')->first()->type)->toBe('admin') + ->and(Company::where('name', 'Compiler Logistics')->exists())->toBeTrue(); +}); + test('onboard controller validates verification resend session identity before creating codes', function () { $capsule = onboard_controller_database(); onboard_controller_seed_user($capsule);