Add BootstrapTenantController and requests for tenant domain normalization; implement API route and tests

This commit is contained in:
2026-06-23 09:49:54 -03:00
parent 6d83d3dd7d
commit 3c6469617b
10 changed files with 399 additions and 34 deletions

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Domains\Tenant\Controllers;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Tenant\Requests\BootstrapTenantRequest;
use App\Domains\Tenant\Resources\TenantResource;
use App\Http\Controllers\Controller;
class BootstrapTenantController extends Controller
{
public function __invoke(BootstrapTenantRequest $request): TenantResource
{
/** @var string $dominio */
$dominio = $request->validated('dominio');
return TenantResource::make(
Tenant::query()->where('dominio', $dominio)->firstOrFail()
);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Domains\Tenant\Requests;
use App\Domains\Tenant\Support\TenantDomainNormalizer;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
class BootstrapTenantRequest extends FormRequest
{
protected bool $hasInvalidDomain = false;
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$rawDomain = $this->route('dominio');
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain);
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
&& $normalizedDomain === null;
$this->merge([
'dominio' => $normalizedDomain,
]);
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'dominio' => [
'bail',
function (string $attribute, mixed $value, Closure $fail): void {
if ($this->hasInvalidDomain) {
$fail("The {$attribute} field must contain a valid domain or URL.");
}
},
'required',
'string',
'max:255',
],
];
}
}

View File

@@ -3,16 +3,33 @@
namespace App\Domains\Tenant\Requests;
use App\Domains\Prop\Support\PropRules;
use App\Domains\Tenant\Support\TenantDomainNormalizer;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreTenantRequest extends FormRequest
{
protected bool $hasInvalidDomain = false;
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$rawDomain = $this->input('dominio');
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain);
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
&& $normalizedDomain === null;
$this->merge([
'dominio' => $normalizedDomain,
]);
}
/**
* @return array<string, mixed>
*/
@@ -21,7 +38,18 @@ class StoreTenantRequest extends FormRequest
return [
'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')],
'nombre' => ['required', 'string', 'max:255'],
'dominio' => ['nullable', 'string', 'max:255'],
'dominio' => [
'bail',
function (string $attribute, mixed $value, Closure $fail): void {
if ($this->hasInvalidDomain) {
$fail("The {$attribute} field must contain a valid domain or URL.");
}
},
'nullable',
'string',
'max:255',
Rule::unique('tenants', 'dominio'),
],
...PropRules::sync(),
];
}

View File

@@ -4,16 +4,33 @@ namespace App\Domains\Tenant\Requests;
use App\Domains\Prop\Support\PropRules;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Tenant\Support\TenantDomainNormalizer;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateTenantRequest extends FormRequest
{
protected bool $hasInvalidDomain = false;
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$rawDomain = $this->input('dominio');
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain);
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
&& $normalizedDomain === null;
$this->merge([
'dominio' => $normalizedDomain,
]);
}
/**
* @return array<string, mixed>
*/
@@ -30,7 +47,18 @@ class UpdateTenantRequest extends FormRequest
Rule::unique('tenants', 'codigo')->ignore($tenant?->id),
],
'nombre' => ['required', 'string', 'max:255'],
'dominio' => ['nullable', 'string', 'max:255'],
'dominio' => [
'bail',
function (string $attribute, mixed $value, Closure $fail): void {
if ($this->hasInvalidDomain) {
$fail("The {$attribute} field must contain a valid domain or URL.");
}
},
'nullable',
'string',
'max:255',
Rule::unique('tenants', 'dominio')->ignore($tenant?->id),
],
...PropRules::sync(),
];
}

View File

@@ -21,9 +21,7 @@ class TenantResource extends JsonResource
'codigo' => $this->codigo,
'nombre' => $this->nombre,
'dominio' => $this->dominio,
'props' => PropValueResource::collapsedFromPropable($this->resource),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'props' => PropValueResource::collapsedFromPropable($this->resource)
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Domains\Tenant\Support;
class TenantDomainNormalizer
{
public static function hasValue(mixed $domain): bool
{
return is_string($domain) && trim(urldecode($domain)) !== '';
}
public static function normalize(mixed $domain): ?string
{
if (! is_string($domain)) {
return null;
}
$decodedDomain = trim(urldecode($domain));
if ($decodedDomain === '') {
return null;
}
$candidate = str_contains($decodedDomain, '://')
? $decodedDomain
: "//{$decodedDomain}";
$host = parse_url($candidate, PHP_URL_HOST);
if (! is_string($host) || $host === '') {
return null;
}
return strtolower($host);
}
}

View File

@@ -1,9 +1,13 @@
<?php
use App\Domains\Tenant\Controllers\BootstrapTenantController;
use App\Domains\Tenant\Controllers\TenantController;
use App\Domains\Tenant\Controllers\TenantPropController;
use Illuminate\Support\Facades\Route;
Route::get('tenants/bootstrap/{dominio}', BootstrapTenantController::class)
->where('dominio', '.*');
Route::apiResource('tenants', TenantController::class);
Route::apiResource('tenant-props', TenantPropController::class)
->parameters(['tenant-props' => 'tenantProp']);