feat: implement tenant management system including database schema, CRUD services, and bootstrap functionality
This commit is contained in:
@@ -6,12 +6,17 @@ use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Requests\StoreTenantRequest;
|
||||
use App\Domains\Tenant\Requests\UpdateTenantRequest;
|
||||
use App\Domains\Tenant\Resources\TenantResource;
|
||||
use App\Domains\Tenant\Services\TenantService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class TenantController extends Controller
|
||||
{
|
||||
public function __construct(protected TenantService $tenantService)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return TenantResource::collection(Tenant::query()->latest()->get())->response();
|
||||
@@ -19,7 +24,7 @@ class TenantController extends Controller
|
||||
|
||||
public function store(StoreTenantRequest $request): JsonResponse
|
||||
{
|
||||
$tenant = Tenant::query()->create($request->validated());
|
||||
$tenant = $this->tenantService->create($request->validated());
|
||||
|
||||
return TenantResource::make($tenant)->response()->setStatusCode(201);
|
||||
}
|
||||
@@ -31,7 +36,7 @@ class TenantController extends Controller
|
||||
|
||||
public function update(UpdateTenantRequest $request, Tenant $tenant): TenantResource
|
||||
{
|
||||
$tenant->update($request->validated());
|
||||
$tenant = $this->tenantService->update($tenant, $request->validated());
|
||||
|
||||
return TenantResource::make($tenant);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
'codigo',
|
||||
'nombre',
|
||||
'dominio',
|
||||
'primary_color',
|
||||
'secondary_color',
|
||||
'danger_color',
|
||||
'header_footer_bg_color',
|
||||
'header_logo',
|
||||
'footer_logo',
|
||||
])]
|
||||
class Tenant extends Model
|
||||
{
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Domains\Tenant\Requests;
|
||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreTenantRequest extends FormRequest
|
||||
@@ -34,6 +36,52 @@ class StoreTenantRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$logoRule = [
|
||||
'nullable',
|
||||
static function (string $attribute, mixed $value, Closure $fail): void {
|
||||
if ($value instanceof UploadedFile) {
|
||||
$mime = $value->getMimeType();
|
||||
if (! str_starts_with($mime, 'image/')) {
|
||||
$fail("The {$attribute} must be a valid image or SVG.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if (Str::isUuid($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = trim($value);
|
||||
$declaredMimeType = null;
|
||||
if (preg_match('/^data:(?<mime>[-\w.+\/]+);base64,(?<data>.+)$/s', $payload, $matches) === 1) {
|
||||
$declaredMimeType = strtolower($matches['mime']);
|
||||
$payload = $matches['data'];
|
||||
}
|
||||
$decoded = base64_decode(preg_replace('/\s+/', '', $payload), true);
|
||||
if ($decoded === false || $decoded === '') {
|
||||
$fail("The {$attribute} must be a valid base64 image or SVG.");
|
||||
return;
|
||||
}
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->buffer($decoded);
|
||||
if (! $mime && $declaredMimeType) {
|
||||
$mime = $declaredMimeType;
|
||||
}
|
||||
if (! $mime || ! str_starts_with($mime, 'image/')) {
|
||||
$fail("The {$attribute} must be a valid image or SVG.");
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$fail("The {$attribute} must be a valid image or SVG.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$fail("The {$attribute} must be a valid file or base64 string.");
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')],
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
@@ -49,6 +97,12 @@ class StoreTenantRequest extends FormRequest
|
||||
'max:255',
|
||||
Rule::unique('tenants', 'dominio'),
|
||||
],
|
||||
'primary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'secondary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'danger_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'header_footer_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'header_logo' => $logoRule,
|
||||
'footer_logo' => $logoRule,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateTenantRequest extends FormRequest
|
||||
@@ -38,6 +40,52 @@ class UpdateTenantRequest extends FormRequest
|
||||
/** @var Tenant|null $tenant */
|
||||
$tenant = $this->route('tenant');
|
||||
|
||||
$logoRule = [
|
||||
'nullable',
|
||||
static function (string $attribute, mixed $value, Closure $fail): void {
|
||||
if ($value instanceof UploadedFile) {
|
||||
$mime = $value->getMimeType();
|
||||
if (! str_starts_with($mime, 'image/')) {
|
||||
$fail("The {$attribute} must be a valid image or SVG.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if (Str::isUuid($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = trim($value);
|
||||
$declaredMimeType = null;
|
||||
if (preg_match('/^data:(?<mime>[-\w.+\/]+);base64,(?<data>.+)$/s', $payload, $matches) === 1) {
|
||||
$declaredMimeType = strtolower($matches['mime']);
|
||||
$payload = $matches['data'];
|
||||
}
|
||||
$decoded = base64_decode(preg_replace('/\s+/', '', $payload), true);
|
||||
if ($decoded === false || $decoded === '') {
|
||||
$fail("The {$attribute} must be a valid base64 image or SVG.");
|
||||
return;
|
||||
}
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->buffer($decoded);
|
||||
if (! $mime && $declaredMimeType) {
|
||||
$mime = $declaredMimeType;
|
||||
}
|
||||
if (! $mime || ! str_starts_with($mime, 'image/')) {
|
||||
$fail("The {$attribute} must be a valid image or SVG.");
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$fail("The {$attribute} must be a valid image or SVG.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$fail("The {$attribute} must be a valid file or base64 string.");
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
'codigo' => [
|
||||
'required',
|
||||
@@ -58,6 +106,12 @@ class UpdateTenantRequest extends FormRequest
|
||||
'max:255',
|
||||
Rule::unique('tenants', 'dominio')->ignore($tenant?->id),
|
||||
],
|
||||
'primary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'secondary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'danger_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'header_footer_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'header_logo' => $logoRule,
|
||||
'footer_logo' => $logoRule,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ class TenantResource extends JsonResource
|
||||
'codigo' => $this->codigo,
|
||||
'nombre' => $this->nombre,
|
||||
'dominio' => $this->dominio,
|
||||
'primary_color' => $this->primary_color,
|
||||
'secondary_color' => $this->secondary_color,
|
||||
'danger_color' => $this->danger_color,
|
||||
'header_footer_bg_color' => $this->header_footer_bg_color,
|
||||
'header_logo' => $this->header_logo,
|
||||
'footer_logo' => $this->footer_logo,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
105
app/Domains/Tenant/Services/TenantService.php
Normal file
105
app/Domains/Tenant/Services/TenantService.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Tenant\Services;
|
||||
|
||||
use App\Domains\Attachable\Services\AttachmentService;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TenantService
|
||||
{
|
||||
public function __construct(protected AttachmentService $attachmentService)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tenant and store its logos.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return Tenant
|
||||
*/
|
||||
public function create(array $data): Tenant
|
||||
{
|
||||
return DB::transaction(function () use ($data): Tenant {
|
||||
$headerLogo = $data['header_logo'] ?? null;
|
||||
$footerLogo = $data['footer_logo'] ?? null;
|
||||
|
||||
unset($data['header_logo'], $data['footer_logo']);
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::query()->create($data);
|
||||
|
||||
if ($headerLogo) {
|
||||
$attachment = $this->attachmentService->store($headerLogo, 'tenants');
|
||||
$tenant->header_logo = $attachment->key;
|
||||
$tenant->attachments()->attach($attachment->id);
|
||||
}
|
||||
|
||||
if ($footerLogo) {
|
||||
$attachment = $this->attachmentService->store($footerLogo, 'tenants');
|
||||
$tenant->footer_logo = $attachment->key;
|
||||
$tenant->attachments()->attach($attachment->id);
|
||||
}
|
||||
|
||||
if ($headerLogo || $footerLogo) {
|
||||
$tenant->save();
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing tenant and store new logos if uploaded.
|
||||
*
|
||||
* @param Tenant $tenant
|
||||
* @param array<string, mixed> $data
|
||||
* @return Tenant
|
||||
*/
|
||||
public function update(Tenant $tenant, array $data): Tenant
|
||||
{
|
||||
return DB::transaction(function () use ($tenant, $data): Tenant {
|
||||
$hasHeaderLogoKey = array_key_exists('header_logo', $data);
|
||||
$hasFooterLogoKey = array_key_exists('footer_logo', $data);
|
||||
$headerLogo = $data['header_logo'] ?? null;
|
||||
$footerLogo = $data['footer_logo'] ?? null;
|
||||
|
||||
unset($data['header_logo'], $data['footer_logo']);
|
||||
|
||||
$tenant->fill($data);
|
||||
|
||||
if ($hasHeaderLogoKey) {
|
||||
if ($headerLogo) {
|
||||
if (! Str::isUuid($headerLogo)) {
|
||||
$attachment = $this->attachmentService->store($headerLogo, 'tenants');
|
||||
$tenant->header_logo = $attachment->key;
|
||||
$tenant->attachments()->attach($attachment->id);
|
||||
} else {
|
||||
$tenant->header_logo = $headerLogo;
|
||||
}
|
||||
} else {
|
||||
$tenant->header_logo = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasFooterLogoKey) {
|
||||
if ($footerLogo) {
|
||||
if (! Str::isUuid($footerLogo)) {
|
||||
$attachment = $this->attachmentService->store($footerLogo, 'tenants');
|
||||
$tenant->footer_logo = $attachment->key;
|
||||
$tenant->attachments()->attach($attachment->id);
|
||||
} else {
|
||||
$tenant->footer_logo = $footerLogo;
|
||||
}
|
||||
} else {
|
||||
$tenant->footer_logo = null;
|
||||
}
|
||||
}
|
||||
|
||||
$tenant->save();
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user