From ea87502ba4c044c9a35cf01a2aa416f1fc1b1d72 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 26 May 2025 15:16:55 +0200 Subject: [PATCH] fix: :bug: update api routes for multi-tenancy --- app/Http/Controllers/API/AppController.php | 10 +- app/Http/Controllers/API/SyncController.php | 148 +++++++++--------- .../Api/Contacts/DestroyContactsRequest.php | 12 -- .../Api/Contacts/UpsertContactsRequest.php | 27 ++-- app/Http/Requests/Api/SendFlyerRequest.php | 28 ++-- app/Providers/TranslationServiceProvider.php | 2 +- config/tenancy.php | 4 +- database/factories/ContactFactory.php | 6 +- .../views/bo/pages/contacts/show.blade.php | 2 +- routes/api.php | 28 ++-- routes/breadcrumbs-bo.php | 8 +- 11 files changed, 138 insertions(+), 137 deletions(-) diff --git a/app/Http/Controllers/API/AppController.php b/app/Http/Controllers/API/AppController.php index e58e651..f393f50 100644 --- a/app/Http/Controllers/API/AppController.php +++ b/app/Http/Controllers/API/AppController.php @@ -16,8 +16,16 @@ class AppController extends Controller */ public function get(): \App\Http\Resources\ApiResponseResource { + /** @var string $tenantKey */ + $tenantKey = request()->tenant; + $response = []; - $settings = Setting::where('key', 'Button1')->orWhere('key', 'Button2')->orWhere('key', 'Button3')->get(); + $settings = Setting::query() + ->where('tenant_id', $tenantKey) + ->where('key', 'Button1') + ->orWhere('key', 'Button2') + ->orWhere('key', 'Button3') + ->get(); foreach ($settings as $setting) { $directory = Directory::find($setting->value)->only('id', 'name'); diff --git a/app/Http/Controllers/API/SyncController.php b/app/Http/Controllers/API/SyncController.php index e98f066..f362391 100644 --- a/app/Http/Controllers/API/SyncController.php +++ b/app/Http/Controllers/API/SyncController.php @@ -21,7 +21,6 @@ use App\Models\Directory; use App\Models\Document; use App\Models\Formulary; use App\Models\Setting; -use Google\Cloud\Vision\V1\ImageAnnotatorClient; use GuzzleHttp\Exception\ClientException; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; @@ -38,8 +37,11 @@ class SyncController extends Controller */ public function getUsers(): \App\Http\Resources\ApiResponseResource { - $users = AppUserResource::collection(AppUser::where('status', 1)->get())->toArray(request()); - + /** @var string $tenantKey */ + $tenantKey = request()->tenant; + $users = AppUserResource::collection( + AppUser::query()->where('tenant_id', $tenantKey)->where('status', 1)->get() + )->toArray(request()); return new ApiResponseResource("OK", $users); } @@ -53,9 +55,11 @@ class SyncController extends Controller */ public function getContacts(AppUser $app_user): \Illuminate\Http\Resources\Json\AnonymousResourceCollection { + /** @var string $tenantKey */ + $tenantKey = request()->tenant; return ContactResource::collection( - Contact::query()->whereHas('appUser', function (Builder $q) use ($app_user) { - $q->where('id', $app_user->id); + Contact::query()->whereHas('appUser', function (Builder $q) use ($tenantKey, $app_user) { + $q->where('tenant_id', $tenantKey)->where('id', $app_user->id); })->get() ); } @@ -71,40 +75,46 @@ class SyncController extends Controller UpsertContactsRequest $request, AppUser $app_user ): \Illuminate\Http\JsonResponse { + /** @var string $tenantKey */ + $tenantKey = $request->tenant; + /** @var \Illuminate\Support\Collection */ $formularies = Formulary::query()->pluck('slug')->flip(); - $contactQuery = Contact::query()->whereHas('appUser', function (Builder $q) use ($app_user) { - $q->where('id', $app_user->id); + $contactQuery = Contact::query()->whereHas('appUser', function (Builder $q) use ($tenantKey, $app_user) { + $q->where('tenant_id', $tenantKey)->where('id', $app_user->getKey()); }); - \collect($request->contacts)->each(function (array $contactRequestData) use ($contactQuery, $formularies) { - /** @var \App\Models\Contact */ - $model = (clone $contactQuery)->where('id', Arr::get($contactRequestData, 'id'))->firstOrNew(); + \collect($request->contacts)->each( + function (array $contactRequestData) use ($contactQuery, $formularies, $app_user) { + /** @var \App\Models\Contact $model */ + $model = (clone $contactQuery)->where('id', Arr::get($contactRequestData, 'id'))->firstOrNew(); - $contactRequestData['timestamp'] = Carbon::parse($contactRequestData['timestamp'], 'UTC') - ->setTimezone(\now()->getTimezone()); - // @phpstan-ignore logicalAnd.leftAlwaysTrue - if ($model->timestamp and $model->timestamp->gte($contactRequestData['timestamp'])) { - return; + $contactRequestData['timestamp'] = Carbon::parse($contactRequestData['timestamp'], 'UTC') + ->setTimezone(\now()->getTimezone()); + // @phpstan-ignore logicalAnd.leftAlwaysTrue + if ($model->timestamp and $model->timestamp->gte($contactRequestData['timestamp'])) { + return; + } + // * Set parse values on data prop to fill in model. + $contactRequestData['data'] = \collect($contactRequestData['dataParsed']) + // * FILTER CONTACT DATA WITH ACTUAL FORMULARY + ->filter(function (Input $input) use ($formularies) { + return $formularies->has($input->data->formSlug); + }); + $model->fill($contactRequestData); + $model->app_user_id = $app_user->getKey(); + + // * Send contact to Feature + if ( + !empty($contactRequestData['send_to_app_user_id']) and + ($destUser = AppUser::query()->where('id', $contactRequestData['send_to_app_user_id'])->first()) + ) { + $model->appUser()->associate($destUser); + } + + $model->saveOrFail(); } - // * Set parse values on data prop to fill in model. - $contactRequestData['data'] = \collect($contactRequestData['dataParsed']) - // * FILTER CONTACT DATA WITH ACTUAL FORMULARY - ->filter(function (Input $input) use ($formularies) { - return $formularies->has($input->data->formSlug); - }); - $model->fill($contactRequestData); - - // * Send contact to Feature - if ( - !empty($contactRequestData['send_to_app_user_id']) and - ($destUser = AppUser::query()->where('id', $contactRequestData['send_to_app_user_id'])->first()) - ) { - $model->appUser()->associate($destUser); - } - - $model->saveOrFail(); - }); + ); // * HTTP 200 is ok. return \response()->json(); @@ -119,9 +129,11 @@ class SyncController extends Controller */ public function destroyContacts(DestroyContactsRequest $request, AppUser $app_user): \Illuminate\Http\JsonResponse { + /** @var string $tenantKey */ + $tenantKey = $request->tenant; Contact::query()->whereIn('id', $request->contactUuidList) - ->whereHas('appUser', function (Builder $q) use ($app_user) { - $q->where('id', $app_user->id); + ->whereHas('appUser', function (Builder $q) use ($tenantKey, $app_user) { + $q->where('tenant_id', $tenantKey)->where('id', $app_user->id); })->delete(); // * HTTP 200 is ok. @@ -135,7 +147,11 @@ class SyncController extends Controller */ public function getDirectories(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection { - return DirectoryResource::collection(Directory::query()->orderBy('order')->get()); + /** @var string $tenantKey */ + $tenantKey = request()->tenant; + return DirectoryResource::collection( + Directory::query()->where('tenant_id', $tenantKey)->orderBy('order')->get() + ); } /** @@ -143,7 +159,11 @@ class SyncController extends Controller */ public function getDocuments(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection { - return DocumentResource::collection(Document::where('status', 1)->get()); + /** @var string $tenantKey */ + $tenantKey = request()->tenant; + return DocumentResource::collection( + Document::query()->where('tenant_id', $tenantKey)->where('status', 1)->get() + ); } /** @@ -152,10 +172,12 @@ class SyncController extends Controller */ public function getVersion(Request $request): array { + /** @var string $tenantKey */ + $tenantKey = $request->tenant; $app_uuid = $request->app_uuid; $app_version = $request->app_version; - $version = Setting::query()->where('key', 'version')->value('value'); - $ShowIsActive = Setting::query()->where('key', 'ShowIsActive')->value('value'); + $version = Setting::query()->where('tenant_id', $tenantKey)->where('key', 'version')->value('value'); + $ShowIsActive = Setting::query()->where('tenant_id', $tenantKey)->where('key', 'ShowIsActive')->value('value'); try { $client = new \GuzzleHttp\Client(); @@ -183,7 +205,11 @@ class SyncController extends Controller */ public function getFormularies(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection { - return FormularyResource::collection(Formulary::query()->orderBy('order')->get()); + /** @var string $tenantKey */ + $tenantKey = request()->tenant; + return FormularyResource::collection( + Formulary::query()->where('tenant_id', $tenantKey)->orderBy('order')->get() + ); } /** @@ -192,12 +218,14 @@ class SyncController extends Controller */ public function sendFlyer(SendFlyerRequest $request): string { - $requestData = \json_decode($request->emails, true); + /** @var string $tenantKey */ + $tenantKey = $request->tenant; + $requestData = $request->validated()['emails']; $email = $requestData['email']; $subject = $requestData['subject']; $message = $requestData['message']; $app_user_id = $requestData['app_user_id']; - $docs = \json_decode($requestData['documents'], true); + $docs = $requestData['documents']; $user = AppUser::find($app_user_id); @@ -206,12 +234,12 @@ class SyncController extends Controller $noExists = []; foreach ($docs as $doc) { $exists = Storage::disk('public') - ->exists(sprintf('docs/%s/%s.%s', \tenant()->getKey(), $doc['uuid'], $doc['type'])); + ->exists(sprintf('docs/%s/%s.%s', $tenantKey, $doc['uuid'], $doc['type'])); if ($exists) { array_push($attach, URL::temporarySignedRoute( - 'docs.download', + 'bo.docs.download', now()->addDays(30), - ['uuid' => $doc['uuid']] + ['tenant' => $tenantKey, 'uuid' => $doc['uuid']] )); array_push($filesName, $doc['title']); } else { @@ -228,9 +256,7 @@ class SyncController extends Controller 'filesName' => $filesName, ]); - Mail::to($email) - ->cc($user->email) - ->queue($message); + Mail::to($email)->cc($user->email)->queue($message); } else { $message = new SendFlyerFailed([ 'email' => $email, @@ -244,30 +270,4 @@ class SyncController extends Controller } //end if return 'OK'; } - - /** - * @return void - */ - public function processOcr(): void - { - // Instantiates a client. - $imageAnnotator = new ImageAnnotatorClient([ - 'credentials' => public_path() . '/credentials.json' - ]); - - // The name of the image file to annotate. - $fileName = 'test.jpg'; - - // Prepare the image to be annotated. - $image = file_get_contents(public_path() . "/" . $fileName); - - // Performs label detection on the image file. - $response = $imageAnnotator->textDetection($image); - $labels = $response->getTextAnnotations(); - - // @phpstan-ignore foreach.nonIterable - foreach ($labels as $label) { - echo $label->getDescription() . "
"; - } - } } diff --git a/app/Http/Requests/Api/Contacts/DestroyContactsRequest.php b/app/Http/Requests/Api/Contacts/DestroyContactsRequest.php index 6248983..d5a80bd 100644 --- a/app/Http/Requests/Api/Contacts/DestroyContactsRequest.php +++ b/app/Http/Requests/Api/Contacts/DestroyContactsRequest.php @@ -6,18 +6,6 @@ use Illuminate\Foundation\Http\FormRequest; class DestroyContactsRequest extends FormRequest { - /** - * Prepare the data for validation. - * - * @return void - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'contactUuidList' => \collect($this->contactUuidList)->filter()->all() - ]); - } - /** * Get the validation rules that apply to the request. * diff --git a/app/Http/Requests/Api/Contacts/UpsertContactsRequest.php b/app/Http/Requests/Api/Contacts/UpsertContactsRequest.php index ba3ca93..b6d21b9 100644 --- a/app/Http/Requests/Api/Contacts/UpsertContactsRequest.php +++ b/app/Http/Requests/Api/Contacts/UpsertContactsRequest.php @@ -5,22 +5,10 @@ namespace App\Http\Requests\Api\Contacts; use App\Enums\Formularies\InputTypeEnum; use App\Lib\Inputs\Definition\InputData; use Illuminate\Foundation\Http\FormRequest; -use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\ValidationException; class UpsertContactsRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - * - * @return boolean - */ - public function authorize(): bool - { - // * Check is done with api.token middleware - return true; - } - /** * Prepare the data for validation. * @@ -67,11 +55,11 @@ class UpsertContactsRequest extends FormRequest 'contacts' => 'present|array', 'contacts.*' => 'array', 'contacts.*.id' => 'uuid', - 'contacts.*.lastname' => 'nullable|string|max:255', - 'contacts.*.firstname' => 'nullable|string|max:255', + 'contacts.*.lastname' => 'nullable|string|between:3,255', + 'contacts.*.firstname' => 'nullable|string|between:3,255', // * Dot not validate email here (soft validate), if it does not pass, app will be blocked. - 'contacts.*.email' => 'required|string|max:255', - 'contacts.*.phone' => 'nullable|string|max:255', + 'contacts.*.email' => 'required|string|between:3,255', + 'contacts.*.phone' => 'nullable|string|between:3,255', 'contacts.*.timestamp' => 'required|date', // * DATA @@ -80,7 +68,10 @@ class UpsertContactsRequest extends FormRequest 'contacts.*.data.*' => 'array', 'contacts.*.data.*.title' => 'present|string|min:1|max:255', 'contacts.*.data.*.formSlug' => 'present|string|min:1|max:255', - 'contacts.*.data.*.type' => ['present', new Enum(InputTypeEnum::class)], + 'contacts.*.data.*.type' => 'present|in:' . implode( + ',', + array_column(InputTypeEnum::cases(), 'name') + ), 'contacts.*.data.*.value' => 'present', 'contacts.*.data.*.required' => 'present|boolean', 'contacts.*.data.*.send_to_app_user_id' => 'nullable|numeric', @@ -117,7 +108,7 @@ class UpsertContactsRequest extends FormRequest 'error' => "wrong formSlug on data index '{$index}'" ]); } - $type = InputTypeEnum::make($data->get('type') ? $data->get('type') : null); + $type = InputTypeEnum::make($data->get('type') ? $data->get('type') : null, 'name'); $value = $data['value'] ?? null; $required = $data->get('required') ? true : false; $inputClass = $type->inputTypes(); diff --git a/app/Http/Requests/Api/SendFlyerRequest.php b/app/Http/Requests/Api/SendFlyerRequest.php index cf432c7..1e17640 100644 --- a/app/Http/Requests/Api/SendFlyerRequest.php +++ b/app/Http/Requests/Api/SendFlyerRequest.php @@ -15,18 +15,26 @@ class SendFlyerRequest extends FormRequest public function rules(): array { return [ - 'email' => [ + 'emails.email' => [ 'required', 'email:rfc,strict,dns,spoof,filter', - Rule::unique('users', 'email')->where('tenant_id', tenant('id')) + Rule::exists('contacts', 'email')->where(function ($query) { + $query->whereIn('app_user_id', function ($subQuery) { + $subQuery->select('id')->from('app_users')->where('tenant_id', $this->tenant); + }); + }), ], - 'subject' => 'required|string|between:3,255', - 'message' => 'required|string|min:3', - 'app_user_id' => [ + 'emails.subject' => 'required|string|between:3,255', + 'emails.message' => 'required|string|min:3', + 'emails.app_user_id' => [ 'required', 'integer', - Rule::exists('app_users', 'id')->where('tenant_id', tenant('id')) + Rule::exists('app_users', 'id')->where('tenant_id', $this->tenant) ], + 'emails.documents' => 'required|array|min:1', + 'emails.documents.*.title' => 'required|string|min:3', + 'emails.documents.*.uuid' => 'required|string|min:3', + 'emails.documents.*.type' => 'required|string|min:1', ]; } @@ -38,10 +46,10 @@ class SendFlyerRequest extends FormRequest public function attributes(): array { return [ - 'email' => trans('validation.attributes.email'), - 'subject' => trans('validation.attributes.subject'), - 'message' => trans('validation.attributes.message'), - 'app_user_id' => trans('models.classes.app_user'), + 'emails.email' => trans('validation.attributes.email'), + 'emails.subject' => trans('validation.attributes.subject'), + 'emails.message' => trans('validation.attributes.message'), + 'emails.app_user_id' => trans('models.classes.app_user'), ]; } } diff --git a/app/Providers/TranslationServiceProvider.php b/app/Providers/TranslationServiceProvider.php index d843133..8059631 100644 --- a/app/Providers/TranslationServiceProvider.php +++ b/app/Providers/TranslationServiceProvider.php @@ -20,7 +20,7 @@ class TranslationServiceProvider extends ServiceProvider // * Suported locales Cache::rememberForever(sprintf('translations.%s', $locale), function () use ($locale) { $translations = [ - 'php' => $this->phpTranslations($locale), + 'php' => $this->phpTranslations($locale), 'json' => $this->jsonTranslations($locale), ]; return $translations; diff --git a/config/tenancy.php b/config/tenancy.php index e3cc1c8..d6d5019 100644 --- a/config/tenancy.php +++ b/config/tenancy.php @@ -17,9 +17,7 @@ return [ * * Only relevant if you're using the domain or subdomain identification middleware. */ - 'central_domains' => [ - 'showroom-bo.s-web.test' - ], + 'central_domains' => [], /** * Tenancy bootstrappers are executed when tenancy is initialized. diff --git a/database/factories/ContactFactory.php b/database/factories/ContactFactory.php index b75cb3c..70510be 100644 --- a/database/factories/ContactFactory.php +++ b/database/factories/ContactFactory.php @@ -29,8 +29,10 @@ class ContactFactory extends Factory 'notes' => $this->faker->boolean() ? $this->faker->text() : null, 'visits' => \collect(range(0, $this->faker->numberBetween(1, 5)))->mapWithKeys(function () { return [ - 'title' => 'Visite du ' . $this->faker->dateTime()->format('D/M/Y H:i'), - 'note' => $this->faker->text() + [ + 'title' => 'Visite du ' . $this->faker->dateTime()->format('D/M/Y H:i'), + 'note' => $this->faker->text(), + ] ]; }), 'data' => null, diff --git a/resources/views/bo/pages/contacts/show.blade.php b/resources/views/bo/pages/contacts/show.blade.php index bdf3778..c77504e 100644 --- a/resources/views/bo/pages/contacts/show.blade.php +++ b/resources/views/bo/pages/contacts/show.blade.php @@ -1,4 +1,4 @@ -@extends('bo.layout') +@extends('bo.layout', ['brParam' => $contactModel]) @section('title', sprintf('%s - %s', str(__('models.classes.contact'))->plural(), Str::ucfirst(__('liste')))) @section('description', __('Page de contôle sur tous les contacts de l\'application.')) diff --git a/routes/api.php b/routes/api.php index 7d0d355..67fd7df 100644 --- a/routes/api.php +++ b/routes/api.php @@ -16,16 +16,18 @@ use Illuminate\Support\Facades\Route; | */ -Route::name('api.')->middleware('api.token')->group(function () { - Route::get('settings', [SettingsController::class, 'get'])->name('settings.index'); - Route::get('app', [AppController::class, 'get'])->name('settings.app'); - Route::get('formularies', [SyncController::class, 'getFormularies'])->name('formularies.index'); - Route::get('users', [SyncController::class, 'getUsers'])->name('users.index'); - Route::get('directories', [SyncController::class, 'getDirectories'])->name('directories.index'); - Route::get('documents', [SyncController::class, 'getDocuments'])->name('documents.index'); - Route::get('version', [SyncController::class, 'getVersion'])->name('version'); - Route::get('contacts/{app_user}', [SyncController::class, 'getContacts'])->name('contacts.index'); - Route::post('contacts/{app_user}', [SyncController::class, 'upsertContact'])->name('contacts.upsert'); - Route::delete('contacts/{app_user}', [SyncController::class, 'destroyContacts'])->name('contacts.delete'); - Route::post('sendBrochure', [SyncController::class, 'SendFlyer'])->name('send-flyers'); -}); +Route::middleware('api.token') + ->name('api.') + ->group(function () { + Route::get('settings', [SettingsController::class, 'get'])->name('settings.index'); + Route::get('app', [AppController::class, 'get'])->name('settings.app'); + Route::get('formularies', [SyncController::class, 'getFormularies'])->name('formularies.index'); + Route::get('users', [SyncController::class, 'getUsers'])->name('users.index'); + Route::get('directories', [SyncController::class, 'getDirectories'])->name('directories.index'); + Route::get('documents', [SyncController::class, 'getDocuments'])->name('documents.index'); + Route::get('version', [SyncController::class, 'getVersion'])->name('version'); + Route::get('contacts/{app_user}', [SyncController::class, 'getContacts'])->name('contacts.index'); + Route::post('contacts/{app_user}', [SyncController::class, 'upsertContact'])->name('contacts.upsert'); + Route::delete('contacts/{app_user}', [SyncController::class, 'destroyContacts'])->name('contacts.delete'); + Route::post('sendBrochure', [SyncController::class, 'SendFlyer'])->name('send-flyers'); + }); diff --git a/routes/breadcrumbs-bo.php b/routes/breadcrumbs-bo.php index 51d72b7..7bf660c 100644 --- a/routes/breadcrumbs-bo.php +++ b/routes/breadcrumbs-bo.php @@ -1,6 +1,7 @@ \tenant()]) ); }); -Breadcrumbs::for('bo.contacts.show', function (Generator $trail) { +Breadcrumbs::for('bo.contacts.show', function (Generator $trail, Contact $contact) { $trail->parent('bo.contacts.index'); - $trail->push(Str::ucfirst(trans('crud.actions.show')), route('bo.contacts.show', ['tenant' => \tenant()])); + $trail->push(Str::ucfirst(trans('crud.actions.show')), route('bo.contacts.show', [ + 'tenant' => \tenant(), + 'contact' => $contact + ])); }); // * FORMULARIES