ldc-crm/app/Http/Controllers/Api/V1/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

570 lines
22 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1;
use App\Enums\Country;
use App\Http\Controllers\Controller;
use App\Http\Resources\CustomerResource;
use App\Http\Resources\StatusResource;
use App\Models\Customer;
use App\Models\CustomerInfo;
use App\Models\CustomerLogin;
use App\Models\CustomerOptInHistory;
use App\Models\SingleOptin;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class CustomerController extends Controller
{
/**
* Store a newly created resource in storage
* @param Request $request
* @return CustomerResource|StatusResource
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Squiz.Commenting.FunctionComment.Missing
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required',
'firstname' => 'required',
'lastname' => 'required',
'opt_in' => 'required',
'birth_date' => 'date_format:Y-m-d'
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 102,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = strtolower(filter_var($request->email, FILTER_SANITIZE_EMAIL));
$firstname = filter_var($request->firstname, FILTER_SANITIZE_STRING);
$lastname = filter_var($request->lastname, FILTER_SANITIZE_STRING);
$optIn = filter_var($request->opt_in, FILTER_SANITIZE_STRING);
$hashcode = Str::uuid();
// Récupère le customer s'il existe déjà.
$customer = Customer::where('email', $email)->first();
// Si le customer n'existe pas déjà.
if (!$customer) {
$customer = new Customer();
$customer->email = $email;
$customer->save();
$singleOptin = SingleOptin::where([['email', $email], ['brand_id', $brandId]])->first();
if ($singleOptin) {
$singleOptin->delete();
}
} else {
// Récupère les infos s'ils existent déjà.
$CustomerInfo = CustomerInfo::where([['customer_id', $customer->id], ['brand_id', $brandId]])->first();
if ($CustomerInfo) {
return new StatusResource([
'status' => 'KO', 'code' => 103,
'message' => 'User already registered on this site.'
]);
}
}
$customerInfos = new CustomerInfo();
$customerInfos->firstname = ucfirst(strtolower($firstname));
$customerInfos->lastname = ucfirst(strtolower($lastname));
$customerInfos->birth_date = !empty($request->birth_date) ?
filter_var($request->birth_date, FILTER_SANITIZE_STRING) : null;
$customerInfos->address = !empty($request->address) ?
filter_var($request->address, FILTER_SANITIZE_STRING) : null;
$customerInfos->address2 = !empty($request->address2) ?
filter_var($request->address2, FILTER_SANITIZE_STRING) : null;
$customerInfos->phone = !empty($request->phone) ?
filter_var($request->phone, FILTER_SANITIZE_STRING) : null;
try {
$customerInfos->country = Country::make(filter_var($request->country, FILTER_SANITIZE_STRING));
} catch (\Exception $e) {
$customerInfos->country = Country::france();
}
$customerInfos->city = !empty($request->city) ?
filter_var($request->city, FILTER_SANITIZE_STRING) : null;
$customerInfos->postcode = !empty($request->postcode) ?
filter_var($request->postcode, FILTER_SANITIZE_STRING) : null;
$customerInfos->opt_in = $optIn;
$customerInfos->hashcode = $hashcode;
$customerInfos->brand_id = $brandId;
$customerInfos->customer_id = $customer->id;
$customer->infos()->save($customerInfos);
// Création du login pour cet utilisateur sur ce service.
$customerLogin = new CustomerLogin();
$customerLogin->customer_id = $customer->id;
$customerLogin->password = Hash::make(trim($request->password));
$customerLogin->last_connection = now();
$customerLogin->brand_id = $brandId;
$customerLogin->status = 0;
$customer->logins()->save($customerLogin);
// Ajout dans la table d'historique des opt-in de la nouvelle valeur.
$optInHistory = new CustomerOptInHistory();
$optInHistory->customer_id = $customer->id;
$optInHistory->brand_id = $brandId;
$optInHistory->new_value = $optIn;
$optInHistory->save();
return new CustomerResource($customer, $brandId);
}
/**
* Display the specified resource.
*
* @param Illuminate\Http\Request $request
* @param mixed $type
* @param mixed $value
* @return CustomerResource | StatusResource
*/
public function show(Request $request, $type, $value)
{
$brandId = $request->brand_id;
if ($type === 'id') {
$customer = Customer::find($value);
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
} elseif ($type === 'email') {
$customer = Customer::where('email', $value)->first();
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
$CustomerInfo = $customer->infos()->where('brand_id', $brandId)->first();
if (!$CustomerInfo) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
} else {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
return new CustomerResource($customer, $brandId);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return CustomerResource|StatusResource
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Squiz.Commenting.FunctionComment.Missing
public function update(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'birth_date' => 'date_format:Y-m-d'
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 104,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$customer = Customer::find($id);
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 105, 'message' => 'User not found.']);
}
$CustomerInfo = $customer->infos()->where('brand_id', $brandId)->first();
if (!$CustomerInfo) {
return new StatusResource(['status' => 'KO', 'code' => 105, 'message' => 'User not found.']);
}
if ($request->has('firstname')) {
$CustomerInfo->firstname = filter_var($request->firstname, FILTER_SANITIZE_STRING);
}
if ($request->has('lastname')) {
$CustomerInfo->lastname = filter_var($request->lastname, FILTER_SANITIZE_STRING);
}
if ($request->has('birth_date')) {
if (!empty($request->birth_date)) {
$CustomerInfo->birth_date = filter_var($request->birth_date, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->birth_date = null;
}
}
if ($request->has('address')) {
if (!empty($request->address)) {
$CustomerInfo->address = filter_var($request->address, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->address = null;
}
}
if ($request->has('address2')) {
if (!empty($request->address2)) {
$CustomerInfo->address2 = filter_var($request->address2, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->address2 = null;
}
}
if ($request->has('phone')) {
if (!empty($request->phone)) {
$CustomerInfo->phone = filter_var($request->phone, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->phone = null;
}
}
if ($request->has('country')) {
try {
$CustomerInfo->country = Country::make(filter_var($request->country, FILTER_SANITIZE_STRING));
} catch (\Exception $e) {
$CustomerInfo->country = Country::france();
}
}
if ($request->has('city')) {
if (!empty($request->city)) {
$CustomerInfo->city = filter_var($request->city, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->city = null;
}
}
if ($request->has('postcode')) {
if (!empty($request->postcode)) {
$CustomerInfo->postcode = filter_var($request->postcode, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->postcode = null;
}
}
if ($request->has('opt_in')) {
$optIn = filter_var($request->opt_in, FILTER_SANITIZE_STRING);
if ($CustomerInfo->opt_in != $optIn) {
// Ajout dans la table d'historique des opt-in de la nouvelle valeur.
$optInHistory = new CustomerOptInHistory();
$optInHistory->customer_id = $customer->id;
$optInHistory->brand_id = $brandId;
$optInHistory->new_value = $optIn;
$optInHistory->save();
}
$CustomerInfo->opt_in = $optIn;
}
$CustomerInfo->save();
return new CustomerResource($customer, $brandId);
}
/**
* Authenticate the user
* @param Request $request
* @return CustomerResource|StatusResource
*/
public function authenticate(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 115,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = filter_var($request->email, FILTER_SANITIZE_EMAIL);
$password = trim($request->password);
$customer = Customer::where('email', $email)->first();
if ($customer) {
$login = $customer->logins()->where('brand_id', $brandId)->first();
if ($login) {
if (Hash::check($password, $login->password)) {
if ($login->status === false) {
$infos = $customer->infos()->where('brand_id', $brandId)->first();
// "Le compte de l'utilisateur n'est pas activé"
return new StatusResource(
[
'status' => 'KO',
'code' => 116,
'message' => 'Account not activated.',
'user' => [
'crm_id' => $customer->id,
'firstname' => $infos->firstname,
'lastname' => $infos->lastname,
'email' => $customer->email,
'birth_date' => $infos->birth_date,
'address' => $infos->address,
'address2' => $infos->address2,
'phone' => $infos->phone,
'country' => $infos->country->label,
'city' => $infos->city,
'postcode' => $infos->postcode,
'opt_in' => $infos->opt_in,
'hashcode' => $infos->hashcode,
'status' => $login->status,
]
]
);
}//end if
$login->last_connection = now();
$customer->logins()->save($login);
return new CustomerResource($customer, $brandId);
}//end if
// "Mot de passe incorrect"
return new StatusResource(['status' => 'KO', 'code' => 117, 'message' => 'Authentication failed.']);
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 118, 'message' => 'User not found.']);
}//end if
} else {
// "Ce compte n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 119, 'message' => 'User not found.']);
}//end if
}
/**
* Change the password of the user for the brand
* @param Request $request
* @param mixed $id
* @return StatusResource
*/
public function changePassword(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'password' => 'required',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 107,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$customer = Customer::find($id);
$password = trim($request->password);
if ($customer) {
$customerLogin = $customer->logins()->where('brand_id', $brandId)->first();
if ($customerLogin) {
// Check if same password.
if (Hash::check($password, $customerLogin->password)) {
return new StatusResource(['status' => 'KO', 'code' => 123, 'message' => 'Same password.']);
}
$customerLogin->password = Hash::make($password);
$customerLogin->save();
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 108, 'message' => 'User not found.']);
}
} else {
// "Ce compte n'existe pas"
return new StatusResource(['status' => 'KO', 'code' => 109, 'message' => 'User not found.']);
}
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
}
/**
* Activate the account of the user
* @param Request $request
* @param mixed $id
* @return StatusResource
*/
public function activateAccount(Request $request, $id)
{
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$login = $customer->logins()->where('brand_id', $brandId)->first();
if ($login) {
if ($login->status === false) {
$login->status = true;
$login->save();
// "Le compte a bien été activé"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte est déjà activé"
return new StatusResource(['status' => 'KO', 'code' => 110, 'message' => 'User already active.']);
}
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 111, 'message' => 'User not found.']);
}
} else {
// "Cet utilisateur n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 112, 'message' => 'User not found.']);
}
}
/**
* Delete the user account for the brand
* @param Request $request
* @param mixed $id
*/
// phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
public function delete(Request $request, $id)
{
die("Not implemented yet");
// TODO : Gestion avec mesBR.
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$customer->infos()->where('brand_id', $brandId)->first()->delete();
$customer->logins()->where('brand_id', $brandId)->first()->delete();
if (count($customer->infos) === 0 && count($customer->logins) === 0) {
$customer->optInHistory()->delete();
$customer->delete();
// Suppression du compte de la CRM terminée.
return new StatusResource(['status' => 'OK', 'message' => 'Deletion complete.']);
} else {
// Suppression des informations du compte pour la marque terminée.
return new StatusResource(['status' => 'OK', 'message' => 'Deletion complete.']);
}
} else {
return new StatusResource(['status' => 'KO', 'code' => 106, 'message' => 'User not found.']);
}
// phpcs:enable Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
}
/**
* Change the status of the opt-in for the customer on this brand
* @param Request $request
* @param mixed $id
* @return StatusResource
*/
public function toggleOptin(Request $request, $id)
{
// TODO : Liaison des opt-in avec MailJet.
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$infos = $customer->infos()->where('brand_id', $brandId)->first();
if ($infos) {
$infos->opt_in = $infos->opt_in === 0 ? 1 : 0;
// On enregistre l'historique du changement d'optin.
$optinHistory = new CustomerOptInHistory();
$optinHistory->customer_id = $customer->id;
$optinHistory->new_value = $infos->opt_in;
$optinHistory->brand_id = $brandId;
$optinHistory->save();
$infos->save();
// "Changement effectué"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte n'a pas d'information liés à cette marque"
return new StatusResource(['status' => 'KO', 'code' => 113, 'message' => 'User not found.']);
}
} else {
// "Cet utilisateur n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 114, 'message' => 'User not found.']);
}//end if
}
/**
* Remove opt-in for the customer on this brand
* @param Request $request
* @param mixed $hashcode
* @return StatusResource
*/
public function removeOptin(Request $request, $hashcode)
{
// TODO : Liaison des opt-in avec MailJet.
$brandId = $request->brand_id;
$infos = CustomerInfo::where([
['brand_id', $brandId],
['hashcode', $hashcode]
])->first();
if ($infos) {
$infos->opt_in = 0;
// On enregistre l'historique du changement d'optin.
$optinHistory = new CustomerOptInHistory();
$optinHistory->customer_id = $infos->customer_id;
$optinHistory->new_value = 0;
$optinHistory->brand_id = $brandId;
$optinHistory->save();
$infos->save();
// "Changement effectué"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte n'a pas d'information liés à cette marque"
return new StatusResource(['status' => 'KO', 'code' => 122, 'message' => 'User not found.']);
}
}
/**
* @param Request $request
* @return void
*/
// phpcs:ignore Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
public function addSingleOptin(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 120,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = filter_var($request->email, FILTER_SANITIZE_EMAIL);
// Check si le customer ou l'optin simple existe avant de l'ajouter en optin simple.
if (
!SingleOptin::where([['email', $email], ['brand_id', $brandId]])->exists() &&
!CustomerInfo::join("customers", "customer_infos.customer_id", "=", "customers.id")->where([
['brand_id', $brandId], ['email', $email]
])->exists()
) {
$singleOptin = new SingleOptin();
$singleOptin->email = $email;
$singleOptin->brand_id = $brandId;
$singleOptin->save();
} else {
// "Cet utilisateur est déjà inscrit à la newsletter pour cette marque"
return new StatusResource([
'status' => 'KO', 'code' => 121,
'message' => 'This user is already subscribed to the newsletter for this brand.'
]);
}
return new StatusResource(['status' => 'OK', 'message' => 'Optin added.']);
}
}