feat(tenant): HasUuid + TenantScope (null-user default-deny) + BelongsToTenant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
Boban Blaskovic 2026-05-22 00:53:10 +02:00
parent 4e428cc58e
commit af49f091f5
5 changed files with 88 additions and 0 deletions

View File

@ -0,0 +1,28 @@
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$user = auth()->user();
if ($user === null) {
// Default-deny: null-user must NEVER silently see all records
// (prevents leaks in queue jobs, Tinker, etc.)
$builder->whereRaw('0 = 1');
return;
}
if ($user->hasRole('platform-admin')) {
return; // platform-admin sees all tenants
}
$builder->where($model->getTable() . '.tenant_id', $user->tenant_id);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Traits;
use App\Scopes\TenantScope;
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope(new TenantScope());
static::creating(function ($model) {
$user = auth()->user();
if ($user && !$user->hasRole('platform-admin')) {
// Force tenant_id — prevents cross-tenant mass-assign attacks
$model->tenant_id = $user->tenant_id;
}
});
}
}

20
app/Traits/HasUuid.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace App\Traits;
use Illuminate\Support\Str;
trait HasUuid
{
protected static function bootHasUuid(): void
{
static::creating(function ($model) {
$model->uuid ??= (string) Str::uuid();
});
}
public function getRouteKeyName(): string
{
return 'uuid';
}
}

View File

@ -22,6 +22,9 @@ pest()->extend(TestCase::class)
// ->use(RefreshDatabase::class)
->in('Feature');
pest()->extend(TestCase::class)
->in('Unit');
/*
|--------------------------------------------------------------------------
| Expectations

View File

@ -0,0 +1,16 @@
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('generiert UUID beim Erstellen eines Users', function () {
$user = User::factory()->create();
expect($user->uuid)->toMatch('/^[0-9a-f-]{36}$/');
});
it('nutzt UUID als Route-Key', function () {
$user = User::factory()->create();
expect($user->getRouteKeyName())->toBe('uuid');
});