ldc-crm/app/Http/Controllers/Api/V2/CustomerController.php
Jérémy Munsch 81aabdab72
Version 3.0.0
This removes api authentification with token in favor of signature method.
Refactorize code and clean up. It add more unit test plus code coverage
in test/converage.

- Added config.api.timestampTimeout
- Added Helper::signRequest This calculates the request signature
- Added more unit tests and extend code coverage.
- Added rate limiter for web routes
- Added RemoveSpaces Sanitizer on api requests
- Added stress test suite
- Changed AuthServiceProvider api-token to api-hmac
- Changed CustomerSeeder to have at least of customer activated (for unit tests)
- Changed used transation for saveing multiple models
- Changed, in ServiceController generateApiToken to generateServicePassword
- Changed, timestamp has to be send in request with a DateTime format, it also has to a UTC  value
- Composer added brianium/paratest
- Composer update
- Fixed case where removing single optin could be the wrong one (not for the good service)
- Fixed customer delete
- Fixed customer would not be resolved by brand.
- Fixed Really check that request is json checking body content, preventing sending malformed requests
- Remove ApiError::needAuth
- Removed api_token
- Removed ApiController::authenticate
- Removed config.api.tokenTimeout
- Removed Header check or usage because of HAProxy doing weird stuff
- Removed lock files from gitignore
- Removed memcached dependency
- Removed unused questions resources api routes
- Removed workspace file

Migrations
- Added unique index on services.bearer_token
- Changed customer_id on customer_answer is nullable
- Changed services.bearer_pwd length to 312 (the size of the token)
- Dropped services.api_token and service.api_token_date
2022-01-06 12:11:05 +01:00

252 lines
9.5 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V2;
use App\Enums\ApiError;
use App\Exceptions\Api\V2\ApiErrorException;
use App\Exceptions\Api\V2\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V2\AuthenticateRequest;
use App\Http\Requests\Api\V2\StoreCustomerRequest;
use App\Http\Requests\Api\V2\UpdateCustomerRequest;
use App\Http\Resources\Api\V2\CustomerResource;
use App\Jobs\UpdateCustomerLastConnection;
use App\Models\Customer;
use App\Models\Service;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\DB;
class CustomerController extends Controller
{
/**
* Creates a new customer for the specified service.
*
* @param StoreCustomerRequest $request
* @param Service $service
* @return JsonResource
* @throws ApiErrorException With ApiError::customerExists if the customer with this email already exists.
* @throws CrmException If saving the customer to DB failed.
*/
public function store(StoreCustomerRequest $request, Service $service): JsonResource
{
$data = (object)$request->validated();
/** @var \App\Models\Customer */
$customer = Customer::where('email', $data->email)->firstOrCreate([
'email' => $data->email
]);
DB::transaction(function () use (&$customer, $data, $request, $service) {
$saved = $customer->save();
$customer->setOptIn($service->brand, $data->opt_in);
// * Delete Single optin if exists.
/** @var \App\Models\SingleOptin|null $singleOptin */
$singleOptin = SingleOptin::where('email', $data->email)
->where('brand_id', $service->brand->id)->first();
if ($singleOptin) {
$saved = $singleOptin->delete() && $saved;
}
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW);
if ($customerInfo->exists) {
throw new ApiErrorException(ApiError::customerExists());
}
$customerInfo->fill($request->validated());
$customerInfo->brand()->associate($service->brand);
$customerInfo->customer()->associate($customer);
$saved = $customerInfo->save() && $saved;
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_NEW);
$customerLogin->fill($request->validated());
$customerLogin->brand()->associate($service->brand);
$customerLogin->customer()->associate($customer);
$saved = $customerLogin->save() && $saved;
if (!$saved) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to store customer.');
// @codeCoverageIgnoreEnd
}
});
return new CustomerResource($service->brand, $customer);
}
/**
* Renders the specific customer infos
*
* @param Service $service
* @param Customer $customer
* @return JsonResource
*/
public function show(Service $service, Customer $customer): JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
return new CustomerResource($service->brand, $customer);
}
/**
* Updates information for a specific customer
*
* @param UpdateCustomerRequest $request
* @param Service $service
* @param Customer $customer
* @return JsonResource
* @throws CrmException If saving the customer to DB failed.
*/
public function update(UpdateCustomerRequest $request, Service $service, Customer $customer): JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
DB::transaction(function () use ($request, $customer, $service) {
// To correct with create.
$customer->fill($request->validated());
$saved = $customer->save();
if ($request->has('opt_in')) {
$customer->setOptIn($service->brand, $request->opt_in);
}
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
$customerInfo->fill($request->validated());
$saved = $customerInfo->save() && $saved;
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
$customerLogin->fill($request->validated());
$saved = $customerLogin->save() && $saved;
if (!$saved) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer.');
// @codeCoverageIgnoreEnd
}
});
return new CustomerResource($service->brand, $customer);
}
/**
* Deletes customer information for a specific service
*
* @param Service $service
* @param Customer $customer
* @return JsonResponse
* @throws CrmException If removing the customer off the DB failed.
*/
public function destroy(Service $service, Customer $customer): JsonResponse
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
// TODO: Gestion avec mesBR.
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
DB::transaction(function () use ($service, $customer, $customerLogin, $customerInfo) {
if (
$customerInfo->delete() === false ||
$customerLogin->delete() === false
) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to delete customer.');
// @codeCoverageIgnoreEnd
};
$customer->optInHistory()->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand_id);
})->delete();
// * Force loading real relation counts.
$customer->unsetRelation('logins');
$customer->unsetRelation('infos');
$logins = $customer->logins()->count();
$infos = $customer->infos()->count();
// If customer is not associated with any brand then delete.
if (!$logins and !$infos) {
if ($customer->delete() === false) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer.');
// @codeCoverageIgnoreEnd
}
}
});
return response()->json();
}
/**
* Authenticate the user
* @param AuthenticateRequest $request
* @param Service $service
* @return JsonResource
* @throws ApiErrorException With ApiError::authFailed() in case of wrong credentials.
* @throws ApiErrorException With ApiError::customerNotActivated() if customer account is not activated.
*/
public function authenticate(AuthenticateRequest $request, Service $service): JsonResource
{
try {
/** @var \App\Models\Customer */
$customer = Customer::where('email', $request->email)->firstOrFail();
/** @var \App\Models\CustomerLogin $customerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if (!$customerLogin->passwordMatch($request->password)) {
throw new ApiErrorException(ApiError::authFailed());
}
if (!$customerLogin->status) {
throw new ApiErrorException(ApiError::customerNotActivated());
}
UpdateCustomerLastConnection::dispatch($customer, $customerLogin, now())
->onConnection('database')->afterResponse();
return new CustomerResource($service->brand, $customer);
} catch (ModelNotFoundException $e) {
throw new ApiErrorException(ApiError::authFailed());
}//end try
}
/**
* Activate the account of the user
* @param Customer $customer
* @param Service $service
* @return \Illuminate\Http\JsonResponse
* @throws ApiErrorException With ApiError::customerAlreadyActive if customer account is already active.
* @throws CrmException If saving the customer to DB failed.
*/
public function activateAccount(Customer $customer, Service $service)
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
/** @var \App\Models\CustomerLogin $login */
$login = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if ($login->status) {
throw new ApiErrorException(ApiError::customerAlreadyActive());
}
$login->status = true;
if (!$login->save()) {
// @codeCoverageIgnoreStart
throw new CrmException('Failed to save login');
// @codeCoverageIgnoreEnd
}
return response()->json();
}
}