Files
autocrm/app/Http/Middleware/ResolveTenant.php
T
Vasyka 4b1635d045 Faza 2: multi-tenancy + Filament dual panels + seed PSauto
Schema centrală:
- companies (slug unique, status, plan_id, settings JSON, trial/active dates)
- super_admins (operator platform)
- plans (free/basic/pro)

Schema tenant (toate cu company_id NOT NULL):
- users (UNIQUE company_id+email)
- clients
- vehicles

Tenancy core:
- App\Tenancy\TenantManager singleton
- App\Models\Concerns\BelongsToTenant trait + TenantScope
- ResolveTenant middleware (slug → Company, 404 pentru rezervate/missing)
- CheckTenantStatus middleware (suspended/expired/archived)
- Fail-safe: TenantScope returns 0 rows când tenant nu e rezolvat

Auth guards:
- 'central' guard cu super_admins provider (panou platform)
- 'web' guard cu users provider (per-tenant)

Filament panels:
- CentralPanelProvider la service.mir.md/admin
- TenantPanelProvider la <slug>.service.mir.md/app
- CompanyResource (central): CRUD companii cu status badge + filtre
- ClientResource (tenant): CRUD clienți cu status, sursă, sold
- VehicleResource (tenant): CRUD mașini cu marcă/model/VIN

Seed:
- 3 plans (free/basic/pro)
- super-admin: vasyka.moraru@gmail.com / admin123
- demo company 'psauto' cu admin user admin@psauto.md / admin123
- 3 clienți + 3 mașini preluate din AutoCRM.html

Bootstrap:
- TrustProxies (Cloudflare→Traefik HTTPS detection)
- forceScheme/forceRootUrl când APP_URL e HTTPS
- Helper global tenant() în app/helpers.php (autoload via composer)
- RUN_SEED env var în entrypoint pentru db:seed condiționat
2026-05-05 21:29:52 +00:00

70 lines
2.4 KiB
PHP

<?php
namespace App\Http\Middleware;
use App\Models\Central\Company;
use App\Tenancy\TenantManager;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Reads the request Host, extracts the tenant slug (subdomain of CENTRAL_DOMAIN),
* loads the matching Company and stores it in the TenantManager singleton.
*
* Examples (CENTRAL_DOMAIN=service.mir.md):
* psauto.service.mir.md → slug="psauto"
* service.mir.md → no tenant (central context)
* www.service.mir.md → reserved → 404
*/
class ResolveTenant
{
/** Subdomains that are NEVER treated as tenant slugs. */
public const RESERVED = [
'www', 'api', 'app', 'admin', 'mail', 'mailpit',
'git', 'gitea', 'coolify', 's3', 's3-admin',
'ws', 'reverb', 'pulse', 'horizon',
];
public function handle(Request $request, Closure $next, ?string $required = null)
{
$host = $request->getHost();
$central = config('tenancy.central_domains', []);
$centralPrimary = config('app.central_domain') ?: $central[0] ?? 'service.mir.md';
// No subdomain → central context.
if (in_array($host, $central, true)) {
// Tenant panel must never run on the central host.
if (str_starts_with($request->path(), 'app')) {
throw new NotFoundHttpException('Tenant routes are not available on the central domain.');
}
return $next($request);
}
$slug = null;
if (str_ends_with($host, ".{$centralPrimary}")) {
$slug = substr($host, 0, -strlen(".{$centralPrimary}"));
}
if (! $slug || in_array($slug, self::RESERVED, true) || str_contains($slug, '.')) {
// Reserved or multi-level subdomain → not a tenant. 404.
throw new NotFoundHttpException("Unknown subdomain: {$host}");
}
$company = Company::where('slug', $slug)->first();
if (! $company) {
throw new NotFoundHttpException("Tenant '{$slug}' not found.");
}
app(TenantManager::class)->setCurrent($company);
$request->attributes->set('tenant', $company);
if ($required === 'required' && ! app(TenantManager::class)->isResolved()) {
throw new NotFoundHttpException();
}
return $next($request);
}
}