fix: 🐛 update api routes for multi-tenancy

This commit is contained in:
Alexis Gousseau 2025-05-26 15:16:55 +02:00
parent 2e8ca74962
commit ea87502ba4
11 changed files with 138 additions and 137 deletions

View file

@ -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');

View file

@ -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<string,integer> */
$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() . "<br>";
}
}
}

View file

@ -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.
*

View file

@ -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();

View file

@ -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'),
];
}
}

View file

@ -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;

View file

@ -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.

View file

@ -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,

View file

@ -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.'))

View file

@ -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');
});

View file

@ -1,6 +1,7 @@
<?php
use App\Models\AppUser;
use App\Models\Contact;
use App\Models\Directory;
use App\Models\Document;
use App\Models\Formulary;
@ -105,9 +106,12 @@ Breadcrumbs::for('bo.contacts.index', function (Generator $trail) {
route('bo.contacts.index', ['tenant' => \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