From 00d5abae5f8757c3e5a13e2b56240dfac26ab790 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski Date: Fri, 3 Apr 2026 22:33:56 +0200 Subject: [PATCH] Eliminate Company\CompaniesController, introduce owner role Redistribute methods: - show() -> BootstrapController::currentCompany() - store(), destroy(), userCompanies() -> Admin\CompaniesController - transferOwnership() -> CompanySettingsController Security fix: introduce 'owner' role for company-level admin, distinct from 'super admin' which is now global platform admin only. - CompanyService::setupRoles() creates 'owner' role per company - Company creation assigns scoped 'owner' role instead of global 'super admin' - Seeders updated to assign 'owner' Migration renames all existing company-scoped 'super admin' roles to 'owner' and ensures every company owner has the role assigned. --- .../Controllers/Admin/CompaniesController.php | 60 ++++++++++++ .../Company/CompaniesController.php | 97 ------------------- .../Company/General/BootstrapController.php | 7 ++ .../Settings/CompanySettingsController.php | 23 +++++ app/Services/CompanyService.php | 8 +- ...me_company_scoped_super_admin_to_owner.php | 47 +++++++++ database/seeders/DemoSeeder.php | 2 +- database/seeders/UsersTableSeeder.php | 2 +- routes/api.php | 14 +-- tests/Feature/Admin/CompanyTest.php | 2 +- 10 files changed, 151 insertions(+), 111 deletions(-) delete mode 100644 app/Http/Controllers/Company/CompaniesController.php create mode 100644 database/migrations/2026_04_03_203140_rename_company_scoped_super_admin_to_owner.php diff --git a/app/Http/Controllers/Admin/CompaniesController.php b/app/Http/Controllers/Admin/CompaniesController.php index 6e628832..8bb11190 100644 --- a/app/Http/Controllers/Admin/CompaniesController.php +++ b/app/Http/Controllers/Admin/CompaniesController.php @@ -2,14 +2,22 @@ namespace App\Http\Controllers\Admin; +use App\Facades\Hashids; use App\Http\Controllers\Controller; use App\Http\Requests\AdminCompanyUpdateRequest; +use App\Http\Requests\CompaniesRequest; use App\Http\Resources\CompanyResource; use App\Models\Company; +use App\Services\CompanyService; use Illuminate\Http\Request; +use Silber\Bouncer\BouncerFacade; class CompaniesController extends Controller { + public function __construct( + private readonly CompanyService $companyService, + ) {} + public function index(Request $request) { $companies = Company::query() @@ -54,4 +62,56 @@ class CompaniesController extends Controller return new CompanyResource($company); } + + public function store(CompaniesRequest $request) + { + $this->authorize('create company'); + + $user = $request->user(); + + $company = Company::create($request->getCompanyPayload()); + $company->unique_hash = Hashids::connection(Company::class)->encode($company->id); + $company->save(); + $this->companyService->setupDefaults($company); + $user->companies()->attach($company->id); + + BouncerFacade::scope()->to($company->id); + $user->assign('owner'); + + if ($request->address) { + $company->address()->create($request->address); + } + + return new CompanyResource($company); + } + + public function destroy(Request $request) + { + $company = Company::find($request->header('company')); + + $this->authorize('delete company', $company); + + $user = $request->user(); + + if ($request->name !== $company->name) { + return respondJson('company_name_must_match_with_given_name', 'Company name must match with given name'); + } + + if ($user->loadCount('companies')->companies_count <= 1) { + return respondJson('You_cannot_delete_all_companies', 'You cannot delete all companies'); + } + + $this->companyService->delete($company, $user); + + return response()->json([ + 'success' => true, + ]); + } + + public function userCompanies(Request $request) + { + $companies = $request->user()->companies; + + return CompanyResource::collection($companies); + } } diff --git a/app/Http/Controllers/Company/CompaniesController.php b/app/Http/Controllers/Company/CompaniesController.php deleted file mode 100644 index 1db7a6e8..00000000 --- a/app/Http/Controllers/Company/CompaniesController.php +++ /dev/null @@ -1,97 +0,0 @@ -header('company')); - - return new CompanyResource($company); - } - - public function store(CompaniesRequest $request) - { - $this->authorize('create company'); - - $user = $request->user(); - - $company = Company::create($request->getCompanyPayload()); - $company->unique_hash = Hashids::connection(Company::class)->encode($company->id); - $company->save(); - $this->companyService->setupDefaults($company); - $user->companies()->attach($company->id); - $user->assign('super admin'); - - if ($request->address) { - $company->address()->create($request->address); - } - - return new CompanyResource($company); - } - - public function destroy(Request $request) - { - $company = Company::find($request->header('company')); - - $this->authorize('delete company', $company); - - $user = $request->user(); - - if ($request->name !== $company->name) { - return respondJson('company_name_must_match_with_given_name', 'Company name must match with given name'); - } - - if ($user->loadCount('companies')->companies_count <= 1) { - return respondJson('You_cannot_delete_all_companies', 'You cannot delete all companies'); - } - - $this->companyService->delete($company, $user); - - return response()->json([ - 'success' => true, - ]); - } - - public function transferOwnership(Request $request, User $user) - { - $company = Company::find($request->header('company')); - $this->authorize('transfer company ownership', $company); - - if (! $user->hasCompany($company->id)) { - return response()->json([ - 'success' => false, - 'message' => 'User does not belong to this company.', - ]); - } - - $company->update(['owner_id' => $user->id]); - BouncerFacade::sync($user)->roles(['super admin']); - - return response()->json([ - 'success' => true, - ]); - } - - public function getUserCompanies(Request $request) - { - $companies = $request->user()->companies; - - return CompanyResource::collection($companies); - } -} diff --git a/app/Http/Controllers/Company/General/BootstrapController.php b/app/Http/Controllers/Company/General/BootstrapController.php index 45bad5f7..0ac23e0b 100644 --- a/app/Http/Controllers/Company/General/BootstrapController.php +++ b/app/Http/Controllers/Company/General/BootstrapController.php @@ -76,4 +76,11 @@ class BootstrapController extends Controller 'modules' => Module::where('enabled', true)->pluck('name'), ]); } + + public function currentCompany(Request $request) + { + $company = Company::find($request->header('company')); + + return new CompanyResource($company); + } } diff --git a/app/Http/Controllers/Company/Settings/CompanySettingsController.php b/app/Http/Controllers/Company/Settings/CompanySettingsController.php index 1a5bb288..4f8507ad 100644 --- a/app/Http/Controllers/Company/Settings/CompanySettingsController.php +++ b/app/Http/Controllers/Company/Settings/CompanySettingsController.php @@ -7,9 +7,11 @@ use App\Http\Requests\GetSettingsRequest; use App\Http\Requests\UpdateSettingsRequest; use App\Models\Company; use App\Models\CompanySetting; +use App\Models\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Arr; +use Silber\Bouncer\BouncerFacade; class CompanySettingsController extends Controller { @@ -55,4 +57,25 @@ class CompanySettingsController extends Controller 'has_transactions' => $company->hasTransactions(), ]); } + + public function transferOwnership(Request $request, User $user): JsonResponse + { + $company = Company::find($request->header('company')); + $this->authorize('transfer company ownership', $company); + + if (! $user->hasCompany($company->id)) { + return response()->json([ + 'success' => false, + 'message' => 'User does not belong to this company.', + ]); + } + + $company->update(['owner_id' => $user->id]); + BouncerFacade::scope()->to($company->id); + BouncerFacade::sync($user)->roles(['owner']); + + return response()->json([ + 'success' => true, + ]); + } } diff --git a/app/Services/CompanyService.php b/app/Services/CompanyService.php index 1513c691..d58619ce 100644 --- a/app/Services/CompanyService.php +++ b/app/Services/CompanyService.php @@ -26,14 +26,14 @@ class CompanyService { BouncerFacade::scope()->to($company->id); - $superAdmin = BouncerFacade::role()->firstOrCreate([ - 'name' => 'super admin', - 'title' => 'Super Admin', + $owner = BouncerFacade::role()->firstOrCreate([ + 'name' => 'owner', + 'title' => 'Owner', 'scope' => $company->id, ]); foreach (config('abilities.abilities') as $ability) { - BouncerFacade::allow($superAdmin)->to($ability['ability'], $ability['model']); + BouncerFacade::allow($owner)->to($ability['ability'], $ability['model']); } } diff --git a/database/migrations/2026_04_03_203140_rename_company_scoped_super_admin_to_owner.php b/database/migrations/2026_04_03_203140_rename_company_scoped_super_admin_to_owner.php new file mode 100644 index 00000000..666aafc5 --- /dev/null +++ b/database/migrations/2026_04_03_203140_rename_company_scoped_super_admin_to_owner.php @@ -0,0 +1,47 @@ +where('name', 'super admin') + ->update([ + 'name' => 'owner', + 'title' => 'Owner', + ]); + + // Ensure every company owner has the owner role + foreach (Company::whereNotNull('owner_id')->get() as $company) { + BouncerFacade::scope()->to($company->id); + $user = User::find($company->owner_id); + if ($user && ! $user->isA('owner')) { + $user->assign('owner'); + } + } + } + + /** + * Reverse: rename owner roles back to super admin. + */ + public function down(): void + { + Role::whereNotNull('scope') + ->where('name', 'owner') + ->update([ + 'name' => 'super admin', + 'title' => 'Super Admin', + ]); + } +}; diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index 67eeaa40..4a508f5c 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -40,7 +40,7 @@ class DemoSeeder extends Seeder $user->companies()->attach($company->id); BouncerFacade::scope()->to($company->id); - $user->assign('super admin'); + $user->assign('owner'); // Set default user settings $user->setSettings([ diff --git a/database/seeders/UsersTableSeeder.php b/database/seeders/UsersTableSeeder.php index 25921757..90e1ebd4 100644 --- a/database/seeders/UsersTableSeeder.php +++ b/database/seeders/UsersTableSeeder.php @@ -37,7 +37,7 @@ class UsersTableSeeder extends Seeder $user->companies()->attach($company->id); BouncerFacade::scope()->to($company->id); - $user->assign('super admin'); + $user->assign('owner'); Setting::setSetting('profile_complete', 0); // Set version. diff --git a/routes/api.php b/routes/api.php index 479cc6ad..f4898b96 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ group(function () { // ---------------------------------- Route::middleware(['auth:sanctum', 'super-admin'])->prefix('super-admin')->group(function () { - Route::get('companies', [App\Http\Controllers\Admin\CompaniesController::class, 'index']); - Route::get('companies/{company}', [App\Http\Controllers\Admin\CompaniesController::class, 'show']); - Route::put('companies/{company}', [App\Http\Controllers\Admin\CompaniesController::class, 'update']); + Route::get('companies', [CompaniesController::class, 'index']); + Route::get('companies/{company}', [CompaniesController::class, 'show']); + Route::put('companies/{company}', [CompaniesController::class, 'update']); Route::get('users', [App\Http\Controllers\Admin\UsersController::class, 'index']); Route::get('users/{user}', [App\Http\Controllers\Admin\UsersController::class, 'show']); @@ -214,7 +214,7 @@ Route::prefix('/v1')->group(function () { Route::get('/number-placeholders', [SerialNumberController::class, 'placeholders']); - Route::get('/current-company', [CompaniesController::class, 'show']); + Route::get('/current-company', [BootstrapController::class, 'currentCompany']); // Customers // ---------------------------------- @@ -418,11 +418,11 @@ Route::prefix('/v1')->group(function () { Route::post('companies', [CompaniesController::class, 'store']); - Route::post('/transfer/ownership/{user}', [CompaniesController::class, 'transferOwnership']); + Route::post('/transfer/ownership/{user}', [CompanySettingsController::class, 'transferOwnership']); Route::post('companies/delete', [CompaniesController::class, 'destroy']); - Route::get('companies', [CompaniesController::class, 'getUserCompanies']); + Route::get('companies', [CompaniesController::class, 'userCompanies']); // Users // ---------------------------------- diff --git a/tests/Feature/Admin/CompanyTest.php b/tests/Feature/Admin/CompanyTest.php index 0103ede0..e15dbf5f 100644 --- a/tests/Feature/Admin/CompanyTest.php +++ b/tests/Feature/Admin/CompanyTest.php @@ -1,6 +1,6 @@