diff --git a/app/Console/Commands/UserCreate.php b/app/Console/Commands/UserCreate.php index ccad412..07d6c4f 100644 --- a/app/Console/Commands/UserCreate.php +++ b/app/Console/Commands/UserCreate.php @@ -3,7 +3,6 @@ namespace App\Console\Commands; use App\Enums\Users\RoleEnum; -use App\Models\Tenant; use App\Models\User; use Carbon\Carbon; use Illuminate\Console\Command; @@ -80,7 +79,6 @@ class UserCreate extends Command 'role' => $this->role, 'published' => true, 'published_at' => Carbon::now(), - 'tenant_id' => Tenant::query()->first()->id, ]); $user->saveOrFail(); diff --git a/app/Http/Controllers/Bo/TenantController.php b/app/Http/Controllers/Bo/TenantController.php new file mode 100644 index 0000000..ce8ac45 --- /dev/null +++ b/app/Http/Controllers/Bo/TenantController.php @@ -0,0 +1,119 @@ +authorizeResource(Tenant::class); + } + + /** + * Display a listing of the resource. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Contracts\View\View + */ + public function index(Request $request): View + { + /** @var \Illuminate\Database\Eloquent\Builder $query */ + $query = Tenant::query(); + + /** @var string $search Search field */ + $search = $request->input('search'); + if ($search) { + $this->searchQuery( + $query, + $search, + null, + 'name', + ); + } + $searchFields = \implode(', ', [ + trans('validation.attributes.name'), + ]); + + /** Sort columns with a query */ + $this->sortQuery($query); + + /** Custom pagination */ + $tenantModelList = $this->paginate($query); + + return view('bo.pages.tenants.index', compact('tenantModelList', 'search', 'searchFields')); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Contracts\View\View + */ + public function create(): \Illuminate\Contracts\View\View + { + return view('bo.pages.tenants.create'); + } + + /** + * Store a newly created resource in storage. + * + * @param \App\Http\Requests\Bo\Tenants\StoreTenantRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(StoreTenantRequest $request): RedirectResponse + { + return DB::transaction(function () use ($request) { + $tenant = new Tenant(); + $tenant->fill($request->validated()); + + $settings = new Settings(); + + if ($tenant->saveOrFail()) { + return redirect()->route('bo.tenants.index', ['tenant' => \tenant()]) + ->with('success', __('crud.messages.has_been_created', [ + 'model' => Str::ucfirst(\trans_choice('models.classes.tenant', 1)) + ])); + } + return redirect()->back() + ->with('error', __('crud.messages.cannot_be_created', [ + 'model' => Str::ucfirst(\trans_choice('models.classes.tenant', 1)) + ])); + }); + } + + /** + * Remove the specified resource from storage. + * + * @param \App\Models\Tenant $tenant + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Tenant $tenant): RedirectResponse + { + // FIXME: c pt + /** @var array */ + $previousQueries = []; + \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); + if ($tenant->deleteOrFail()) { + return redirect()->route('bo.tenants.index', array_merge(['tenant' => \tenant('id')], $previousQueries)) + ->with('success', __('crud.messages.has_been_deleted', [ + 'model' => Str::ucfirst(\trans_choice('models.classes.tenant', 1)) + ])); + } + return redirect()->back() + ->with('error', __('crud.messages.cannot_be_deleted', [ + 'model' => Str::ucfirst(\trans_choice('models.classes.tenant', 1)) + ])); + } +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 0cc9630..a8a216c 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -18,7 +18,7 @@ class Authenticate extends Middleware protected function redirectTo(Request $request): ?string { $logins = [ - 'backend' => route('bo.login', ['tenant' => Tenant::query()->first()->id]) + 'backend' => route('bo.login') ]; if (!$request->expectsJson()) { // * Notify must login diff --git a/app/Http/Middleware/RestrictTenantAccess.php b/app/Http/Middleware/RestrictTenantAccess.php index 589a59b..519cd0a 100644 --- a/app/Http/Middleware/RestrictTenantAccess.php +++ b/app/Http/Middleware/RestrictTenantAccess.php @@ -3,6 +3,7 @@ namespace App\Http\Middleware; use App\Enums\Users\RoleEnum; +use App\Models\Tenant; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; @@ -20,8 +21,18 @@ class RestrictTenantAccess public function handle(Request $request, Closure $next): Response { $user = \auth('backend')->user(); - if ($user->tenant_id !== $request->tenant && !(RoleEnum::conceptor->equals($user->role))) { - abort(403); + + if ($user) { + if ($request->routeIs('bo.tenants.*')) { + tenancy()->initialize(Tenant::firstOrFail()); + return $next($request); + } + if ( + !\in_array($request->tenant, $user->tenants->pluck('id')->toArray()) && + !(RoleEnum::conceptor->equals($user->role)) + ) { + abort(403); + } } return $next($request); } diff --git a/app/Http/Requests/Bo/Tenants/StoreTenantRequest.php b/app/Http/Requests/Bo/Tenants/StoreTenantRequest.php new file mode 100644 index 0000000..1832cfb --- /dev/null +++ b/app/Http/Requests/Bo/Tenants/StoreTenantRequest.php @@ -0,0 +1,32 @@ + + */ + public function rules(): array + { + return [ + 'name' => 'required|min:2|max:255', + ]; + } +} diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php index b9d677a..f81f7b3 100644 --- a/app/Models/Tenant.php +++ b/app/Models/Tenant.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Lib\Helpers\ToolboxHelper; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Stancl\Tenancy\Database\Models\Tenant as BaseTenant; use Stancl\Tenancy\Contracts\Tenant as TenantContract; use Illuminate\Support\Str; @@ -14,7 +15,10 @@ class Tenant extends BaseTenant implements TenantContract * * @var list */ - protected $fillable = ['id']; + protected $fillable = [ + 'id', + 'name', + ]; /** * Define the custom columns (that won't be stored in the data JSON column) @@ -71,4 +75,16 @@ class Tenant extends BaseTenant implements TenantContract ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id); ToolboxHelper::assertFieldIsUnique($table, 'id', Str::slug($name), $id); } + + // * RELATIONSHIPS + + /** + * The tenants that belong to the user. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 621e2c4..d152926 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,9 +6,9 @@ use App\Enums\Users\RoleEnum; use App\Traits\Models\ActivityLog; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Stancl\Tenancy\Database\Concerns\BelongsToTenant; /** * @property string $first_name Firstname. @@ -27,7 +27,6 @@ class User extends Authenticatable use ActivityLog; use HasFactory; use Notifiable; - use BelongsToTenant; /** * The attributes that are mass assignable. @@ -94,4 +93,16 @@ class User extends Authenticatable ); }); } + + // * RELATIONSHIPS + + /** + * The tenants that belong to the user. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function tenants(): BelongsToMany + { + return $this->belongsToMany(Tenant::class); + } } diff --git a/app/Policies/TenantPolicy.php b/app/Policies/TenantPolicy.php new file mode 100644 index 0000000..8cc9d37 --- /dev/null +++ b/app/Policies/TenantPolicy.php @@ -0,0 +1,59 @@ + Product::class, 'range' => Range::class, 'settings' => Settings::class, - 'user' => User::class, ]; foreach ($tenantRelatedModels as $key => $tenantRelatedModel) { diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index 3995f13..959088f 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Models\Settings; +use App\Models\Tenant; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; @@ -39,6 +40,8 @@ class ViewServiceProvider extends ServiceProvider View::composer('*', function () { View::share('settingsGlobal', Settings::getCached()->where('tenant_id', tenant('id'))->first()); }); + + View::share('tenantGlobal', Tenant::all()); } } } diff --git a/database/migrations/2025_05_16_110725_update_tables_for_multitenant.php b/database/migrations/2025_05_16_110725_update_tables_for_multitenant.php index 7e443df..871cacc 100644 --- a/database/migrations/2025_05_16_110725_update_tables_for_multitenant.php +++ b/database/migrations/2025_05_16_110725_update_tables_for_multitenant.php @@ -13,35 +13,31 @@ return new class extends Migration */ public function up(): void { - Schema::table('activity_logs', function (Blueprint $table) { - $table->string('tenant_id')->index()->after('data'); - }); + $tables = [ + ['name' => 'activity_logs', 'after' => 'data'], + ['name' => 'brands', 'after' => 'status'], + ['name' => 'fonts', 'after' => 'font'], + ['name' => 'products', 'after' => 'range_id'], + ['name' => 'ranges', 'after' => 'font_id'], + ['name' => 'settings', 'after' => 'home_timeout'], + ]; - Schema::table('brands', function (Blueprint $table) { - $table->string('tenant_id')->index()->after('status'); - }); + foreach ($tables as $entry) { + $tableName = $entry['name']; + $after = $entry['after']; - Schema::table('fonts', function (Blueprint $table) { - $table->string('tenant_id')->index()->after('font'); - }); + Schema::table($tableName, function (Blueprint $table) use ($after) { + $table->string('tenant_id')->index()->after($after)->comment('Tenant reference.'); + }); - Schema::table('products', function (Blueprint $table) { - $table->string('tenant_id')->index()->after('range_id'); - }); - - Schema::table('ranges', function (Blueprint $table) { - $table->string('tenant_id')->index()->after('font_id'); - }); - - Schema::table('settings', function (Blueprint $table) { - $table->string('tenant_id')->index()->after('home_timeout'); - }); - - Schema::table('users', function (Blueprint $table) { - $table->string('tenant_id')->index()->after('published'); - $table->dropUnique(['email']); - $table->unique(['tenant_id', 'email']); - }); + Schema::table($tableName, function (Blueprint $table) use ($tableName) { + $table->foreign('tenant_id', $tableName . '_tenant_id_foreign') + ->references('id') + ->on('tenants') + ->onUpdate('cascade') + ->onDelete('cascade'); + }); + } //end foreach } /** @@ -51,29 +47,20 @@ return new class extends Migration */ public function down(): void { - Schema::table('activity_logs', function (Blueprint $table) { - $table->dropColumn('tenant_id'); - }); + $tables = [ + 'activity_logs', + 'brands', + 'fonts', + 'products', + 'ranges', + 'settings', + ]; - Schema::table('brands', function (Blueprint $table) { - $table->dropColumn('tenant_id'); - }); - - Schema::table('fonts', function (Blueprint $table) { - $table->dropColumn('tenant_id'); - }); - - Schema::table('products', function (Blueprint $table) { - $table->dropColumn('tenant_id'); - }); - - Schema::table('ranges', function (Blueprint $table) { - $table->dropColumn('tenant_id'); - }); - - Schema::table('settings', function (Blueprint $table) { - $table->dropColumn('tenant_id'); - }); + foreach ($tables as $entry) { + Schema::table($entry, function (Blueprint $table) { + $table->dropColumn('tenant_id'); + }); + } Schema::table('users', function (Blueprint $table) { $table->dropUnique(['tenant_id', 'email']); diff --git a/database/migrations/2025_05_16_110726_create_tenant_user_table.php b/database/migrations/2025_05_16_110726_create_tenant_user_table.php new file mode 100644 index 0000000..8072f5b --- /dev/null +++ b/database/migrations/2025_05_16_110726_create_tenant_user_table.php @@ -0,0 +1,36 @@ +unsignedBigInteger('user_id'); + $table->string('tenant_id', 255); + + $table->primary(['user_id', 'tenant_id']); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('tenant_user'); + } +}; diff --git a/resources/ts/bo/utilities/fontawesome.ts b/resources/ts/bo/utilities/fontawesome.ts index 0f41552..64c6c57 100644 --- a/resources/ts/bo/utilities/fontawesome.ts +++ b/resources/ts/bo/utilities/fontawesome.ts @@ -12,6 +12,7 @@ import { faArrowsAltV, faBackspace, faBan, + faBuilding, faCheck, faChevronLeft, faChevronRight, @@ -90,6 +91,7 @@ library.add(faEarthAmericas); library.add(faG); library.add(faP); library.add(faTriangleExclamation); +library.add(faBuilding); // Front library.add(faChevronLeft); diff --git a/resources/views/bo/layouts/header.blade.php b/resources/views/bo/layouts/header.blade.php index 001090b..147147d 100644 --- a/resources/views/bo/layouts/header.blade.php +++ b/resources/views/bo/layouts/header.blade.php @@ -72,6 +72,24 @@ @auth + + +