Tüm SaaS uygulamaları her istekte “hangi şirket içinde kim ne yapabilir?” sorusuna yanıt vermelidir.
Basit gibi görünüyor. Ama değil.
Kohana.io üzerinde çalışıyorum – küçük işletmeler için bir SaaS CRM/ERP. Çoklu kiracılık modülünü doğru hale getirmek, diğer modüllerden daha uzun sürdü. Bu, kodun karmaşık olduğu anlamına gelmiyor, ama kararlar karmaşık: tek veritabanı mı yoksa ayrı veritabanları mı? Yapılandırma odaklı mı yoksa veritabanı odaklı izinler mi? İzin çelişkilerini nasıl ele alırsınız? Bir kullanıcı 403 aldığında ne olur?
Artık bu modülü LaraFoundry’ye çıkarıyorum – her kişinin bu kararları sıfırdan yapmasına gerek kalmaması için açık kaynak bir Laravel SaaS çerçevesi.
İşte tam olarak nasıl çalıştığı:
<hr/>
<h2>
<a name="architecture-overview" href="#architecture-overview"></a>
Mimari Genel Görünüm
</h2>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Komponent</th>
<th>Amaç</th>
</tr>
</thead>
<tbody>
<tr>
<td>'BelongsToCompany' trait</td>
<td>Aktif şirket için otomatik sorgu filtreleme</td>
</tr>
<tr>
<td>'config/roles-and-permissions.php'</td>
<td>Tüm izinler tek bir yerde tanımlanır</td>
</tr>
<tr>
<td>Gate sınıfları (8 dosya)</td>
<td>Modül bazında karmaşık yetkilendirme mantığı</td>
</tr>
<tr>
<td>'HasRolesAndPermissions' trait</td>
<td>Kullanıcı modelinde 5 seviyeli izin hiyerarşisi</td>
</tr>
<tr>
<td>'SetActiveCompanyMiddleware'</td>
<td>Kiralayan bağlamını otomatik olarak çözer</td>
</tr>
<tr>
<td>'CheckAccessMiddleware'</td>
<td>Ban + ödeme durumu kontrolleri</td>
</tr>
<tr>
<td>'LayoutDataService'</td>
<td>İzin farkındalığı olan menü + FAR yönlendirmesi</td>
</tr>
</tbody>
</table>
</div>
<p><strong>Teknoloji yığını:</strong> Laravel 12, Inertia.js v2, Vue 3, Pest PHP</p>
<hr/>
<h2>
<a name="1-data-isolation-the-belongstocompany-trait" href="#1-data-isolation-the-belongstocompany-trait"></a>
1. Veri İzolasyonu - BelongsToCompany Traiti
</h2>
<p>Çokça kiracılığın en korkutucu hatası veri sızıntısıdır. Şirket A'nın siparişlerini Şirket B'ye göstermek.</p>
<p>Bir unutulmuş 'where('company_id', ...)' yeter ve GDPR kabusu başlar.</p>
<p>LaraFoundry bunu Eloquent model seviyesinde çözüyor:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="kd">trait</span> <span class="nc">BelongsToCompany</span>{
protected static function bootBelongsToCompany(): void
{
static::addGlobalScope(‘company’, function (Builder $builder) {
if (auth()->check()) {
$companyId = auth()->user()->getCurrentCompanyId();
if ($companyId) {
$builder->where(
$builder->getModel()->getTable() . ‘.company_id’,
$companyId
);
}
}
});
}
<span class="k">public</span> <span class="k">function</span> <span class="n">scopeForCompany</span><span class="p">(</span><span class="kt">Builder</span> <span class="nv">$query</span><span class="p">,</span> <span class="kt">int</span> <span class="nv">$companyId</span><span class="p">):</span> <span class="kt">Builder</span>
<span class="p">{</span>
<span class="k">return</span> <span class="nv">$query</span><span class="o">-></span><span class="nf">withoutGlobalScope</span><span class="p">(</span><span class="s1">'company'</span><span class="p">)</span>
<span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">getTable</span><span class="p">()</span> <span class="mf">.</span> <span class="s1>'.company_id'</span><span class="p">,</span> <span class="nv">$companyId</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">scopeForAdmin</span><span class="p">(</span><span class="kt">Builder</span> <span class="nv">$query</span><span class="p">):</span> <span class="kt">Builder</span>
<span class="p">{</span>
<span class="k">return</span> <span class="nv">$query</span><span class="o">-></span><span class="nf">withoutGlobalScope</span><span class="p">(</span><span class="s1">'company'</span><span class="p">);</span>
<span class="p">}</span>}
<p><strong>Kullanım:</strong><br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="c1">// Kontrolcü - filtreleme gerekli değil</span>$orders = Order::query()->latest()->paginate(20);
// Otomatik olarak: SELECT * FROM orders WHERE company_id = 5
// Yönetici paneli – her şeyi gör
$allOrders = Order::forAdmin()->latest()->paginate(50);
// Belirli bir şirket için sorgu
$report = Order::forCompany(7)->where(‘status’, <span class=”s1>’completed’)->get();
<p>Tablo ön eki ('$builder->getModel()->getTable() . '.company_id'') join’lerde belirsiz sütun hatalarını önler. Küçük bir ayrıntı, saatlerce hata ayıklama süresini kurtarır.</p>
<hr/>
<h2>
<a name="2-configdriven-permission-registration" href="#2-configdriven-permission-registration"></a>
2. Yapılandırma Tabanlı İzin Kaydı
</h2>
<p>Tüm izinler 'config/roles-and-permissions.php' dosyasında tanımlanır:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="s1">'permissions'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'orders'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'label'</span> <span class="o">=></span> <span class="s1>'Orders'</span><span class="p">,</span>
<span class="s1>'permissions'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'orders.view'</span> <span class="o">=></span> <span class="s1>'View orders'</span><span class="p">,</span>
<span class="s1>'orders.create'</span> <span class="o">=></span> <span class="s1>'Create orders'</span><span class="p">,</span>
<span class="s1>'orders.update'</span> <span class="o">=></span> <span class="s1>'Update orders'</span><span class="p">,</span>
<span class="s1>'orders.delete'</span> <span class="o">=></span> <span class="s1>'Delete orders'</span><span class="p">,</span>
<span class="s1>'orders.approve'</span> <span class="o">=></span> <span class="s1>'Approve orders'</span><span class="p">,</span>
<span class="s1>'orders.status_change'</span> <span class="o">=></span> <span class="s1>'Change order status'</span><span class="p">,</span>
<span class="s1>'orders.export'</span> <span class="o">=></span> <span class="s1>'Export orders'</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="s1>'warehouse'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'label'</span> <span class="o">=></span> <span class="s1>'Warehouse'</span><span class="p">,</span>
<span class="s1>'permissions'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'warehouse.view'</span> <span class="o">=></span> <span class="s1>'View warehouse'</span><span class="p">,</span>
<span class="s1>'warehouse.create'</span> <span class="o">=></span> <span class="s1>'Add to warehouse'</span><span class="p">,</span>
<span class="s1>'warehouse.update'</span> <span class="o">=></span> <span class="s1>'Update warehouse'</span><span class="p">,</span>
<span class="s1>'warehouse.delete'</span> <span class="o">=></span> <span class="s1>'Delete from warehouse'</span><span class="p">,</span>
<span class="s1>'warehouse.inventory'</span> <span class="o">=></span> <span class="s1>'Inventory'</span><span class="p">,</span>
<span class="s1>'warehouse.transfer'</span> <span class="o">=></span> <span class="s1>'Transfer goods'</span><span class="p">,</span>
<span class="s1>'warehouse.reserve'</span> <span class="o">=></span> <span class="s1>'Reserve goods'</span><span class="p">,</span>
<span class="s1>'warehouse.assembly'</span> <span class="o">=></span> <span class="s1>'Order assembly'</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="c1">// 20+ modül, 100+ izin toplam</span>],
<p>'AuthServiceProvider' bu yapılandırmayı okur ve her izin için bir Gate kaydeder:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="k">protected</span> <span class="k">function</span> <span class="n">registerPermissionGates</span><span class="p">():</span> <span class="kt">void</span>{
$permissions = $this->getAllPermissionsFromConfig();
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$permissions</span> <span class="k">as</span> <span class="nv">$permissionSlug</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">Gate</span><span class="o">::</span><span class="nb">define</span><span class="p">(</span><span class="nv">$permissionSlug</span><span class="p">,</span> <span class="k">function</span> <span class="p">(</span><span class="kt">User</span> <span class="nv">$user</span><span class="p">,</span> <span class="kt">?Company</span> <span class="nv">$company</span> <span class="o">=</span> <span class="kc">null</span><span class="p">)</span> <span class="k">use</span> <span class="p">(</span><span class="nv">$permissionSlug</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$company</span> <span class="o">=</span> <span class="nv">$company</span> <span class="o">??</span> <span class="nv">$user</span><span class="o">-></span><span class="nf">getActiveCompany</span><span class="p">();</span>
<span class="k">return</span> <span class="nv">$user</span><span class="o">-></span><span class="nf">hasPermissionTo</span><span class="p">(</span><span class="nv>$permissionSlug</span><span class="p">,</span> <span class="nv">$company</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>}
<p>Tek bir döngü. Tüm izinler kaydedildi. Yeni bir izin eklemek ister misiniz? Yapılandırmaya bir satır ekleyin, 'php artisan permissions:sync' çalıştırın.</p>
<hr/>
<h2>
<a name="3-dedicated-gate-classes-for-complex-logic" href="#3-dedicated-gate-classes-for-complex-logic"></a>
3. Karmaşık Mantık için Ayrı Gate Sınıfları
</h2>
<p>Yapılandırma, izinlerin %90'ını (basit CRUD) işler. Ancak bazı yetkilendirme mantığı iş kurallarına sahiptir. Örneğin bir çalışanı kaldırma:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="c1">// app/Gates/EmployeeGates.php</span>class EmployeeGates
{
public static function register(): void
{
Gate::define(<span class=”s1>’employees.remove’, function (User $user, User $employee) {
$company = $user->getActiveCompany();
<span class="k">if</span> <span class="p">(</span><span class="nv">$user</span><span class="o">-></span><span class="n">id</span> <span class="o">===</span> <span class="nv">$employee</span><span class="o">-></span><span class="n">id</span><span class="p">)</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="c1">// kendini kaldıramaz</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$employee</span><span class="o">-></span><span class="nf">isOwnerOf</span><span class="p">(</span><span class="nv">$company</span><span class="p">))</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="c1">// sahip olanı kaldıramaz</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$employee</span><span class="o">-></span><span class="n">companies</span><span class="o">-></span><span class="nf">contains</span><span class="p">(</span><span class="nv">$company</span><span class="p">))</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="c1">// yanlış şirket</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$user</span><span class="o">-></span><span class="nf">isOwnerOf</span><span class="p">(</span><span class="nv">$company</span><span class="p">))</span> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span> <span class="c1">// sahibi herkes kaldırabilir</span>
<span class="k">return</span> <span class="nv">$user</span><span class="o">-></span><span class="nf">hasPermissionTo</span><span class="p">(</span><span class="s1>'company.employees.remove'</span><span class="p">,</span> <span class="nv>$company</span><span class="p">);</span>
<span class="p">});</span>
<span class="c1">// employees.assignRole, employees.grantPermissions,</span>
<span class="c1">// employees.manageRolesAndPermissions, employees.requestRemoval,</span>
<span class="c1">// employees.confirmRemoval...</span>
<span class="p">}</span>}
<p>Her modül kendi Gate sınıfına sahip. Tüm kaydedilmeler 'AuthServiceProvider' tarafından:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="k">protected</span> <span class="k">function</span> <span class="n">registerCustomGates</span><span class="p">():</span> <span class="kt">void</span>{
CompanyGates::register();
EmployeeGates::register();
RoleGates::register();
ContragentGates::register();
WarehouseGates::register();
ProductionGates::register();
}
<p><strong>Sonuç:</strong> AuthServiceProvider 60 satırın altında kalır. Her Gate sınıfı bağımsız bir şekilde test edilebilir.</p>
<hr/>
<h2>
<a name="4-the-5level-permission-hierarchy" href="#4-the-5level-permission-hierarchy"></a>
4. 5 Seviyeli İzin Hiyerarşisi
</h2>
<p>Sistemin merkezi. Beş seviye, üstten alta kontrol edilir:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>Level 1: Süper Yönetici -> her şeyi atlarLevel 2: Şirket Sahibi -> kendi şirketine tam erişim
Level 3: İptal Edilmiş -> açıkça engellenmiş (rolleri geçersiz kılar!)
Level 4: Bireysel -> açıkça verilmiş (rolleri geçersiz kılar)
Level 5: Rol tabanlı -> rollerden miras alınan izinler
<p>Uygulama:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="k">public</span> <span class="k">function</span> <span class="n">hasPermissionTo</span><span class="p">(</span><span class="kt">string</span> <span class="nv">$permissionSlug</span><span class="p">,</span> <span class="kt">Company</span><span class="o">|</span><span class="n">int</span><span class="o">|</span><span class="kc">null</span> <span class="nv">$company</span> <span class="o">=</span> <span class="kc">null</span><span class="p">):</span> <span class="kt">bool</span>{
// Seviye 1: Süper yönetici
if ($this->isSuperAdmin()) return true;
<span class="c1">// Seviye 2: Şirket sahibi</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$company</span> <span class="o">&&</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">isOwnerOf</span><span class="p">(</span><span class="nv">$company</span><span class="p">))</span> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="nv">$companyId</span> <span class="o">=</span> <span class="nv">$company</span> <span class="k">instanceof</span> <span class="nc">Company</span> <span class="o">?</span> <span class="nv">$company</span><span class="o">-></span><span class="n">id</span> <span class="o">:</span> <span class="nv">$company</span><span class="p">;</span>
<span class="c1">// Seviye 3: Açıkça iptal edilmişse kontrol et</span>
<span class="nv">$isRevoked</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">permissions</span><span class="p">()</span>
<span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1>'permissions.slug'</span><span class="p">,</span> <span class="nv">$permissionSlug</span><span class="p">)</span>
<span class="o">-></span><span class="nf">wherePivot</span><span class="p">(</span><span class="s1>'company_id'</span><span class="p">,</span> <span class="nv">$companyId</span><span class="p">)</span>
<span class="o">-></span><span class="nf">wherePivot</span><span class="p">(</span><span class="s1>'is_revoked'</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span>
<span class="o">-></span><span class="nf">exists</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$isRevoked</span><span class="p">)</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="c1">// Seviye 4: Açıkça verilmişse kontrol et</span>
<span class="nv">$isGranted</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">permissions</span><span class="p">()</span>
<span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1>'permissions.slug'</span><span class="p">,</span> <span class="nv">$permissionSlug</span><span class="p">)</span>
<span class="o">-></span><span class="nf">wherePivot</span><span class="p">(</span><span class="s1>'company_id'</span><span class="p">,</span> <span class="nv">$companyId</span><span class="p">)</span>
<span class="o">-></span><span class="nf">wherePivot</span><span class="p">(</span><span class="s1>'is_revoked'</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span>
<span class="o">-></span><span class="nf">exists</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$isGranted</span><span class="p">)</span> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="c1">// Seviye 5: Rol izinlerini kontrol et (şirket bazında)</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">roles</span><span class="p">()</span>
<span class="o">-></span><span class="nf">wherePivot</span><span class="p">(</span><span class="s1>'company_id'</span><span class="p">,</span> <span class="nv">$companyId</span><span class="p">)</span>
<span class="o">-></span><span class="nf">whereHas</span><span class="p">(</span><span class="s1>'permissions'</span><span class="p">,</span> <span class="k">fn</span><span class="p">(</span><span class="nv">$q</span><span class="p">)</span> <span class="o">=></span> <span class="nv">$q</span><span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1>'slug'</span><span class="p">,</span> <span class="nv>$permissionSlug</span><span class="p">))</span>
<span class="o">-></span><span class="nf">exists</span><span class="p">();</span>}
<p><strong>Bu sıranın önemi:</strong></p>
<p>'Manager' rolü ile 'orders.delete' iznine sahip bir durum düşünün. Ama belirli bir yönetici siparişleri silmemeli. Yeni bir rol oluşturmak yerine, şirket sahibi yalnızca o izni iptal eder. Tamamlandı.</p>
<p>Aynı şekilde - 'Worker' rolü 'production.assign'e sahip değil, ama bir kıdemli çalışanın bu izne ihtiyacı var. Bunu bireysel olarak verin.</p>
<p>'is_revoked' bayrağı, 'user_permissions' pivot tablosundaki her şeyi mümkün kılar:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>user_permissions: user_id, permission_id, company_id, is_revoked, granted_by_id<p>Tek bir boolean. Bu kadar çok esneklik.</p>
<hr/>
<h2>
<a name="5-role-templates-amp-custom-roles" href="#5-role-templates-amp-custom-roles"></a>
5. Rol Şablonları ve Özel Roller
</h2>
<p>Yeni bir şirket oluşturulduğunda, 5 rol şablonu otomatik olarak klonlanır:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="s1>'role_templates'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'manager'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'name'</span> <span class="o">=></span> <span class="s1>'Manager'</span><span class="p">,</span>
<span class="s1>'description'</span> <span class="o">=></span> <span class="s1>'Siparişleri yönet, karşı taraflarla işlemleri, raporları gör'</span><span class="p">,</span>
<span class="s1>'permissions'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1>'orders.view'</span><span class="p">,</span>
<span class="s1>'orders.create'</span><span class="p">,</span>
<span class="s1>'orders.update'</span><span class="p">,</span>
<span class="s1>'orders.delete'</span><span class="p">,</span>
<span class="s1>'contragents.viewAny'</span><span class="p">,</span>
<span class="s1>'contragents.view'</span><span class="p">,</span>
<span class="s1>'contragents.create'</span><span class="p">,</span>
<span class="s1>'production.view'</span><span class="p">,</span>
<span class="s1>'production.create'</span><span class="p">,</span>
<span class="s1>'dashboard.view'</span><span class="p">,</span>
<span class="c1">// ...20+ izin</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="s1>'accountant'</span> <span class="o">=></span> <span class="p">[</span> <span class="cm">/* ... */</span> <span class="p">],</span>
<span class="s1>'storekeeper'</span> <span class="o">=></span> <span class="p">[</span> <span class="cm">/* ... */</span> <span class="p">],</span>
<span class="s1>'logistician'</span> <span class="o">=></span> <span class="p">[</span> <span class="cm">/* ... */</span> <span class="p">],</span>
<span class="s1>'worker'</span> <span class="o">=></span> <span class="p">[</span> <span class="cm">/* ... */</span> <span class="p">],</span>],
<p>Şirket sahibi şunları yapabilir:</p>
<ul>
<li>Şablon rollerini düzenleme (izin ekleme/çıkarma)</li>
<li>Tamamen özel roller oluşturma</li>
<li>Bir çalışana birden fazla rol atama</li>
<li>Herhangi bir rol iznini bireysel kullanıcı bazında geçersiz kılma</li>
<li>Özel rolleri silme (atama yoksa)</li>
</ul>
<p>Tüm bunlar UI üzerinden. Geliştiriciye ihtiyaç yok.</p>
<hr/>
<h2>
<a name="6-middleware-stack-tenant-context" href="#6-middleware-stack-tenant-context"></a>
6. Middleware Yığını - Kiracı Bağlamı
</h2>
<p>Üç middleware bileşeni, çoklu kiracılık yaşam döngüsünü yönetir:</p>
<h3>
<a name="setactivecompanymiddleware" href="#setactivecompanymiddleware"></a>
SetActiveCompanyMiddleware
</h3>
<p>Her web isteğinde çalışır. Aktif şirket bağlamını ayarlar:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">(</span><span class="kt">Request</span> <span class="nv">$request</span><span class="p">,</span> <span class="kt">Closure</span> <span class="nv">$next</span><span class="p">):</span> <span class="kt">Response</span>{
if (!auth()->check() || !auth()->user()->hasVerifiedEmail()) {
return $next($request);
}
<span class="nv">$user</span> <span class="o">=</span> <span class="nf">auth</span><span class="p">()</span><span class="o">-></span><span class="nf">user</span><span class="p">();</span>
<span class="c1">// Zaten aktif şirket var mı? Sahipliği doğrula</span>
<span class="k">if</span> <span class="p">(</span><span class="nf">session</span><span class="p">()</span><span class="o">-></span><span class="nf">has</span><span class="p">(</span><span class="s1>'active_company_id'</span><span class="p">))</span> <span class="p">{</span>
<span class="nv">$companyBelongsToUser</span> <span class="o">=</span> <span class="nv">$user</span><span class="o">-></span><span class="nf">companies</span><span class="p">()</span>
<span class="o">-></span><span class="nf">wherePivot</span><span class="p">(</span><span class="s1>'is_deleted'</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span>
<span class="o">-></span><span class="nf">where</span><span class="p">(</span><span class="s1>'companies.id'</span><span class="p">,</span> <span class="nf">session</span><span class="p">(</span><span class="s1>'active_company_id'</span><span class="p">))</span>
<span class="o">-></span><span class="nf">exists</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$companyBelongsToUser</span><span class="p">)</span> <span class="k">return</span> <span class="nv">$next</span><span class="p">(</span><span class="nv">$request</span><span class="p">);</span>
<span class="nf">session</span><span class="p">()-><span class="nf">forget</span><span class="p">(</span><span class="s1>'active_company_id'</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// Öncelik: sahip olunan şirket > çalışan şirketi</span>
<span class="nv">$ownedCompany</span> <span class="o">=</span> <span class="nv">$user</span><span class="o">-></span><span class="n">companies</span><span class="o">-></span><span class="nf">first</span><span class="p">(</span>
<span class="k">fn</span><span class="p">(</span><span class="nv">$company</span><span class="p">)</span> <span class="o">=></span> <span class="nv">$user</span><span class="o">-></span><span class="nf">isOwnerOf</span><span class="p">(</span><span class="nv">$company</span><span class="p">)</span>
<span class="p">);</span>
<span class="nv">$user</span><span class="o">-></span><span class="nf">setActiveCompany</span><span class="p">(</span><span class="nv">$ownedCompany</span> <span class="o">??</span> <span class="nv">$user</span><span class="o">-></span><span class="n">companies</span><span class="o">-></span><span class="nf">first</span><span class="p">());</span>
<span class="k">return</span> <span class="nv">$next</span><span class="p">(</span><span class="nv">$request</span><span class="p">);</span>}
<p>Eğer bir kullanıcı bir şirketten kaldırılırsa, middleware bir sonraki istekte otomatik olarak geçiş yapar. Geçersiz bir durum yok.</p>
<h3>
<a name="checkaccessmiddleware" href="#checkaccessmiddleware"></a>
CheckAccessMiddleware
</h3>
<p>Ban durumu (kullanıcı + sahip) ve ödeme durumunu (deneme + abonelik) kontrol eder. Yasaklı kullanıcılar bile destek biletlerine erişebilir.</p>
<h3>
<a name="checkcompanyaccess" href="#checkcompanyaccess"></a>
CheckCompanyAccess
</h3>
<p>Sahip olanlara özel yollar: şirket ayarları, çalışan yönetimi, rol yapılandırması.</p>
<p><strong>Middleware sırası bootstrap/app.php dosyasında:</strong><br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>1. HandleInertiaRequestsSetActiveCompanyMiddleware
Sıra önemlidir. Hangi şirket olduğunu bilmeden şirket izinlerini kontrol edemezsiniz.
7. İzin Farkındalığı Olan NavigasyonHer menü öğesinin bir ‘policyName’ değeri vardır:
[ => __(), => , => , ], [ => __(), => , => , ],Her öğe ‘checkUserAndCompanyPolicy()’ üzerinden geçer:
private function checkUserAndCompanyPolicy($policyName) { if ($this->isAdmin()) return true; if (empty($policyName)) return false; if ($policyName === ) return auth()->check();$user = auth()->user(); $company = $user->getActiveCompany(); if (!$company) return false;
if ($user->isOwnerOf($company)) return true;
return $user->hasPermissionTo(<span class="nv>$policyName, <span class="nv>$company); }
Bir “Storekeeper” depoyu görür. Bir “Manager” siparişleri, üretimi, karşı tarafları görür. Bir sahibi her şeyi görür. Eğer erişemezseniz – onu görmezsiniz.
8. İlk İzin Verilen Rota (FAR) DeseniBütün 403 sayfalarını SaaS’tan kaldıran desen.
Bir 403 hata sayfası göstermek yerine, kullanıcıyı ulaşabileceği ilk sayfaya yönlendirin:
// bootstrap/app.php - istisna işleyici $exceptions->renderable(function (AccessDeniedHttpException $e, Request $request) { $layoutService = app(LayoutDataService::class);return redirect() ->route($layoutService->getFirstAllowedRouteName()) ->with(<span class="s1>'message-disappear-error', __(<span class="s1>'Bu sayfaya erişim izniniz yok')); });
‘getFirstAllowedRouteName()’ metodu:
- Kullanıcının bu şirket için kaydedilmiş varsayılan sayfasını kontrol eder
- O sayfa hala erişilebilir ise – oraya gider
- Eğer değilse – temizle, uyarı göster, yedekle
- Menü öğelerini dolaş, ilk erişilebilir olanı bul
- Yedekleme bildirimlerine geç
public function getFirstAllowedRouteName(): string if (self::isAdmin()) return ;if (!$this->authUserData->hasUserCompanyOrRole()) { return <span class="s1>'notifications.index'; }
// Kaydedilmiş varsayılan rotayı kontrol et $saved = auth()->user()->getDefaultRouteForActiveCompany(); if (<span $saved && $this->isBottomLevelRouteAccessible($saved)) { return $saved; }
// Menü öğelerini dolaş foreach ($this->getNavUpLevelItemsArray() as $navItem) { if (!empty($navItem[<span class="s1>'unvisibly'])) continue; if (<span $this->checkUserAndCompanyPolicy(<span $navItem[<span class="s1>'policyName'])) { return $this->getFirstAllowedBottomLevelRoute(<span $navItem[<span class="s1>'routeName']) ?? <span $navItem[<span class="s1>'routeName']; } }
return <span class="s1>'notifications.index'; }
4 yerinde kullanılır: giriş sonrası, 403 sonrası, şirket geçişinde, yan menü “Ana” linkinde.
9. Test – Çalıştığını KanıtlaÇoklu kiracılık hataları sessiz veri sızıntılarıdır. Otomatik testler zorunludur.
İzin hiyerarşisi testi:it(, function () { $employee->assignRole(, );<span class="c1>// Yönetici rolü 'orders.view' iznine sahiptir $employee->revokePermissionFrom(<span class="nv>$ordersView, <span class="nv>$company); <span class="c1>// Yönetici 'warehouse.delete' iznine sahip değildir $employee->givePermissionTo(<span class="nv>$warehouseDelete, <span class="nv>$company);
expect($employee->hasPermissionTo(<span class="s1>'orders.view', <span class="nv>$company))->toBeFalse() ->and(<span class="nv>$employee->hasPermissionTo(<span class="s1>'warehouse.delete', <span class="nv>$company))->toBeTrue(); });
Şirketler arası izolasyon:it(, function () { $roleFromCompany2 = Role::create([ => ->id, ]);$this->actingAs$ownerOfCompany1); expect(Gate::denies(<span class="s1>'roles.update', <span class="nv>$roleFromCompany2))->toBeTrue(); });
Menü görünürlüğü:it(, function () { $employee->givePermissionTo(, );$menu = $service->getNav'upLevel'); $labels = collect<span class="p>($menu)->pluck'linkName')->toArray<span class="p>();
expect($labels) ->toContain'Siparişler') ->not->toContain'Üretim') ->not->toContain'Depo') ->not->toContain'Şirketim'); });
Sahip İzinli Yollar:it(, function () { $response = $this->withCookie$cookieName, ) ->get(route());$response->assertRedirect(); $response->assertSessionHas(<span class="s1>'message-disappear-error'); });
Toplam test kapsamı: izin hiyerarşisi, şirketler arası izolasyon, Gate yetkilendirmesi, menü görünürlüğü
Kaynak: Orijinal Makale


