feat: bump laravel 12 / vite / vue3 / tutti quanti

This commit is contained in:
Noah Lancien 2026-03-19 11:21:49 +01:00
parent fe02fcca8f
commit effc5db63f
360 changed files with 30026 additions and 15847 deletions

View file

@ -58,3 +58,11 @@ MAILJET_APISECRET=
MAILJET_SMSTOKEN=
#MAILJET_SMSENDPOINT=
#MAILJET_SMSFROM=
# SAML
SAML2_DEFAULT_TENANT_UUID=
SAML2_SP_CERT_x509=
SAML2_SP_CERT_PRIVATEKEY=
SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=

62
.eslintrc.json Normal file
View file

@ -0,0 +1,62 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-strongly-recommended",
"@vue/eslint-config-typescript"
],
"overrides": [
{
"files": ["*.ts", "*.vue"],
"rules": {
"no-undef": "off"
}
},
{
"files": ["*.vue"],
"rules": {
"indent": "off"
}
}
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["vue", "@typescript-eslint"],
"ignorePatterns": [
"webpack.mix.js",
"**/*.php",
"**/*.svg",
"**/vendor/*.js",
"**/vendor/**/*.js",
"**/vendor/**/*.scss"
],
"rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
"vue/one-component-per-file": "off"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".vue", ".ts", ".d.ts"]
},
"alias": {
"extensions": [".vue", ".js", ".ts", ".scss", ".d.ts"],
"map": [
["~", "./resources/assets"],
["@", "./node_modules"]
]
}
}
}
}

45
.stylelintrc.json Normal file
View file

@ -0,0 +1,45 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-sass-guidelines",
"stylelint-config-idiomatic-order"
],
"plugins": [
"stylelint-scss"
],
"files": [
"**/*.css",
"**/*.scss"
],
"ignoreFiles": [
"**/vendor/*.css",
"**/vendor/**/*.css",
"**/vendor/*.scss",
"**/vendor/**/*.scss",
"**/vendor/*.sass",
"**/vendor/**/*.sass",
"**/vendor/*.less",
"**/vendor/**/*.less"
],
"customSyntax": "postcss-scss",
"rules": {
"declaration-property-value-no-unknown": true,
"selector-max-id": null,
"color-no-invalid-hex": true,
"order/properties-alphabetical-order": null,
"max-nesting-depth": 5,
"selector-max-compound-selectors": 5,
"declaration-colon-newline-after": null,
"value-list-comma-newline-after": null,
"scss/double-slash-comment-empty-line-before": [
"always",
{
"ignore": [
"between-comments",
"stylelint-commands",
"inside-block"
]
}
]
}
}

View file

@ -1,5 +1,44 @@
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-7.3:latest
COPY --chown=www-data:www-data . /app/srcs
FROM composer:2.9.2 AS vendor
ARG GITLAB_USER
ARG GITLAB_PASSWORD
COPY database/ database/
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer config gitlab-token.lab.s-web.fr ${GITLAB_USER} ${GITLAB_PASSWORD}
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--no-dev \
--prefer-dist
FROM node:20.18 AS frontend
ARG GITLAB_PASSWORD
RUN mkdir -p /app/public/build
COPY package*.json tsconfig.json vite.config.ts .eslintrc.json .stylelintrc.json /app/
COPY resources/ /app/resources/
WORKDIR /app
RUN npm config set @ldc-web:registry https://lab.s-web.fr/api/v4/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm ci && npm run prod
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.4:170625-130159
ARG USER=www-data
COPY --chown=${USER}:${USER} . /app/srcs
COPY --chown=${USER}:${USER} ./resources/favicon /app/srcs/public
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/
USER root
RUN chmod -R 755 /app/srcs/storage
USER www-data
USER ${USER}

View file

@ -4,11 +4,11 @@ namespace App\Auth;
use App\Models\Customer;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Contracts\Auth\UserProvider as UserProviderContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use LDC\LDCCRM\Enums\ApiError;
use LDC\LDCCRM\Exceptions\ApiErrorException;
use LDC\LDCCRM\Exceptions\CrmException;
@ -17,10 +17,11 @@ use LDC\LDCCRM\Models\Customer as CrmCustomer;
/**
* Crm user provider to integrate nicely with Laravel
*/
class CrmUserProvider extends EloquentUserProvider implements UserProviderContract
class CrmUserProvider extends EloquentUserProvider implements UserProvider
{
/**
* The model to use, specified in auth config guard.
*
* @var string
*/
private $usedModel = Customer::class;
@ -28,16 +29,16 @@ class CrmUserProvider extends EloquentUserProvider implements UserProviderContra
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
public function retrieveByCredentials(array $credentials): \Illuminate\Contracts\Auth\Authenticatable|null
{
// * Let laravel find a model
$model = parent::retrieveByCredentials($credentials);
$model = parent::retrieveByCredentials($credentials) ?? self::retrieveTrashed($credentials);
try {
/** @var \LDC\LDCCRM\CrmApiV2 */
/** @var \LDC\LDCCRM\CrmApiV4 */
$crm = app('crmAPI');
// Si pas de compte local.
@ -54,11 +55,11 @@ class CrmUserProvider extends EloquentUserProvider implements UserProviderContra
return $model;
} catch (ApiErrorException $e) {
// * Customer is not found.
if ($e->getApiError()->equals(ApiError::modelNotFound())) {
if ($e->getApiError()->equals(ApiError::modelNotFound)) {
return null;
}
report($e);
Log::error(sprintf('Unhandled response : %s', $e->getApiError()->label));
Log::error('Unhandled response ' . $e->getApiError()->label());
} catch (CrmException | \Exception $e) {
report($e);
// Une erreur critique est survenue avec la CRM.
@ -71,14 +72,14 @@ class CrmUserProvider extends EloquentUserProvider implements UserProviderContra
/**
* Validate a user against the given credentials.
*
* @param UserContract $model
* @param array $credentials
* @param \Illuminate\Contracts\Auth\Authenticatable $model
* @param array $credentials
* @return boolean
* @throws \RuntimeException If model provided is not a Customer or connection to server fails.
* @throws \LDC\LDCCRM\Exceptions\CrmException On CRM call error.
* @throws \Exception On CRM call error.
*/
public function validateCredentials(UserContract $model, array $credentials)
public function validateCredentials(Authenticatable $model, array $credentials): bool
{
/** @var \Illuminate\Database\Eloquent\Model $model */
if (\get_class($model) !== $this->usedModel) {
@ -86,30 +87,33 @@ class CrmUserProvider extends EloquentUserProvider implements UserProviderContra
}
try {
/** @var \LDC\LDCCRM\CrmApiV2 */
/** @var \LDC\LDCCRM\CrmApiV4 */
$crm = app('crmAPI');
// * If has trait soft delete and customer trashed, then restore customer.
if (
\in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, \class_uses_recursive($model)) and
// @phpstan-ignore-next-line
$model->trashed()
) {
// @phpstan-ignore-next-line
$model->restore();
}
// * Ne devrait pas survenir, $crm->login throws ApiError::authFailed()
// * Ne devrait pas survenir, $crm->login throws ApiError::authFailed
return $crm->login($credentials['email'], $credentials['password']);
} catch (ApiErrorException $e) {
// @phpstan-ignore-next-line
if (!$model->hashcode or !CrmCustomer::find($model->hashcode)) {
$model->forceDelete();
return false;
}
if ($e->getApiError()->equals(ApiError::authFailed())) {
if ($e->getApiError()->equals(ApiError::authFailed)) {
return false;
}
// ! Assert Authenticable model must be valided (MustValidate trait).
if ($e->getApiError()->equals(ApiError::customerNotActivated())) {
Session::flash('warning', \trans('vous devez activer votre compte'));
if ($e->getApiError()->equals(ApiError::customerNotActivated)) {
Session::flash('warning', \trans('Vous devez activer votre compte'));
return true;
}
} catch (CrmException | \Exception $e) {
@ -121,7 +125,8 @@ class CrmUserProvider extends EloquentUserProvider implements UserProviderContra
} //end try
Log::error(__CLASS__ . '::validateCredentials reached end of possibilities.');
return null;
return false;
}
/**
@ -131,17 +136,47 @@ class CrmUserProvider extends EloquentUserProvider implements UserProviderContra
* @return \Illuminate\Database\Eloquent\Model
* @throws \Throwable If saving to DB failed.
*/
private function createLocalCustomer(\LDC\LDCCRM\Models\Customer $crmCustomer): Model
private function createLocalCustomer(\LDC\LDCCRM\Models\Customer $crmCustomer): \Illuminate\Database\Eloquent\Model
{
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = $this->usedModel::firstOrNew([
'email' => $crmCustomer->email,
$model = $this->usedModel::firstOrNew([
'email' => $crmCustomer->email
]);
$model->firstname = $crmCustomer->firstname;
$model->lastname = $crmCustomer->lastname;
$model->hashcode = $crmCustomer->hashcode;
// @phpstan-ignore-next-line
$model->hashcode = $crmCustomer->hashcode;
// @phpstan-ignore-next-line
$model->email_verified_at = $crmCustomer->status ? now() : null;
$model->saveOrFail();
return $model;
}
/**
* Retrieve a trashed customer by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
* @throws \ErrorException If cannot connect to LDAP.
*/
private function retrieveTrashed(array $credentials): ?\Illuminate\Contracts\Auth\Authenticatable
{
// * Make changes
if (empty($credentials)) {
return null;
}
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a customer, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
if (!Str::contains($key, 'password')) {
$query->where($key, $value);
}
}
// @phpstan-ignore-next-line
return $query->withTrashed()->first();
}
}

View file

@ -1,107 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Enums\UserRoleEnum;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:create';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a backend admin user';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$email = null;
while (is_null($email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,spoof'
]);
$validator->validate();
$email = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid email %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
$firstname = null;
while (is_null($firstname)) {
try {
$tmp = $this->ask('Type the wanted user firstname', 'Admin');
$validator = Validator::make(['firstname' => $tmp], [
'firstname' => 'required|min:5,max:255'
]);
$validator->validate();
$firstname = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid firstname %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
$lastname = null;
while (is_null($lastname)) {
try {
$tmp = $this->ask('Type the wanted user lastname', 'Saïtama');
$validator = Validator::make(['lastname' => $tmp], [
'lastname' => 'required|min:5,max:255'
]);
$validator->validate();
$lastname = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid lastname %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
$password = null;
while (
($password = $this->secret(
(!\is_null($password) ? 'Failed to confirm, ' : '') . 'Type the wanted user password'
)) !== $this->secret('Confirm the password')
) {
continue;
}; //end while
$user = new User();
$user->firstname = $firstname;
$user->lastname = $lastname;
$user->email = $email;
$user->password = $password;
$user->role = $this->confirm('Is the user a super admin?') ?
UserRoleEnum::superAdmin() : UserRoleEnum::user();
$user->saveOrFail();
$this->info('User created.');
}
}

View file

@ -0,0 +1,201 @@
<?php
namespace App\Console\Commands;
use App\Enums\UserRoleEnum;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
class UserCreate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:user:create';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a backend admin user';
/**
* The first name of the new user.
*
* @var string|null
*/
protected $first_name;
/**
* The last name of the new user.
*
* @var string|null
*/
protected $last_name;
/**
* The email of the new user.
*
* @var string|null
*/
protected $email;
/**
* The role of the new user.
*
* @var string|null
*/
protected $role;
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$this->first_name = null;
$this->addFirstName();
$this->last_name = null;
$this->addLastName();
$this->email = null;
$this->addEmail();
$this->role = null;
$this->addRole();
$user = new User([
'firstname' => $this->first_name,
'lastname' => $this->last_name,
'email' => $this->email,
'role' => $this->role,
'published' => true,
'published_at' => Carbon::now(),
]);
$user->saveOrFail();
$this->info('User created 👌');
}
/**
* Add a first name to the user.
*
* @return void
*/
private function addFirstName(): void
{
while (is_null($this->first_name)) {
try {
$tmp = $this->ask('Type the wanted user first name', 'Admin');
$validator = Validator::make(['first_name' => $tmp], [
'first_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->first_name = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid first name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
/**
* Add a last name to the user.
*
* @return void
*/
private function addLastName(): void
{
while (is_null($this->last_name)) {
try {
$tmp = $this->ask('Type the wanted user last name', 'Saïtama');
$validator = Validator::make(['last_name' => $tmp], [
'last_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->last_name = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid last_name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
/**
* Add an email to the user.
*
* @return void
*/
private function addEmail(): void
{
while (is_null($this->email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,dns'
]);
$validator->validate();
$this->email = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid email %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
/**
* Add a role to the user.
*
* @return void
*/
private function addRole(): void
{
while (is_null($this->role)) {
try {
$tmp = $this->choice(
'Select his role',
[
Str::ucFirst(UserRoleEnum::superAdmin->label()),
Str::ucFirst(UserRoleEnum::admin->label())
],
Str::ucFirst(UserRoleEnum::admin->label()),
);
if ($tmp === Str::ucFirst(UserRoleEnum::superAdmin->label())) {
$tmp = UserRoleEnum::superAdmin;
}
if ($tmp === Str::ucFirst(UserRoleEnum::admin->label())) {
$tmp = UserRoleEnum::admin;
}
$validator = Validator::make(['role' => $tmp], [
'role' => 'required'
]);
$validator->validate();
$this->role = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please choose from the selection %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
}

View file

@ -2,37 +2,22 @@
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/**
* @method static self professional()
* @method static self individual()
*/
final class CustomerTypeEnum extends BaseEnumRoutable
enum CustomerTypeEnum: int
{
/**
* Enum values
*
* @return array<string,int>
*/
protected static function values(): array
{
return [
'professional' => 3,
'individual' => 4
];
}
use BaseEnum;
case professional = 3;
case individual = 4;
/**
* Enum values
* Optionnal labels definition
*
* @return array<string,string>
* @phpstan-ignore-next-line
*/
protected static function labels(): array
{
return [
'professional' => __('Professionnel'),
'individual' => __('Particulier'),
];
}
private const LABELS = [
self::professional->name => 'Professionnel',
self::individual->name => 'Particulier',
];
}

View file

@ -2,59 +2,30 @@
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/**
* @method static self Ble()
* @method static self Colza()
* @method static self Mais()
* @method static self Orge()
* @method static self Tournesol_Oleique()
* @method static self Tournesol_Standard()
*/
final class ProduitEnum extends BaseEnum
enum ProduitEnum: int
{
/**
* Enum values
*
* @return array<string,int>
*/
protected static function values(): array
{
return [
'Ble' => 1,
'Colza' => 2,
'Mais' => 3,
'Orge' => 4,
'Tournesol_Oleique' => 5,
'Tournesol_Standard' => 6,
];
}
use BaseEnum;
case ble = 1;
case colza = 2;
case mais = 3;
case orge = 4;
case tournesol_oleique = 5;
case tournesol_standard = 6;
/**
* Enum values
* Optionnal labels definition
*
* @return array<string,string>
* @phpstan-ignore-next-line
*/
protected static function labels(): array
{
return [
'Ble' => __('Ble'),
'Colza' => __('Colza'),
'Mais' => __('Maïs'),
'Orge' => __('Orge'),
'Tournesol_Oleique' => __('Tournesol Oléique'),
'Tournesol_Standard' => __('Tournesol Standard'),
];
}
/**
* To string
*
* @return string
*/
public function __toString(): string
{
return (string)$this->label;
}
private const LABELS = [
self::ble->name => 'Ble',
self::colza->name => 'Colza',
self::mais->name => 'Maïs',
self::orge->name => 'Orge',
self::tournesol_oleique->name => 'Tournesol Oléique',
self::tournesol_standard->name => 'Tournesol Standard',
];
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum as BaseEnum;
enum StaticPageType: int
{
use BaseEnum;
case legals = 1;
case confidentiality = 2;
/**
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::legals->name => 'Mentions légales',
self::confidentiality->name => 'Confidentialité',
];
}

View file

@ -2,37 +2,22 @@
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/**
* @method static self superAdmin()
* @method static self admin()
*/
final class UserRoleEnum extends BaseEnumRoutable
enum UserRoleEnum: int
{
/**
* Enum values
*
* @return array<string,int>
*/
protected static function values(): array
{
return [
'superAdmin' => 1,
'admin' => 2
];
}
use BaseEnum;
case superAdmin = 1;
case admin = 2;
/**
* Enum values
* Optionnal labels definition
*
* @return array<string,string>
* @phpstan-ignore-next-line
*/
protected static function labels(): array
{
return [
'superAdmin' => __('Super-Administrateur'),
'admin' => __('Administrateur'),
];
}
private const LABELS = [
self::superAdmin->name => 'Super-Administrateur',
self::admin->name => 'Administrateur',
];
}

View file

@ -3,22 +3,31 @@
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [];
/**
* A list of the exception types that are not reported.
*
* @var array<int,class-string<\Throwable>>
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [];
/**
* A list of the inputs that are never flashed for validation exceptions.
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int,string>
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
@ -30,7 +39,7 @@ class Handler extends ExceptionHandler
*/
public function register()
{
$this->reportable(function (\Throwable $e) {
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}

View file

@ -14,7 +14,6 @@ class ArticlesController extends Controller
{
public function index(Request $request)
{
// $a = Articles::with('category')->get();
$a = $this->filterArticles($request);
$a->with('category');
@ -40,7 +39,7 @@ class ArticlesController extends Controller
Session::save();
}
if (Session::has('articles.sort_col') and Session::has('articles.sort_way')) {
if (Session::has('articles.sort_col') && Session::has('articles.sort_way')) {
switch (Session::get('articles.sort_col')) {
case 'category_id':
$articles = $articles->orderBy('category_id', Session::get('articles.sort_way'));

View file

@ -98,6 +98,7 @@ class CotationsController extends Controller
protected function sendMessageObjectif($objectif)
{
$objectif->load('customer');
// par défaut on va envoyer un mail, car on peut toujours
// envoi du mail d'alerte
Mail::to($objectif->customer->email)->send(new AlertGoalAchieved($objectif));

View file

@ -101,7 +101,7 @@ class CustomerController extends Controller
$customer->role = CustomerTypeEnum::make(\intval($request->post('role')));
$send_openaccess_mail = false;
if ($customer->role->equals(CustomerTypeEnum::professional())) {
if ($customer->role->equals(CustomerTypeEnum::professional)) {
// * if num client has been filled then il va falloir envoyer un mail d'information d'accès
$send_openaccess_mail = empty($customer->getOriginal('client_number')) &&
!empty($customer->client_number);

View file

@ -22,7 +22,7 @@ class ObjectivesController extends Controller
$objectives = $this->filterObjectives($request);
$objectives->with(['customer', 'status']);
$objectives = $objectives->paginate(10);
$objectives = $this->paginate($objectives);
return view('bo.objectives.index', compact('objectives', 'filter_tc_id'));
}
@ -53,7 +53,7 @@ class ObjectivesController extends Controller
Session::save();
}
if (Session::has('objectives.sort_col') and Session::has('objectives.sort_way')) {
if (Session::has('objectives.sort_col') && Session::has('objectives.sort_way')) {
switch (Session::get('objectives.sort_col')) {
case 'produit':
$objectives = $objectives->orderBy('produit', Session::get('objectives.sort_way'));
@ -89,8 +89,8 @@ class ObjectivesController extends Controller
if (Session::has('objectives.filter_tc_id')) {
if (
Session::has('objectives.sort_col')
and Session::has('objectives.sort_way')
and Session::get('objectives.sort_col') == 'tc_id'
&& Session::has('objectives.sort_way')
&& Session::get('objectives.sort_col') == 'tc_id'
) {
$objectives = $objectives->where('customers.tc_id', '=', Session::get('objectives.filter_tc_id'));
} else {

View file

@ -0,0 +1,84 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\StaticPages\UpdateStaticPageRequest;
use App\Models\StaticPage;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class StaticPageController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = StaticPage::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'title',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.title'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$staticPageModels = $this->paginate($query);
return view('bo.static_pages.index', compact('staticPageModels', 'search', 'searchFields'));
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\StaticPage $static_page
* @return \Illuminate\Contracts\View\View
*/
public function edit(StaticPage $static_page): \Illuminate\Contracts\View\View
{
return view('bo.static_pages.edit', ['staticPageModel' => $static_page]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\StaticPages\UpdateStaticPageRequest $request
* @param \App\Models\StaticPage $static_page
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateStaticPageRequest $request, StaticPage $static_page): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $static_page) {
$static_page->fill($request->validated());
if ($static_page->saveOrFail()) {
return redirect()->route('bo.static_pages.edit', $static_page)
->with('success', __('crud.messages.has_been_updated', [
'model' => Str::ucfirst(\trans_choice('models.classes.static_page', 1))
]));
}
return redirect()->route('bo.static_pages.edit', $static_page)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(\trans_choice('models.classes.static_page', 1))
]));
});
}
}

View file

@ -31,7 +31,7 @@ class UsersController extends Controller
/** @var \App\Models\User */
$loggedUser = Auth::guard('backend')->user();
$users = User::query()->where('role', '>=', $loggedUser->role->value)->paginate(20);
$users = User::query()->where('role', '>=', $loggedUser->role)->paginate(20);
return view('bo.users.index', compact('users'));
}
@ -57,7 +57,7 @@ class UsersController extends Controller
$loggedUser = Auth::guard('backend')->user();
$user = new User($request->validated());
$user->role = $loggedUser->role->equals(UserRoleEnum::superAdmin()) ?
$user->role = $loggedUser->role->equals(UserRoleEnum::superAdmin) ?
UserRoleEnum::make(\intval($request->role)) : $loggedUser->role;
if ($user->saveOrFail()) {
@ -95,7 +95,7 @@ class UsersController extends Controller
$loggedUser = Auth::guard('backend')->user();
$user->fill($request->validated());
$user->role = $loggedUser->role->equals(UserRoleEnum::superAdmin()) ?
$user->role = $loggedUser->role->equals(UserRoleEnum::superAdmin) ?
UserRoleEnum::make(\intval($request->role)) : $loggedUser->role;
if ($user->saveOrFail()) {

View file

@ -2,124 +2,14 @@
namespace App\Http\Controllers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Uwil\LaravelCommon\Controllers\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
/**
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @param \Illuminate\Database\Eloquent\Builder $query The eloquent query builder.
* @param string $search The query string.
* @param callable $searchQuery The query string.
* @param string ...$fields Either a simple array or a string
* in case of a string the two fields
* would be concatenate to make the search.
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function searchQuery(
Builder $query,
string $search,
callable $searchQuery = null,
string ...$fields
): Builder {
if (count($fields)) {
// Search using words.
$words = explode(' ', $search);
$query->where(function ($query) use ($searchQuery, $words, $search, $fields) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$this->searchOnFields($query, $word, $fields);
} //end foreach
// Search whole using whole sentence.
$this->searchOnFields($query, $search, $fields);
if ($searchQuery) {
$searchQuery($query);
}
});
} //end if
return $query;
}
/**
* Build query search on all fields
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param array $fields
* @return \Illuminate\Database\Eloquent\Builder
*/
private function searchOnFields(Builder $query, string $search, array $fields): Builder
{
$table = $query->getModel()->getTable();
foreach ($fields as $f) {
$f = str_replace('?', '\?', $f);
if (is_array($f)) {
$query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($f)->map(function ($q) use ($table) {
return "`$table`.`$q`";
})->implode(', \' \', ')),
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
} else {
$query->orWhereRaw(
"UPPER(`$table`.`$f`) LIKE UPPER(?)",
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
}
}
return $query;
}
/**
* Sort columns with a query
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): Builder
{
$rName = request()->route()->getName();
$table = $query->getModel()->getTable();
if (request()->sort_col) {
Session::put("$rName.sort_col", request()->sort_col);
Session::put("$rName.sorted", true);
}
if (request()->sort_way) {
Session::put("$rName.sort_way", request()->sort_way);
Session::put("$rName.sorted", true);
}
if (request()->rst) {
Session::remove("$rName.sorted");
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
}
if (request()->sort_col and request()->sort_way and Schema::hasColumn($table, request()->sort_col)) {
if (
!isset($ignore) or
(isset($ignore) and !in_array(request()->sort_col, $ignore))
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->sort_way);
}
} elseif (Schema::hasColumn($table, 'order')) {
$query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order');
Session::put("$rName.sort_way", 'asc');
}
return $query;
}
}

View file

@ -3,13 +3,18 @@
namespace App\Http\Controllers\Fo\Auth;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Providers\RouteServiceProvider;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use LDC\LDCCRM\Models\Customer as CrmCustomer;
class LoginController extends Controller
{
@ -89,4 +94,49 @@ class LoginController extends Controller
{
return response()->redirectToIntended()->getTargetUrl();
}
/**
* Attempt to log the user into the application.
*
* @param \Illuminate\Http\Request $request
* @return boolean
*/
protected function attemptLogin(Request $request): bool
{
$credentials = $this->credentials($request);
$customerModel = Customer::query()->where('email', Arr::get($credentials, 'email'))->first();
$crmCustomer = CrmCustomer::findWithEmail(Arr::get($credentials, 'email'));
// Check database customer.
if (!$customerModel) {
// Check crm customer.
if (!$crmCustomer) {
return false;
}
/** @var \App\Models\Customer $customerModel */
$customerModel = new Customer(Arr::only($crmCustomer->jsonSerialize(), ['email']));
$customerModel->hashcode = $crmCustomer->hashcode;
if (!$customerModel->saveOrFail()) {
return false;
}
} else {
// Check crm customer.
if (!$crmCustomer) {
$customerModel->delete();
return false;
}
} //end if
try {
CrmCustomer::login(Arr::get($credentials, 'email'), Arr::get($credentials, 'password'));
} catch (Exception $ex) {
Log::error($ex);
return false;
}
$this->guard()->loginUsingId($customerModel->getKey());
return true;
}
}

View file

@ -79,7 +79,7 @@ class RegisterController extends Controller
protected function create(array $data)
{
// * OK CGV has to be validated as true.
$data['country'] = Country::france();
$data['country'] = Country::france;
$data['ok_cgv'] = \now();
$data['phone'] = Arr::get($data, 'phone');
$data['role'] = intval(Arr::get($data, 'user_cat'));
@ -97,7 +97,7 @@ class RegisterController extends Controller
$crmCustomer = new CrmCustomer($this->getOnlyCrmData($data));
// * Country Is forced to france
$crmCustomer->country = Country::france();
$crmCustomer->country = Country::france->value;
$crmCustomer->save();
// * Enregistrement du Hashcode
@ -117,12 +117,12 @@ class RegisterController extends Controller
return $customer;
});
} catch (ApiErrorException $e) {
if ($e->getApiError()->equals(ApiError::customerExists())) {
if ($e->getApiError()->equals(ApiError::customerExists)) {
throw ValidationException::withMessages([
'error' => 'Un compte est déjà associé à cette adresse email.'
]);
}
if ($e->getApiError()->equals(ApiError::validationFailed())) {
if ($e->getApiError()->equals(ApiError::validationFailed)) {
throw ValidationException::withMessages([
'error' => $e->getMessage()
]);

View file

@ -68,7 +68,7 @@ class ResetPasswordController extends Controller
/** @var \App\Models\Customer\Customer $user */
/** @var string $password */
/** @var \LDC\LDCCRM\CrmApiV2 */
/** @var \LDC\LDCCRM\CrmApiV4 */
$crm = app('crmAPI');
try {

View file

@ -2,7 +2,9 @@
namespace App\Http\Controllers\Fo;
use App\Enums\StaticPageType;
use App\Http\Controllers\Controller;
use App\Models\StaticPage;
class LegalNoticeController extends Controller
{
@ -12,6 +14,7 @@ class LegalNoticeController extends Controller
*/
public function index()
{
return view('fo.legalnotice');
$staticPageModel = StaticPage::query()->where('type', StaticPageType::legals)->first();
return view('fo.legalnotice', compact('staticPageModel'));
}
}

View file

@ -2,7 +2,9 @@
namespace App\Http\Controllers\Fo;
use App\Enums\StaticPageType;
use App\Http\Controllers\Controller;
use App\Models\StaticPage;
class PrivacyPolicyController extends Controller
{
@ -12,6 +14,7 @@ class PrivacyPolicyController extends Controller
*/
public function index()
{
return view('fo.privacypolicy');
$staticPageModel = StaticPage::query()->where('type', StaticPageType::confidentiality)->first();
return view('fo.privacypolicy', compact('staticPageModel'));
}
}

View file

@ -151,7 +151,7 @@ class CustomerController extends Controller
} catch (CrmException $e) {
return false;
} catch (ApiErrorException $e) {
if ($e->getApiError()->equals(ApiError::validationFailed())) {
if ($e->getApiError()->equals(ApiError::validationFailed)) {
throw ValidationException::withMessages([
'error' => $e->getMessage()
]);

View file

@ -14,8 +14,9 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
@ -29,16 +30,25 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
'htmlsanitize',
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
],
'saml' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
],
'api' => [
'throttle:api',
'throttle:40,1',
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
@ -53,6 +63,7 @@ class Kernel extends HttpKernel
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
@ -64,6 +75,6 @@ class Kernel extends HttpKernel
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'admin-secure' => \App\Http\Middleware\AdminSecureMiddleware::class,
'htmlsanitize' => \Uwil\LaravelCommon\Middlewares\HtmlSanitizer::class,
];
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Uwil\LaravelCommon\Helpers\ToolboxHelper as ToolboxHelper;
class HtmlSanitizer
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|array ...$params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(
Request $request,
\Closure $next,
string|array ...$params
): \Symfony\Component\HttpFoundation\Response {
$inputs = $request->all();
/** @var callable|null */
$cleanInput = null;
$cleanInput = function (
null|string|array &$input,
string $name,
?string $rootName = null
) use (
$params,
&$cleanInput
): void {
// * Nested element array
if (is_array($input)) {
array_walk_recursive($input, $cleanInput, $name);
}
if (!is_string($input)) {
return;
}
if (in_array($name, $params) or in_array($rootName ?? $name, $params)) {
// ? Is a json input ?
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
// ! Decode each json field recursive.
\array_walk_recursive($isJson, function (string|array &$item) {
if (\is_string($item)) {
$item = clean($item);
$item = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($item);
}
});
} else {
$input = clean($input);
$input = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($input);
}
} else {
$input = strip_tags($input);
}
};
\array_walk($inputs, $cleanInput);
$request->merge($inputs);
return $next($request);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LogoutUnpublishedUsers
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
/** @var \App\Models\User|null $userModel */
$userModel = Auth::user();
if (!is_null($userModel) && !$userModel->published) {
Auth::logout();
return redirect()->route('bo.login')->with('warning', trans('auth.unpublished_user'));
}
return $next($request);
}
}

View file

@ -19,7 +19,7 @@ class Pro
{
/** @var \App\Models\Customer|null */
$customer = Auth::guard('frontend')->user();
if ($customer and $customer->role->equals(CustomerTypeEnum::professional())) {
if ($customer and $customer->role->equals(CustomerTypeEnum::professional)) {
return $next($request);
}

View file

@ -0,0 +1,121 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Vite;
class SecureHeaders
{
/**
* Enumerate headers which you do not want in your application's responses.
* Great starting point would be to go check out @Scott_Helme's:
* https://securityheaders.com/
*
* @var array
*/
private $unwantedHeaderList = [
'X-Powered-By',
'Server',
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, \Closure $next): \Symfony\Component\HttpFoundation\Response
{
// * For Vite DEV
$host = \parse_url(\config('app.url'), \PHP_URL_HOST);
View::share('nonce', $nonce = Vite::useCspNonce());
// * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-{$nonce}' 'strict-dynamic'";
$scheme = config('app.env') === 'local' ? 'http' : 'https';
$reportUri = Route::has('cspReportUri') ?
\sprintf(' report-uri %s ;', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
$this->removeUnwantedHeaders($this->unwantedHeaderList);
$response = $next($request);
$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains;');
$cspPolicies = [
"default-src 'none';",
"manifest-src 'self';",
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob: " . implode(' ', [
// * VITE DEV
config('app.debug') ? "wss://{$host}:4443" : '',
config('app.debug') ? "https://{$host}:4443" : '',
'https://yl.s-web.fr'
]) . ';',
"img-src 'self' data: blob: " . implode(' ', [
// * VITE DEV
config('app.debug') ? "https://{$host}:4443" : '',
]) . ';',
"object-src 'self' " . implode(' ', []) . ';',
"frame-src 'self' " . implode(' ', [
'https://www.google.com/recaptcha/',
'https://recaptcha.google.com/recaptcha/',
'https://www.google.com/maps/',
'https://maps.google.com/',
'https://www.youtube.com',
'https://player.vimeo.com',
'blob:'
]) . ';',
"font-src 'self' data: " . \implode(' ', [
// * VITE DEV
config('app.debug') ? "https://{$host}:4443" : '',
'https://fonts.gstatic.com;'
]) . ';',
// ! Using CKEditor will must have unsafe-inline style
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
'https://cdnjs.cloudflare.com',
// * File Manager
'https://fonts.googleapis.com',
'https://cdn.jsdelivr.net',
'https://use.fontawesome.com',
'https://fonts.googleapis.com',
// * VITE DEV
config('app.debug') ? "https://{$host}:4443" : ''
]) . ';',
// ! If you dont use ckeditor use this
// "style-src 'self' 'nonce-$nonce';",
// phpcs:ignore Generic.Files.LineLength.TooLong
"script-src 'self' {$jsDev} " . \implode(' ', [
'https: blob:',
'https://www.googletagmanager.com',
'https://cdnjs.cloudflare.com',
"$scheme://www.google-analytics.com",
// * VITE DEV
config('app.debug') ? "https://{$host}:4443" : '',
]) . ';'
];
$response->headers->set(
'Content-Security-Policy',
implode(' ', $cspPolicies) . $reportUri
);
$response->headers->set('Feature-Policy', 'fullscreen=(self \'https://www.youtube.com\');');
return $response;
}
/**
* Remove unwanted headers.
*
* @param array $headerList
* @return void
*/
private function removeUnwantedHeaders(array $headerList): void
{
foreach ($headerList as $header) {
header_remove($header);
}
}
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Middleware;
use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
@ -10,14 +10,18 @@ class TrustProxies extends Middleware
/**
* The trusted proxies for this application.
*
* @var array|string|null
* @var array<int, string>|string|null
*/
protected $proxies;
protected $proxies = '*';
/**
* The headers that should be used to detect proxies.
*
* @var integer
*/
protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB;
protected $headers = Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\Http\Requests\Bo\StaticPages;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
class UpdateStaticPageRequest extends FormRequest
{
/**
* The model parameter name
*
* @var string
*/
protected $modelName = 'static_page';
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return true;
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'content' => Str::of($this->post('content'))->value()
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'seo_title' => 'required|min:45|max:70',
'seo_description' => 'required|min:50|max:160',
'title' => 'required|min:3|max:255',
'content' => 'present',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'seo_title' => trans('validation.custom.seo_title'),
'seo_description' => trans('validation.custom.seo_description'),
'title' => trans('validation.attributes.title'),
'content' => trans('Contenu'),
];
}
}

View file

@ -4,7 +4,7 @@ namespace App\Http\Requests\Bo;
use App\Enums\UserRoleEnum;
use Illuminate\Foundation\Http\FormRequest;
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
use Illuminate\Validation\Rules\Enum;
class StoreUserRequest extends FormRequest
{
@ -30,7 +30,7 @@ class StoreUserRequest extends FormRequest
'firstname' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
'role' => new EnumIsValidRule(UserRoleEnum::class)
'role' => new Enum(UserRoleEnum::class)
];
}
}

View file

@ -4,7 +4,7 @@ namespace App\Http\Requests\Bo;
use App\Enums\CustomerTypeEnum;
use Illuminate\Foundation\Http\FormRequest;
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
use Illuminate\Validation\Rules\Enum;
class UpdateCustomerRequest extends FormRequest
{
@ -32,7 +32,7 @@ class UpdateCustomerRequest extends FormRequest
'digits_between:1,16'
],
'phone' => 'nullable|string|max:15',
'role' => ['required', new EnumIsValidRule(CustomerTypeEnum::class)]
'role' => ['required', new Enum(CustomerTypeEnum::class)]
];
}
}

View file

@ -7,9 +7,8 @@ use App\Rules\Adult;
use BadMethodCallException;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Password;
use Kwaadpepper\Enum\Exceptions\EnumNotRoutableException;
use Kwaadpepper\Enum\Rules\EnumIsValidRule;
use LDC\LDCCRM\Enums\Country;
class CreateAccountRequest extends FormRequest
@ -66,10 +65,10 @@ class CreateAccountRequest extends FormRequest
$requiredIfRoleIsPro = 'sometimes';
try {
$role = $this->customer ? $this->customer->role : CustomerTypeEnum::make($this->role);
if ($role->equals(CustomerTypeEnum::professional())) {
if ($role->equals(CustomerTypeEnum::professional)) {
$requiredIfRoleIsPro = 'required';
}
} catch (BadMethodCallException | EnumNotRoutableException | \TypeError $e) {
} catch (BadMethodCallException | \TypeError $e) {
$requiredIfRoleIsPro = 'sometimes';
}
return [
@ -91,7 +90,7 @@ class CreateAccountRequest extends FormRequest
'opt_in' => 'required|boolean',
// * Spécific jeusselin
'role' => ['required', new EnumIsValidRule(CustomerTypeEnum::class)],
'role' => ['required', new Enum(CustomerTypeEnum::class)],
'social_denomination' => "{$requiredIfRoleIsPro}|max:255",
'tech_commercial' => "{$requiredIfRoleIsPro}|string|max:255",
@ -158,9 +157,9 @@ class CreateAccountRequest extends FormRequest
{
return [
'phone.phone' => trans('Le champ :attribute est invalide, il accepte les numéros des
pays suivants :countries.', ['countries' => Country::france()->label]),
pays suivants :countries.', ['countries' => Country::france->label()]),
'country.in' => trans('Le champ :attribute est invalide, il accepte les pays
suivants :countries.', ['countries' => Country::france()->label]),
suivants :countries.', ['countries' => Country::france->label()]),
'email.unique' => trans('Un compte existe déjà avec cette adresse.'),
'ok_cgv.accepted' => trans('Vous devez accepter les conditions d\'utilisation du site pour continuer.')
];

View file

@ -1,190 +0,0 @@
<?php
namespace App\Lib\Helpers;
class ExceptionsHelper
{
/**
* Handle sentry event
*
* @param \Sentry\Event $event
* @return null|\Sentry\Event
*/
public static function sentryEventHandler(\Sentry\Event $event): ?\Sentry\Event
{
// Remove unwanted reports.
$event->setModules([]);
$event->setOsContext(null);
/** @var \Sentry\ExceptionDataBag[] */
$exceptionBag = [];
foreach ($event->getExceptions() as $exceptionDataBag) {
$exceptionBag[] = self::getAnonymizedSentryException($exceptionDataBag);
}
$event->setExceptions($exceptionBag);
return $event;
}
/**
* Anonimize Sentry Exception bag
*
* @param \Sentry\ExceptionDataBag $exceptionDataBag
* @return \Sentry\ExceptionDataBag
*/
private static function getAnonymizedSentryException(
\Sentry\ExceptionDataBag $exceptionDataBag
): \Sentry\ExceptionDataBag {
$exceptionDataBag->setValue(self::getAnonymizedMessage($exceptionDataBag->getValue()));
if ($stackTrace = $exceptionDataBag->getStacktrace()) {
$exceptionDataBag->setStacktrace(self::getAnonymizedSentryStackTrace($stackTrace));
}
return $exceptionDataBag;
}
/**
* Anonymise sentry stacktrace
*
* @param \Sentry\Stacktrace $stackTrace
* @return \Sentry\Stacktrace
*/
private static function getAnonymizedSentryStackTrace(\Sentry\Stacktrace $stackTrace): \Sentry\Stacktrace
{
/** @var \Sentry\Frame[] */
$frames = \array_reverse($stackTrace->getFrames(), true);
$i = \count($frames) - 1;
// * Remove all but keep first (index.php).
while ($i-- > 0) {
$stackTrace->removeFrame(count($stackTrace->getFrames()) - 1);
}
foreach ($frames as $index => $frame) {
$frame->setPostContext([]);
$frame->setPreContext([]);
$frame->setContextLine('');
$frame->setVars(self::getAnonymizedSentryParams($frame->getVars()));
$stackTrace->addFrame($frame);
if ($index === 0) {
$stackTrace->removeFrame(count($stackTrace->getFrames()) - 1);
}
}
return $stackTrace;
}
/**
* Get anonymized sentry params
*
* @param mixed[] $params
* @return string[]
*/
private static function getAnonymizedSentryParams(array $params): array
{
foreach ($params as &$param) {
$type = \strtolower(\gettype($param));
if (\is_string($param)) {
$type = \explode(' ', \trim($param))[0] ?? null;
}
switch ($type) {
case 'Lambda':
case 'Object':
continue 2;
case 'array':
$param = sprintf('Array(%d)', \count($param));
continue 2;
default:
if (\is_numeric($param)) {
$param = \ucfirst($type);
continue 2;
}
if (\is_string($param)) {
$param = sprintf('String(%d)', \strlen($param));
continue 2;
}
} //end switch
} //end foreach
return $params;
}
/**
* Anonymize at least Sql queries
*
* @param string $message
* @return string
*/
public static function getAnonymizedMessage(string $message): string
{
$message = \preg_replace('/^(.*)( \(SQL:.*\))$/', '$1', $message) ?? $message;
if ($sqlPos = \strpos($message, 'to use near')) {
$message = \mb_strcut($message, 0, $sqlPos);
}
return $message;
}
/**
* Anonymize stack trace
*
* @param \Throwable $e
* @return string
*/
public static function getAnonymizedStackTrace(\Throwable $e): string
{
$projectPath = \realpath(\sprintf('%s/../', \app_path()));
$traceStack = $e->getTrace();
$traceStr = '';
$maxStackStage = 0;
foreach ($traceStack as $stackStage => $stack) {
$maxStackStage = $stackStage;
$traceStr .= \sprintf(
'#%s %s(%d): ',
$stackStage,
str_replace($projectPath, '', $stack['file'] ?? ''),
$stack['line'] ?? ''
);
if (isset($stack['class'])) {
$traceStr .= "{$stack['class']}->{$stack['function']}(";
if (isset($stack['args'])) {
$traceStr .= self::handleArgs($stack);
}
$traceStr .= ")\n";
} elseif (isset($stack['function'])) {
$traceStr .= "{$stack['function']}(";
if (isset($stack['args'])) {
$traceStr .= self::handleArgs($stack);
}
$traceStr .= ")\n";
}
} //end foreach
$maxStackStage++;
$traceStr .= "#$maxStackStage {main}";
return $traceStr;
}
/**
* Handle Args
*
* @param array $stack
* @return string
*/
public static function handleArgs(array $stack): string
{
$traceArgs = '';
$argsCount = count($stack['args']);
for ($i = 0; $i < $argsCount; $i++) {
$type = \gettype($stack['args'][$i]);
switch ($type) {
case 'object':
$traceArgs .= sprintf('Object(%s)', \get_class($stack['args'][$i]));
break;
case 'array':
$traceArgs .= sprintf('Array(%d)', \count($stack['args'][$i]));
break;
case 'string':
$traceArgs .= sprintf('String(%d)', \strlen($stack['args'][$i]));
break;
default:
$traceArgs .= \gettype($stack['args'][$i]);
}
$traceArgs .= ($i !== ($argsCount - 1) ? ',' : '');
}
return $traceArgs;
}
}

View file

@ -4,6 +4,7 @@ namespace App\Lib\LDC;
use GuzzleHttp\Client;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Log;
use Psr\Http\Message\ResponseInterface;
class MailjetSms
@ -56,8 +57,8 @@ class MailjetSms
]
]);
} catch (\GuzzleHttp\Exception\RequestException $ex) {
\Log::debug('error');
\Log::debug((string)$ex->getResponse()->getBody());
Log::debug('error');
Log::debug((string)$ex->getResponse()->getBody());
throw $ex;
}

View file

@ -31,6 +31,8 @@ class AlertGoalAchieved extends Mailable
public function build()
{
/** @var App\Models\Objectives $this->objectif */
return $this->subject("Objectif Atteint! - " . config('app.name', 'Jeusselin'))->view('fo.mail.alertgoalachieved')->with('objectif', $this->objectif);
return $this->subject("Objectif Atteint! - " . config('app.name', 'Jeusselin'))
->view('fo.mail.alertgoalachieved')
->with('objectif', $this->objectif);
}
}

View file

@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Validation\ValidationException;
use Kwaadpepper\Enum\Traits\CastsEnums;
use LDC\LDCCRM\Enums\ApiError;
use LDC\LDCCRM\Exceptions\ApiErrorException;
use LDC\LDCCRM\Exceptions\CrmException;
@ -56,7 +55,6 @@ use LDC\LDCCRM\Models\Customer as CrmCustomer;
*/
class Customer extends Authenticatable implements MustVerifyEmail, CanResetPassword
{
use CastsEnums;
use HasFactory;
use Notifiable;
@ -114,14 +112,6 @@ class Customer extends Authenticatable implements MustVerifyEmail, CanResetPassw
'ok_cgv' => 'boolean',
'last_connection' => 'datetime',
'email_verified_at' => 'datetime',
];
/**
* Map attribute names to enum classes.
*
* @var array<string,string>
*/
protected $enumCasts = [
'role' => CustomerTypeEnum::class
];
@ -158,7 +148,7 @@ class Customer extends Authenticatable implements MustVerifyEmail, CanResetPassw
return $result;
} catch (ApiErrorException $e) {
if ($e->getApiError()->equals(ApiError::customerAlreadyActive())) {
if ($e->getApiError()->equals(ApiError::customerAlreadyActive)) {
return true && parent::markEmailAsVerified();
}
throw $e;
@ -221,7 +211,7 @@ class Customer extends Authenticatable implements MustVerifyEmail, CanResetPassw
*/
public function getIsValidatedProAttribute(): bool
{
return $this->role->equals(CustomerTypeEnum::professional()) and !empty($this->client_number);
return $this->role->equals(CustomerTypeEnum::professional) and !empty($this->client_number);
}
/**
@ -239,7 +229,7 @@ class Customer extends Authenticatable implements MustVerifyEmail, CanResetPassw
{
// phpcs:enable
if (!$this->crmCustomer) {
/** @var \LDC\LDCCRM\CrmApiV2 */
/** @var \LDC\LDCCRM\CrmApiV4 */
$crm = app('crmAPI');
$this->crmCustomer = $crm->getCustomer(\strval($this->hashcode));
if (!$this->crmCustomer) {
@ -266,7 +256,7 @@ class Customer extends Authenticatable implements MustVerifyEmail, CanResetPassw
$data = $resource->toArray(null);
if (!$this->crmCustomer) {
/** @var \LDC\LDCCRM\CrmApiV2 */
/** @var \LDC\LDCCRM\CrmApiV4 */
$crm = app('crmAPI');
$this->crmCustomer = $crm->getCustomer(\strval($this->hashcode));
if (!$this->crmCustomer) {

147
app/Models/StaticPage.php Normal file
View file

@ -0,0 +1,147 @@
<?php
namespace App\Models;
use App\Enums\StaticPageType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\ToolboxHelper;
/**
* @property \App\Enums\StaticPageType $type Page type (StaticPageEnum).
* @property string $seo_title Seo title about 70 chars as recommended.
* @property string $seo_description Seo description about 160 chars as recommended.
* @property string $title Title for the page.
* @property string $slug Slug of the title for the page.
* @property array $data MINICRUD - Page content.
* @property-read \Illuminate\Support\Carbon $created_at Created date.
* @property-read \Illuminate\Support\Carbon $updated_at Updated date.
*
* @property-read \Illuminate\Database\Eloquent\Relations\MorphMany $sections Get Sections of the static page.
*/
class StaticPage extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'seo_title',
'seo_description',
'title',
'content',
'type',
'slug',
'created_at',
'updated_at'
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'type' => StaticPageType::class,
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::creating(function (self $model) {
// * ASSERTIONS
self::assertTitleIsUnique($model->title);
// * CHANGES
$model->slug = Str::slug($model->title);
});
static::updating(function (self $model) {
// * ASSERTIONS
self::assertTitleIsUnique($model->title, $model->id);
// * CHANGES
$model->slug = Str::slug($model->title);
self::resetCache($model->type);
});
}
// * ROUTABLE
/**
* Get the value of the model's route key.
*
* @return mixed
*/
public function getRouteKey()
{
/** @var \App\Enums\StaticPageType */
$enum = $this->getAttribute($this->getRouteKeyName());
return $enum->value();
}
/**
* Get the route key for the model.
*
* ! Type is the routable parameter
*
* @return string
*/
public function getRouteKeyName(): string
{
return 'type';
}
// * METHODS
/**
* Asserts using validation that the title is unique
*
* @param string|null $title
* @param integer|null $id
* @return void
* @throws \Illuminate\Validation\ValidationException If an actu title already exists.
*/
private static function assertTitleIsUnique(string|null $title, ?int $id = null)
{
$table = (new self())->getTable();
ToolboxHelper::assertFieldIsUnique($table, 'title', $title, $id);
ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($title), $id);
}
/**
* Reset all cache elements related to this model.
*
* @param \App\Enums\StaticPageType $type
* @return void
*/
private static function resetCache(StaticPageType $type): void
{
Cache::forget(sprintf('%sStaticPage', $type->name));
self::getCachedStaticPage($type);
}
/**
* Get cached static page associated to a given type.
*
* @param \App\Enums\StaticPageType $type
* @return \App\Models\StaticPage
*/
public static function getCachedStaticPage(StaticPageType $type): \App\Models\StaticPage
{
return Cache::rememberForever(
sprintf('%sStaticPage', $type->name),
fn() => self::query()->where('type', $type)->first()
);
}
}

View file

@ -9,7 +9,6 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Kwaadpepper\Enum\Traits\CastsEnums;
/**
* @property string $hashcode
@ -27,8 +26,6 @@ use Kwaadpepper\Enum\Traits\CastsEnums;
*/
class User extends Authenticatable
{
use CanResetPassword;
use CastsEnums;
use HasFactory;
use Notifiable;
@ -41,27 +38,27 @@ class User extends Authenticatable
'firstname',
'lastname',
'email',
'password',
'role'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'last_connection' => 'datetime',
'role' => UserRoleEnum::class
];
/**
* The attributes that should be hidden for arrays.
*
* @var array<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Map attribute names to enum classes.
*
* @var array<string,string>
*/
protected $enumCasts = [
'role' => UserRoleEnum::class
'role' => UserRoleEnum::class,
];
/**
@ -72,38 +69,5 @@ class User extends Authenticatable
protected static function booted()
{
// * EVENT CLOSURES.
static::creating(function (User $user) {
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($user->password);
}
});
static::updating(function (User $user) {
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($user->password);
}
});
}
/**
* Send a password reset notification to the user.
*
* @param string $token
* @return void
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
*/
public function sendPasswordResetNotification($token)
{
// phpcs:enable
$notification = new ResetPasswordNotification($token);
// * Changed route here only
$notification->createUrlUsing(function ($notifiable, $token) {
return url(route('bo.password.reset', [
'token' => $token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
});
$this->notify($notification);
}
}

View file

@ -4,7 +4,7 @@ namespace App\Providers;
use Illuminate\Support\Facades\App;
use Illuminate\Support\ServiceProvider;
use LDC\LDCCRM\CrmApiV2;
use LDC\LDCCRM\CrmApiV4;
class CrmApiServiceProvider extends ServiceProvider
{
@ -16,7 +16,7 @@ class CrmApiServiceProvider extends ServiceProvider
public function boot()
{
App::singleton('crmAPI', function () {
return new CrmApiV2(
return new CrmApiV4(
config('crm.endpoint'),
config('crm.token'),
config('crm.password')

View file

@ -2,9 +2,15 @@
namespace App\Providers;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Session;
use Slides\Saml2\Events\SignedIn;
class EventServiceProvider extends ServiceProvider
{
@ -19,6 +25,17 @@ class EventServiceProvider extends ServiceProvider
],
];
/**
* Determine if events and listeners should be automatically discovered.
*
* @return boolean
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
/**
* Register any events for your application.
*
@ -26,5 +43,37 @@ class EventServiceProvider extends ServiceProvider
*/
public function boot()
{
Event::listen('Slides\Saml2\Events\SignedIn', function (SignedIn $event) {
$messageId = $event->getAuth()->getLastMessageId();
if (Cache::get(config('cache.prefix') . "_saml_message_id_" . $messageId)) {
logger()->error(
'SAML MESSAGE ID HANDLER',
['error' => 'Message ID : ' . $messageId . " replayed !"]
);
abort(403);
} else {
// Cache for 8h.
Cache::put(config('cache.prefix') . "_saml_message_id_" . $messageId, $messageId, 28800);
}
$user = $event->getSaml2User();
$email = $user->getNameId();
$lUser = User::query()->where('email', $email)->first();
if (!$lUser) {
abort(403);
}
Auth::guard('backend')->login($lUser);
session(['sessionIndex' => $user->getSessionIndex()]);
$lUser->last_connection = now();
$lUser->saveQuietly();
});
Event::listen('Slides\Saml2\Events\SignedOut', function () {
Auth::guard('backend')->logout();
Session::save();
});
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
class ViewServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
// * FORCE BOOTSTRAP PAGINATOR (or custom if in front)
if (!app()->runningInConsole()) {
// Early boot, request wont be filled as expected.
Paginator::defaultView(
(collect(explode('/', \request()->getPathInfo()))->get(1) === 'bo')
? 'bo.modules.pagination'
: 'fo.modules.pagination'
);
}
}
}

View file

@ -42,7 +42,7 @@ class CrmCustomerPassword implements Rule
try {
return Customer::login($this->crmCustomer->email, strval($value));
} catch (ApiErrorException $e) {
if ($e->getApiError()->equals(ApiError::authFailed())) {
if ($e->getApiError()->equals(ApiError::authFailed)) {
return false;
}
throw $e;

View file

@ -8,29 +8,28 @@
],
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"dxw/cidr": "^3.1",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"kwaadpepper/enum": "^2.1",
"laravel/framework": "^8.12",
"php": "^8.4",
"24slides/laravel-saml2": "^2.4",
"guzzlehttp/guzzle": "^7.5.0",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.5",
"laravel/ui": "^3.4",
"ldc/ldccrmv2client": "^3.0",
"ldc/sitemap-generator": "^1.0",
"sentry/sentry-laravel": "^3.8",
"spatie/laravel-sitemap": "^5.8",
"uwil/k8s-laravel": "^1.2"
"laravel/ui": "^4.0",
"ldc/ldccrmv4client": "^4.1",
"sentry/sentry-laravel": "*",
"spatie/laravel-ignition": "^2.11",
"uwil/admin-secure-laravel": "^5.0",
"uwil/k8s-laravel": "^2.0",
"uwil/laravel-common": "^4.1",
"uwil/sitemap-generator": "^5.0"
},
"require-dev": {
"doctrine/dbal": "^3.0",
"facade/ignition": "^2.5",
"doctrine/dbal": "^3.3",
"fakerphp/faker": "^1.9.1",
"laravel/sail": "^1.0.1",
"mockery/mockery": "^1.4.2",
"nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.3.3",
"larastan/larastan": "^3.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.0",
"slevomat/coding-standard": "^8.13"
},
"repositories": {

7524
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Facades\Facade;
return [
/*
@ -180,7 +182,7 @@ return [
App\Providers\CrmApiServiceProvider::class,
App\Providers\MailjetSmsServiceProvider::class,
App\Providers\TranslationServiceProvider::class,
App\Providers\ViewServiceProvider::class,
],
/*
@ -194,44 +196,8 @@ return [
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
// 'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class
],
'aliases' => Facade::defaultAliases()->merge([
'Carbon' => \Illuminate\Support\Carbon::class,
])->toArray(),
];

View file

@ -15,7 +15,7 @@ return [
'defaults' => [
'guard' => 'frontend',
'passwords' => 'users',
'passwords' => 'customers',
],
/*

132
config/purifier.php Normal file
View file

@ -0,0 +1,132 @@
<?php
/**
* Ok, glad you are here
* first we get a config instance, and set the settings
* $config = HTMLPurifier_Config::createDefault();
* $config->set('Core.Encoding', $this->config->get('purifier.encoding'));
* $config->set('Cache.SerializerPath', $this->config->get('purifier.cachePath'));
* if ( ! $this->config->get('purifier.finalize')) {
* $config->autoFinalize = false;
* }
* $config->load [$this->getConfig());
*
* You must NOT delete the default settings
* anything in settings should be compacted with params that needed to instance HTMLPurifier_Config.
*
* @link http://htmlpurifier.org/live/configdoc/plain.html
*/
// phpcs:disable Generic.Files.LineLength.TooLong
return [
'encoding' => 'UTF-8',
'finalize' => true,
'cachePath' => storage_path('app/purifier'),
'cacheFileMode' => 0755,
'settings' => [
'default' => [
'HTML.SafeIframe' => true,
// * For iframes
'URI.SafeIframeRegexp' => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/|www.dailymotion.com/video/)%",
'HTML.Doctype' => 'HTML 4.01 Transitional',
// * Legacy filter en full avec safe legacy.
'HTML.Allowed' => 'h1[style|id],h2[style|id],h3[style|id],h4[style|id],h5[style|id],h6[style|id],figure[class|style],oembed[url],div[style],b,strong,i,em,u,s,pre,code[class],a[href|title|target|rel|id|class],ul[style],ol[style],dl[style],li,p[style],br,span[style|class],img[width|height|alt|src|style|class|title],iframe[src|width|height|class|frameborder|style],table[style|align|border|cellspacing|cellpadding|width],thead,tbody,th[colspan|align|valign|style],tr[align|valign|style],td[style|colspan|rowspan|scope|align|valign],hr,dl,dt,mark[class],blockquote',
// * Restricted basics.
// 'HTML.Allowed' => 'br,p[style],b[style],u[style],i[style],a[href|title|target|rel],strong[style],span[class],ul[style],ol[style],li[style],h2[style],h3[style],h4[style],hr,blockquote[style]',
// * Legacy filter en full avec safe legacy.
'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align,list-style-type,float,width,height,vertical-align,border,border-color,margin-left',
// * Restricted basics.
// 'CSS.AllowedProperties' => 'text-align,margin-left',
'AutoFormat.AutoParagraph' => false,
'AutoFormat.RemoveEmpty' => false,
'AutoFormat.RemoveEmpty.RemoveNbsp' => false,
],
"youtube" => [
"HTML.SafeIframe" => 'true',
"URI.SafeIframeRegexp" => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/|www.dailymotion.com/video/)%",
],
'custom_definition' => [
'id' => 'html5-definitions',
'rev' => 1,
'debug' => true,
'elements' => [
// * http://developers.whatwg.org/sections.html
['section', 'Block', 'Flow', 'Common'],
['nav', 'Block', 'Flow', 'Common'],
['article', 'Block', 'Flow', 'Common'],
['aside', 'Block', 'Flow', 'Common'],
['header', 'Block', 'Flow', 'Common'],
['footer', 'Block', 'Flow', 'Common'],
// * Content model actually excludes several tags, not modelled here
['address', 'Block', 'Flow', 'Common'],
['hgroup', 'Block', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common'],
// * http://developers.whatwg.org/grouping-content.html
['figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common'],
['figcaption', 'Inline', 'Flow', 'Common'],
['oembed', 'Inline', 'Flow', 'Common'],
// * http://developers.whatwg.org/the-video-element.html#the-video-element
['video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
'src' => 'URI',
'type' => 'Text',
'width' => 'Length',
'height' => 'Length',
'poster' => 'URI',
'preload' => 'Enum#auto,metadata,none',
'controls' => 'Bool',
]],
['source', 'Block', 'Flow', 'Common', [
'src' => 'URI',
'type' => 'Text',
]],
// * http://developers.whatwg.org/text-level-semantics.html
['s', 'Inline', 'Inline', 'Common'],
['var', 'Inline', 'Inline', 'Common'],
['sub', 'Inline', 'Inline', 'Common'],
['sup', 'Inline', 'Inline', 'Common'],
['mark', 'Inline', 'Inline', 'Common'],
['wbr', 'Inline', 'Empty', 'Core'],
// * http://developers.whatwg.org/edits.html
['ins', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
['del', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
],
'attributes' => [
['iframe', 'allowfullscreen', 'Bool'],
['table', 'height', 'Text'],
['td', 'border', 'Text'],
['th', 'border', 'Text'],
['tr', 'width', 'Text'],
['tr', 'height', 'Text'],
['tr', 'border', 'Text'],
['figure', 'class', 'Text'],
['figure', 'style', 'Text'],
['oembed', 'url', 'Text'],
['span', 'style', 'Text'],
['img', 'style', 'Text'],
],
],
'custom_attributes' => [
['a', 'target', 'Enum#_blank,_self,_target,_top'],
['a', 'id', 'Text'],
['ol', 'style', 'Text'],
['figure', 'style', 'Text'],
['img', 'style', 'Text'],
['span', 'style', 'Text'],
['oembed', 'url', 'Text'],
['h1', 'id', 'Text'],
['h2', 'id', 'Text'],
['h3', 'id', 'Text'],
['h4', 'id', 'Text'],
['h5', 'id', 'Text'],
['h6', 'id', 'Text'],
],
'custom_elements' => [
['u', 'Inline', 'Inline', 'Common'],
['oembed', 'Inline', 'Inline', 'Common'],
],
],
];

397
config/saml2.php Normal file
View file

@ -0,0 +1,397 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Tenant Model
|--------------------------------------------------------------------------
|
| This will allow you to override the tenant model with your own.
|
*/
'tenantModel' => \Slides\Saml2\Models\Tenant::class,
/*
|--------------------------------------------------------------------------
| Use built-in routes
|--------------------------------------------------------------------------
|
| If "useRoutes" set to true, the package defines five new routes:
|
| Method | URI | Name
| -------|---------------------------------|------------------
| POST | {routesPrefix}/{uuid}/acs | saml.acs
| GET | {routesPrefix}/{uuid}/login | saml.login
| GET | {routesPrefix}/{uuid}/logout | saml.logout
| GET | {routesPrefix}/{uuid}/metadata | saml.metadata
| GET | {routesPrefix}/{uuid}/sls | saml.sls
|
*/
'useRoutes' => true,
/*
|--------------------------------------------------------------------------
| Built-in routes prefix
|--------------------------------------------------------------------------
|
| Here you may define the prefix for built-in routes.
|
*/
'routesPrefix' => '/saml2',
/*
|--------------------------------------------------------------------------
| Middle groups to use for the SAML routes
|--------------------------------------------------------------------------
|
| Note, Laravel 5.2 requires a group which includes StartSession
|
*/
'routesMiddleware' => ['saml'],
/*
|--------------------------------------------------------------------------
| Signature validation
|--------------------------------------------------------------------------
|
| Set to true if you want to use parameters from $_SERVER to validate the signature.
|
*/
'retrieveParametersFromServer' => false,
/*
|--------------------------------------------------------------------------
| Login redirection URL.
|--------------------------------------------------------------------------
|
| The redirection URL after successful login.
|
*/
'loginRoute' => '/bo',
/*
|--------------------------------------------------------------------------
| Logout redirection URL.
|--------------------------------------------------------------------------
|
| The redirection URL after successful logout.
|
*/
'logoutRoute' => '/bo',
/*
|--------------------------------------------------------------------------
| Login error redirection URL.
|--------------------------------------------------------------------------
|
| The redirection URL after login failing.
|
*/
'errorRoute' => '/bo/login-error',
/*
|--------------------------------------------------------------------------
| Strict mode.
|--------------------------------------------------------------------------
|
| If 'strict' is True, then the PHP Toolkit will reject unsigned
| or unencrypted messages if it expects them signed or encrypted
| Also will reject the messages if not strictly follow the SAML
| standard: Destination, NameId, Conditions... are validated too.
|
*/
'strict' => true,
/*
|--------------------------------------------------------------------------
| Debug mode.
|--------------------------------------------------------------------------
|
| When enabled, errors must be printed.
|
*/
'debug' => env('SAML2_DEBUG', env('APP_DEBUG', false)),
/*
|--------------------------------------------------------------------------
| Whether to use `X-Forwarded-*` headers to determine port/domain/protocol.
|--------------------------------------------------------------------------
|
| If 'proxyVars' is True, then the Saml lib will trust proxy headers
| e.g X-Forwarded-Proto / HTTP_X_FORWARDED_PROTO. This is useful if
| your application is running behind a load balancer which terminates SSL.
|
*/
'proxyVars' => true,
/*
|--------------------------------------------------------------------------
| Service Provider configuration.
|--------------------------------------------------------------------------
|
| General setting of the service provider.
|
*/
'sp' => [
/*
|--------------------------------------------------------------------------
| NameID format.
|--------------------------------------------------------------------------
|
| Specifies constraints on the name identifier to be used to
| represent the requested subject.
|
*/
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
/*
|--------------------------------------------------------------------------
| SP Certificates.
|--------------------------------------------------------------------------
|
| Usually x509cert and privateKey of the SP are provided by files placed at
| the certs folder. But we can also provide them with the following parameters.
|
*/
'x509cert' => env('SAML2_SP_CERT_x509', ''),
'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY', ''),
/*
|--------------------------------------------------------------------------
| Identifier (URI) of the SP entity.
|--------------------------------------------------------------------------
|
| Leave blank to use the 'saml.metadata' route.
|
*/
'entityId' => env('SAML2_SP_ENTITYID', ''),
/*
|--------------------------------------------------------------------------
| The Assertion Consumer Service (ACS) URL.
|--------------------------------------------------------------------------
|
| URL Location where the <Response> from the IdP will be returned, using HTTP-POST binding.
| Leave blank to use the 'saml.acs' route.
|
*/
'assertionConsumerService' => [
'url' => '',
],
/*
|--------------------------------------------------------------------------
| The Single Logout Service URL.
|--------------------------------------------------------------------------
|
| Specifies info about where and how the <Logout Response> message MUST be
| returned to the requester, in this case our SP.
|
| URL Location where the <Response> from the IdP will be returned, using HTTP-Redirect binding.
| Leave blank to use the 'saml.sls' route.
|
*/
'singleLogoutService' => [
'url' => ''
],
],
/*
|--------------------------------------------------------------------------
| OneLogin security settings.
|--------------------------------------------------------------------------
|
|
|
*/
'security' => [
'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256',
/*
|--------------------------------------------------------------------------
| NameId encryption
|--------------------------------------------------------------------------
|
| Indicates that the nameID of the <samlp:logoutRequest> sent by this SP
| will be encrypted.
|
*/
'nameIdEncrypted' => true,
/*
|--------------------------------------------------------------------------
| AuthnRequest signage
|--------------------------------------------------------------------------
|
| Indicates whether the <samlp:AuthnRequest> messages sent by
| this SP will be signed. The Metadata of the SP will offer this info
|
*/
'authnRequestsSigned' => true,
/*
|--------------------------------------------------------------------------
| Logout request signage
|--------------------------------------------------------------------------
|
| Indicates whether the <samlp:logoutRequest> messages sent by this SP
| will be signed.
|
*/
'logoutRequestSigned' => true,
/*
|--------------------------------------------------------------------------
| Logout response signage
|--------------------------------------------------------------------------
|
| Indicates whether the <samlp:logoutResponse> messages sent by this SP
| will be signed.
|
*/
'logoutResponseSigned' => true,
/*
|--------------------------------------------------------------------------
| Whether need to sign metadata.
|--------------------------------------------------------------------------
|
| The possible values:
| - false
| - true (use certs)
| - array:
| ```
| [
| 'keyFileName' => 'metadata.key',
| 'certFileName' => 'metadata.crt'
| ]
| ```
|
*/
'signMetadata' => true,
/*
|--------------------------------------------------------------------------
| Requirement to sign messages.
|--------------------------------------------------------------------------
|
| Indicates a requirement for the <samlp:Response>, <samlp:LogoutRequest> and
| <samlp:LogoutResponse> elements received by this SP to be signed.
|
*/
'wantMessagesSigned' => true,
/*
|--------------------------------------------------------------------------
| Requirement to sign assertion elements.
|--------------------------------------------------------------------------
|
| Indicates a requirement for the <saml:Assertion> elements received by
| this SP to be signed.
|
*/
'wantAssertionsSigned' => true,
/*
|--------------------------------------------------------------------------
| Requirement to encrypt NameID.
|--------------------------------------------------------------------------
|
| Indicates a requirement for the NameID received by this SP to be encrypted.
|
*/
'wantNameIdEncrypted' => false,
/*
|--------------------------------------------------------------------------
| Authentication context.
|--------------------------------------------------------------------------
|
| Set to false and no AuthContext will be sent in the AuthNRequest,
|
| Set true or don't present this parameter and you will get an
| AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
|
| Set an array with the possible auth context values:
| ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509']
|
*/
'requestedAuthnContext' => true,
],
/*
|--------------------------------------------------------------------------
| Contact information.
|--------------------------------------------------------------------------
|
| It is recommended to supply a technical and support contacts.
|
*/
'contactPerson' => [
'technical' => [
'givenName' => env('SAML2_CONTACT_TECHNICAL_NAME', env('APP_NAME', 'Technical')),
'emailAddress' => env('SAML2_CONTACT_TECHNICAL_EMAIL', 'no@reply.com')
],
'support' => [
'givenName' => env('SAML2_CONTACT_SUPPORT_NAME', env('APP_NAME', 'Support')),
'emailAddress' => env('SAML2_CONTACT_SUPPORT_EMAIL', 'no@reply.com')
],
],
/*
|--------------------------------------------------------------------------
| Organization information.
|--------------------------------------------------------------------------
|
| The info in en_US lang is recommended, add more if required.
|
*/
'organization' => [
'en-US' => [
'name' => env('SAML2_ORGANIZATION_NAME', env('APP_NAME', 'Name')),
'displayname' => env('SAML2_ORGANIZATION_NAME', env('APP_NAME', 'Name')),
'url' => env('SAML2_ORGANIZATION_URL', env('APP_URL', 'http://url'))
],
],
/**
* Default tenant UUID
*/
'default_tenant_uuid' => env('SAML2_DEFAULT_TENANT_UUID', null)
];

View file

@ -57,5 +57,5 @@ return [
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate.
'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_TRACES_SAMPLE_RATE'),
'before_send' => [\App\Lib\Helpers\ExceptionsHelper::class, 'sentryEventHandler'],
'before_send' => [\Uwil\LaravelCommon\Helpers\ExceptionsHelper::class, 'sentryEventHandler'],
];

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('password');
$table->date('last_connection')->nullable()->after('remember_token');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('password')->after('email');
$table->dropColumn('last_connection');
});
}
};

View file

@ -0,0 +1,418 @@
<?php
use App\Enums\StaticPageType;
use App\Models\StaticPage;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('static_pages', function (Blueprint $table) {
$table->id();
$table->unsignedTinyInteger('type')->unique()->comment('Page type (StaticPageEnum)');
$table->string('seo_title')->comment('Seo title about 70 chars as recommended');
$table->string('seo_description')->comment('Seo description about 160 chars as recommended');
$table->string('title')->comment('Title for the page');
$table->string('slug')->unique()->comment('Unique title slug');
$table->text('content')->comment('WYSIWYG text content using ckeditor');
$table->timestamps();
});
StaticPage::query()->updateOrCreate(
[
'type' => StaticPageType::legals,
],
[
'seo_title' => 'Mentions légales et CGU',
'seo_description' => 'Mentions légales et CGU du site Jeusselin.fr ',
'title' => 'Mentions légales et CGU',
'type' => StaticPageType::legals,
'content' => '
<h1 class="post-title">
CONDITIONS GENERALES DE VENTE SAS JEUSSELIN
</h1>
<p>
Les présentes conditions générales de vente s\'appliquent à toutes les commandes passées auprès de la SAS Jeusselin (ci-après dénommée le « Vendeur » ou « notre Société ») par ses Clients (ci-après dénommé le « Client »).<br>
En conséquence, toute commande passée au Vendeur implique nécessairement, à titre de condition essentielle et déterminante, l\'acceptation entière et sans réserve par le Client des dites Conditions générales de vente, qui constituent le socle unique de la négociation commerciale, conformément aux dispositions de l\'article L. 441-6 du Code de commerce.<br>
Toute condition contraire et notamment toutes conditions générales ou particulières émanant du Client, y compris ses éventuelles conditions d\'achat et ses bons de commande, sont en conséquence inopposables au Vendeur, sauf acceptation préalable et écrite.<br>
Le fait de ne pas se prévaloir, à un moment donné, de l\'une quelconque des présentes Conditions générales de vente ne peut être interprété par le Client comme valant renonciation par le Vendeur à se prévaloir ultérieurement à l\'une quelconque des dites conditions.<br>
Les présentes Conditions générales de vente sont modifiables à tout moment, étant entendu que toute éventuelle modification sera portée à la connaissance du Client et qu\'elle prendra effet huit jours après sa diffusion.
</p>
<h2 class="post-chapter">
COMMANDES
</h2>
<p>
Tout Client réalisant une première opération avec notre Société devra ouvrir un compte à cet effet pour la bonne validité de la commande.<br>
Les commandes qui sont adressées à notre Société directement par le Client ou qui sont transmises aux agents ou représentants ne lient notre Société que lorsqu\'elles ont été acceptées par elle.
En cas de modification de la commande par le Client, après acceptation du Vendeur, le Vendeur ne sera pas tenu par les délais initialement convenus.<br>
Dans le cas où un Client passe une commande au Vendeur, sans avoir procédé au paiement des livraisons précédentes, le Vendeur pourra refuser d\'honorer la commande et pourra suspendre ou annuler les commandes non encore livrées, sans que le Client puisse prétendre à une quelconque indemnité, pour quelque raison que ce soit. Cette faculté vaut également au cas où le Client ne peut présenter des garanties de solvabilité acceptées par le Vendeur.
</p>
<h2 class="post-chapter">
CONDITIONS ET INFORMATIONS RELATIVES AUX PRODUITS PHYTOPHARMACEUTIQUES : Agrément N° PL00039
</h2>
<p>
Les présentes conditions générales de vente s\'appliquent également à la vente de produits phytopharmaceutiques.<br>
Les commandes de produits phytopharmaceutiques sont faites conformément à la réglementation spécifique à ces produits. Ainsi la vente de produits phytopharmaceutiques à usage professionnel est réservée aux clients attestant de leur qualité d\'utilisateurs professionnels par la présentation des justificatifs visés par l\'arrêté du 6 janvier 2016 relatif aux justificatifs requis pour l\'achat des produits phytopharmaceutiques de la gamme d\'usages « professionnel » et aux personnes pour le compte desquelles des utilisateurs professionnels vont utiliser ces produits sous réserve des conditions et de la présentation des justificatifs prévus à cet effet par l\'arrêté susvisé.<br>
Les fiches de données de sécurité (FDS) des produits phytopharmaceutiques vendus par l\'entreprise sont consultables sur www.quickfds.com ou peuvent être demandées à notre Société.<br>
Le Client est averti que tout utilisateur d\'un produit phytopharmaceutique doit lire l\'étiquette du produit et la fiche de données de sécurité avant son utilisation. L\'utilisateur doit veiller aux conditions d\'emploi des produits et au port des équipements de sécurité. L\'utilisateur s\'engage à respecter la gestion des emballages vides (EVPP) et des produits phytopharmaceutiques non utilisables (PPNU).
</p>
<h2 class="post-chapter">
LIVRAISONS
</h2>
<p>
Les délais de livraison et de transport prévus à la commande sont donnés à titre indicatif.<br>
Les retards éventuels liés à un cas de force majeure ne donnent pas droit au Client d\'annuler la vente, de refuser la marchandise ou de réclamer des dommages et intérêts.<br>
Le transfert des risques sur les marchandises vendues par notre Société s\'effectue à la remise des marchandises au transporteur ou à la sortie de nos locaux.<br>
Il en résulte que les marchandises voyagent aux risques et périls du Client, auquel cas il lui appartient, en cas d\'avarie, de perte ou de manquant, de faire toute réserve ou d\'exercer tout recours auprès des transporteurs responsables conformément à l\'article L. 133-3 du Code de commerce (voir aussi la rubrique Réclamations).<br>
Il est en outre précisé que, dans l\'hypothèse où le transport est directement effectué par le Vendeur, le transfert des risques interviendra lors de la livraison des marchandises au Client qui devra être présent ou avoir donné tous pouvoirs à un préposé pour prendre livraison des dites marchandises et donc procéder à toutes réserves jugées nécessaires.<br>
Par dérogation à la rubrique « Réclamations / Contestations commerciales », en cas d\'absence, toute réserve sur la réalité d\'une livraison doit intervenir par écrit dans les 48 heures suivant la réception du bon de livraison. A défaut, la livraison est considérée comme non contestée.
</p>
<h2 class="post-chapter">
CONDITIONS PARTICULIERES POUR LES PRODUITS PHYTOPHARMACEUTIQUES
</h2>
<p>
La délivrance de produits phytopharmaceutiques n\'est autorisée qu\'aux utilisateurs professionnels dans les conditions et par la présentation des références citées ci-dessus.<br>
Le Client utilisateur professionnel peut, en cas d\'absence, donner une délégation d\'approvisionnement au dépôt chez le Vendeur ou de réception d\'une livraison sur site. Ces délégations devront être faites par écrit sur la demande d\'ouverture de compte, avant l\'enlèvement ou la livraison.<br>
Le Client ou la personne déléguée est tenu d\'être présent(e) au moment de la livraison sur site.<br>
Un lieu devra être désigné pour la livraison des produits avant la livraison. En cas de modification, le Client s\'engage à en informer le Vendeur.
</p>
<h2 class="post-chapter">
CONDITIONS PARTICULIERES POUR LES ENGRAIS CONTENANT DU NITRATE D\'AMMONIUM
</h2>
<p>
La délivrance d\'engrais contenant du nitrate d\'ammonium et dont la teneur en azote est supérieure à 16% n\'est autorisée qu\'aux utilisateurs professionnels dans les conditions et par la présentation des références citées ci-dessous.<br>
Le Client utilisateur professionnel devra démontrer sa qualité d\'utilisateur professionnel.<br>
Il pourra, en cas d\'absence, donner une délégation d\'approvisionnement au dépôt chez le Vendeur ou de réception d\'une livraison sur site. Ces délégations devront être faites par écrit sur la demande d\'ouverture de compte, avant l\'enlèvement ou la livraison.<br>
Le Client ou la personne déléguée est tenu d\'être présent(e) au moment de la livraison sur site.<br>
Un lieu devra être désigné pour la livraison des produits avant la livraison. En cas de modification, le Client s\'engage à en informer le Vendeur.
</p>
<h2 class="post-chapter">
CONDITIONS DE REGLEMENT
</h2>
<p>
Les paiements doivent être effectués à nos bureaux de Marolles les Braults à la date d\'échéance portée sur la facture. Conformément à l\'article L. 441-6 du Code de commerce modifié par la loi n°2008-776 du 4 août 2008 de modernisation de l\'économie, le délai de règlement ne peut dépasser 45 jours fin de mois ou 60 jours à compter de l\'émission de la facture. Selon la règlementation, il n\'est plus appliqué d\'escompte, en vertu de la règlementation applicable à la distribution de produits agricoles.
</p>
<h2 class="post-chapter">
PENALITES DE RETARD
</h2>
<p>
Conformément aux dispositions visées sous les articles L. 441-3 et L. 441-6 du Code de commerce, toute inexécution, totale ou partielle, par le Client de ses obligations de paiement ou tout retard, entraînera l\'exigibilité de plein droit d\'une pénalité d\'un montant minimum annuel égal à trois fois le taux d\'intérêt légal en vigueur à la date d\'échéance sans qu\'il soit besoin d\'une mise en demeure. Le Client accepte expressément les dispositions ci-dessus par dérogation à l\'article 1153 du Code civil.<br>
Les intérêts commenceront à courir à compter du jour suivant la date de paiement figurant sur la facture et continueront à courir jusqu\'au jour du parfait paiement de la totalité des sommes dues au Vendeur. Tout mois commencé sera intégralement dû. Le Vendeur pourra imputer de plein droit lesdites pénalités de retard sur toute réduction de prix due au Client.
</p>
<h2 class="post-chapter">
FRAIS DE RECOUVREMENT :
</h2>
<p>
A compter du 1er janvier 2013, conformément aux dispositions de l\'article L441-6 du Code commerce, en cas de retard de paiement, le client est de plein droit débiteur envers le vendeur, sans qu\'il soit besoin d\'une mise en demeure, d\'une indemnité forfaitaire pour frais de recouvrement de 40€ et ce, sans préjudice des pénalités de retard exigibles de plein droit et des dommages et intérêts susceptibles d\'être réclamés. Si les frais de recouvrement réels dépassent l\'indemnité forfaitaire, le vendeur se réservera le droit de demander le remboursement de tous les frais engagés pour le remboursement de sa créance.<br>
A défaut de paiement, même partiel, d\'une seule des échéances convenues pour l\'une quelconque des livraisons, le Vendeur se réserve la possibilité de demander l\'exigibilité immédiate de la totalité des sommes dues par le Client à quelque titre que ce soit.<br>
En cas de paiement par effet de commerce, le défaut de retour de l\'effet sera considéré comme un refus d\'acceptation assimilable à un défaut de paiement.<br>
Conformément aux dispositions de l\'article L. 442-6-I-8° du Code de commerce, aucun paiement ne peut faire l\'objet d\'une compensation à la seule initiative du Client, notamment en cas d\'allégation par le Client d\'un retard de livraison ou de non-conformité des produits livrés, l\'accord préalable et écrit du Vendeur étant indispensable et ce, quelles que soient les dispositions éventuellement contraires pouvant figurer dans les éventuelles conditions d\'achat du Client.<br>
Une marchandise ne peut être retournée qu\'avec l\'accord du Vendeur et selon ses conditions. Elle pourra faire l\'objet d\'une décote.
</p>
<h2 class="post-chapter">
VARIATION DES PRIX
</h2>
<p>
Les prix applicables sont ceux en vigueur au jour de la commande. Le vendeur se réserve le droit de modifier périodiquement ses barèmes unitaires. Dans cette hypothèse, le client en sera préalablement informé.<br>
Une éventuelle variation des barèmes unitaires décidée par le vendeur entre la commande et la date de livraison effective n\'affectera donc pas le prix convenu entre le vendeur et le client à la commande.</p> <h2 class="post-chapter">CLAUSE DE RESERVE DE PROPRIETE</h2> <p>De convention expresse, le Vendeur se réserve la propriété des fournitures livrées jusqu\'au complet paiement de l\'intégralité du prix et de ses accessoires, conformément aux dispositions des articles 2367 à 2372 du Code civil ainsi que les articles L. 624-9 et L. 624-16 du Code de commerce.<br>
Conformément aux articles L. 624-9 et L.624-16 du Code de commerce et nonobstant toute clause contraire, la présente clause de réserve de propriété est opposable au Client.<br>
Le Client devenant responsable des marchandises dès leur remise matérielle qui entraîne un transfert des risques, en assure les conséquences.<br> Le défaut de l\'une quelconque des échéances peut entraîner la revendication des biens.<br>
Le Vendeur se réserve la faculté, sans formalité, de reprendre matériellement possession des fournitures aux frais du Client et à ses risques et périls.<br>
En cas de revente des fournitures avant paiement intégral du prix et de ses accessoires, le Client s\'engage à la première demande de la part du Vendeur, à céder tout ou partie des créances sur ses sous-acquéreurs, à due concurrence de la valeur des marchandises soumises à la réserve de propriété.
</p>
<h2 class="post-chapter">
RESPONSABILITE
</h2>
<p>
La responsabilité de notre Société est expressément limitée à la revente en l\'état de produits garantis par le fabricant dans leur conformité vis-à-vis de la loi et ce quel que soit leur conditionnement.<br>
Notre Société ne pourra, en conséquence, être tenue pour responsable des conséquences de l\'utilisation de ces produits, l\'utilisateur étant maître de leur emploi.<br>
Notre Société ne sera tenue que par une obligation de moyens et non par une obligation de résultat, le Client restant entièrement responsable de l\'utilisation des produits.<br>
L\'attention du Client est attirée sur la nécessité :<br>
- de n\'utiliser les produits achetés que pour le ou les usages autorisés à la vente indiqués sur l\'étiquette, tout détournement d\'usage étant sanctionné pénalement ;<br>
- de s\'assurer avant toute application que la marchandise livrée correspond à la commande.
</p>
<h2 class="post-chapter">
DONNEES A CARACTERE PERSONNEL
</h2>
<p>
Conformément à la loi n°78-17 du 6 janvier 1978 modifiée relative à l\'informatique, aux fichiers et aux libertés et au règlement 2016/679 du 27 avril 2016, le vendeur informe le client qu\'il respecte la confidentialité, l\'intégrité et la sécurité des données que ces derniers peuvent être amenés à lui communiquer. Le client dispose, en vertu de ces dispositions d\'un droit d\'accès, de rectification, de suppression et d\'opposition au traitement de données le concernant en adressant une demande au vendeur à l\'adresse de son siège social ou par courrier électronique à contact.jeusselin@jeusselin.fr et en joignant à sa demande une copie de sa pièce d\'identité. Toute ouverture d\'un compte client est soumise à la réalisation d\'un traitement de données à caractère personnel sous la responsabilité du vendeur. Le vendeur collecte et conserve les données exclusivement et pour la durée strictement nécessaire à l\'exécution de ses prestations et en cas de mise en jeu de sa responsabilité contractuelle et/ou délictuelle. Au-delà de cette durée, les données personnelles ne seront plus conservées. Le vendeur se réserve le droit de transmettre les données du client à des partenaires commerciaux.<br>
Conformément à l\'article 5 (1er paragraphe), et à l\'article 12 du règlement 2016/679 du 27 avril 2016, le vendeur s\'engage à informer les tiers auxquels les informations ont été communiqués de l\'utilisation par le client du droit d\'accès, de rectification, de suppression et d\'opposition au traitement de données le concernant.
</p>
<h2 class="post-chapter">
EXCLUSION DE TOUTES PENALITES
</h2>
<p>
Aucune pénalité ne sera acceptée par le Vendeur, sauf accord préalable et écrit de ce dernier et ce, quelle que soit la motivation de la pénalité.
</p>
<h2 class="post-chapter">
RECLAMATIONS / CONTESTATIONS COMMERCIALES
</h2>
<p>
Les réclamations concernant nos factures ne pourront être prises en considération que si elles sont présentées par écrit au plus tard 15 jours à compter de leur date d\'émission.<br>
Les réclamations concernant les livraisons ne peuvent être prises en considération que si elles sont faites dans les 3 jours après réception de la marchandise, si celle-ci se trouve dans son emballage d\'origine et si des réserves précises et motivées ont été faites par écrit par le destinataire, lors de la réception.
</p>
<h2 class="post-chapter">
ATTRIBUTION DE JURIDICTION – DROIT APPLICABLE
</h2>
<p>
L\'ensemble des relations contractuelles entre le Vendeur et le Client issues de l\'application des présentes Conditions générales de vente et les éventuels accords particuliers qui pourraient être conclus et tous les litiges en découlant, quelle qu\'en soit la nature, seront soumis à tous égards au droit français et ce, quand bien même les produits seraient-ils vendus à un Client établi à l\'extérieur du territoire français.<br>
Les parties conviennent de faire leur possible pour résoudre à l\'amiable les désaccords susceptibles de résulter de l\'interprétation, l\'exécution ou la cessation des relations commerciales entre le Vendeur et le Client.<br>
Tout litige ayant son origine dans l\'exécution des relations contractuelles établies entre le Vendeur et le Client, ainsi que les actes qui en seront la conséquence, nonobstant toute demande incidente ou tout appel en garantie ou en cas de pluralité de défendeurs, sera soumis au Tribunal de commerce de Le Mans. Cette clause d\'attribution de compétence s\'appliquera même en cas de référé.<br>
Le Vendeur disposera néanmoins de la faculté de saisir toute autre juridiction compétente, en particulier celle du siège social du Client ou celle du lieu de situation des marchandises livrées.<br>
Les effets de commerce ou acceptation de règlement ne feront ni novation ni dérogation à la présente clause.
</p>
<h2 class="post-chapter">
CLAUSE PENALE
</h2>
<p>
A défaut de paiement, même partiel, d\'une seule des échéances convenues pour l\'une quelconque des livraisons, le Vendeur se réserve la possibilité de demander l\'exigibilité immédiate de la totalité des sommes dues par le Client à quelque titre que ce soit. Si, par ailleurs, le Vendeur est mis dans l\'obligation de s\'adresser à un mandataire (avocat, huissier, société de recouvrement, etc…) pour obtenir le règlement des sommes dues, il est expressément convenu, à titre de clause pénale stipulée forfaitairement, de plein droit et non réductible, l\'application d\'une majoration calculée au taux de 10 % du montant des sommes dues par le Client, cette majoration ne pouvant être inférieure à 45 euros et ce, sans préjudice des intérêts de retard, des dommages et intérêts et des frais de recouvrement.
</p>
<h2 class="post-chapter">
ENTREE EN VIGUEUR
</h2>
<p>
Les présentes Conditions générales de vente prennent effet le 1er janvier 2021. Elles annulent et remplacent celles établies antérieurement à la date des présentes.
</p>
',
]
);
StaticPage::query()->updateOrCreate(
[
'type' => StaticPageType::confidentiality,
],
[
'seo_title' => 'Politique de Confidentialité',
'seo_description' => 'Politique de Confidentialité du site Jeusselin.fr ',
'title' => 'Politique de Confidentialité',
'type' => StaticPageType::confidentiality,
'content' => '
<h1 class="post-title">Charte de confidentialité</h1>
<h2 class="post-chapter">Introduction</h2>
<p>Le nouveau Règlement Général de l\'Union Européenne sur la Protection des Données (RGPD), entré en vigueur le 25 mai 2018, vise à mieux encadrer la collecte, le traitement et l\'utilisation de vos données personnelles.
<strong>
Jeusselin, en sa qualité de responsable du traitement, renforce ses engagements pour garantir la protection et la sécurité des données à caractère personnel que vous lui confiez.
</strong>
</p>
<p>
A ce titre, la présente Charte de confidentialité vous informe sur le type de données que nous collectons, comment nous les traitons, dans quelles finalités, de manière à rendre leur utilisation la plus adaptée à vos besoins.
</p>
<p>
Le responsable de traitement, qui collecte et gère vos données, est la SAS JEUSSELIN au capital social de 521 280 € immatriculée au registre du commerce et des sociétés de Mamers sous le numéro 696 850 098, dont le siège social est 9, rue des Lavandières – 72260 MONCÉ EN SAOSNOIS représentée par M. HARDY Jean-Yves.
</p>
<p>
Pour toute question relative à notre Charte de confidentialité,
<strong>merci de nous contacter via notre mail : contact.jeusselin@jeusselin.fr</strong>
</p>
<p>
<strong>
Nous vous invitons à consulter régulièrement cette rubrique car Jeusselin peut être amené à la modifier à tout moment.
</strong>
</p>
<p>
La présente Charte de confidentialité a été mise à jour le 01/03/2021.
</p>
<h2 class="post-chapter">Nos engagements</h2>
<p>
Conformément à la réglementation relative à la protection des données personnelles et à la vie privée, notamment la loi n°78-17 du 6 janvier 1978 relative à l\'informatique, aux fichiers et aux libertés dite loi « Informatique et Libertés » et au Règlement (UE) 2016/679 du 25 mai 2018 relatif à la protection des personnes physiques à l\'égard du traitement des données à caractère personnel et à la libre circulation de ces données, (ci-après le « Règlement RGPD»), Jeusselin s\'engage à respecter les principes suivants :
</p>
<ul>
<li>
<strong>Consentement : </strong>vous devez avoir consenti à la collecte et au traitement de vos données. Si tel n\'était pas le cas ou si vous avez changé d\'avis, merci de vous reporter à l\'article ci-dessous relatif à vos droits.
</li>
<li>
<strong>Limitation des données collectées : </strong>nous ne collectons que les données qui sont strictement nécessaires au traitement et à l\'utilisation envisagée. Nous vous rappelons qu\'il n\'y a aucune obligation à nous transmettre ces données. Toutefois certains services ou opérations promotionnelles nécessitent certaines données, faute de quoi vous ne pourriez y accéder.
</li>
<li>
<strong>Limitation des finalités : </strong>les données que nous collectons sont traitées uniquement dans les finalités décrites ci-dessous.
</li>
<li>
<strong>Limitation des durées de conservation : </strong>vos données sont conservées uniquement le temps nécessaire aux finalités de traitement pour lequel elles ont été collectées. Au-delà de cette durée si aucune obligation légale impose leur conservation, nous veillerons à ce que vos données soient supprimées, détruites ou anonymisées.
</li>
<li>
<strong>Protection des données : </strong>en vue de sécuriser vos données et leur traitement, nous avons mis en place des mesures techniques et organisationnelles détaillées ci-dessous.
</li>
<li>
<strong>Transparence et loyauté : </strong>lorsque Jeusselin collecte une donnée vous concernant, nous vous informons de l\'utilisation qui en sera faite. C\'est pourquoi nous vous conseillons de lire attentivement les informations qui vous sont transmises à travers notre site internet.
</li>
</ul>
<h2 class="post-chapter">Quelles données personnelles sont collectées ?</h2>
<p>
Au sens du Règlement RGPD, une donnée à caractère personnel désigne toute information relative à une personne physique identifiée ou identifiable.
</p>
<p>
Les données personnelles collectées varient en fonction du but de la collecte et du service dont vous souhaitez bénéficier.
</p>
<p>
La société Jeusselin est amenée à collecter les catégories de données personnelles suivantes :
</p>
<ul>
<li>
- coordonnées personnelles (civilité, nom, prénom, adresse e-mail, adresse postale) ;
</li>
<li>
- coordonnées personnelles <strong>non obligatoires</strong> (date de naissance, numéro de téléphone) ;
</li>
<li>
- identifiants utilisés pour accéder à votre compte en ligne (adresse IP, adresse e-mail et mot de passe) ;
</li>
<li>
- données relatives aux tickets de caisse de l\'utilisateur ainsi que son RIB ou son compte Paypal ;
</li>
<li>
- historiques de navigation, telles que les pages visitées et actions sur le site.
</li>
</ul>
<p>
Vous vous engagez à fournir des données personnelles actualisées et valides et garantissez ne faire aucune fausse déclaration et ne donner aucune information erronée.
</p>
<h2 class="post-chapter">Quelles données personnelles sont collectées ?</h2>
<p>
Vos données peuvent être collectées dans la mesure où vous avez consenti de manière spécifique, libre et éclairée. Elles peuvent également être collectées pour satisfaire à l\'exécution d\'une obligation légale ou dans le cadre de la relation contractuelle construite avec vous (notamment conditions générales d\'utilisation, conditions générales de vente) ou encore dans l\'intérêt légitime de Jeusselin (par exemple pour assurer la sécurité des transactions).
</p>
<p>
Nous collectons vos données personnelles afin de vous fournir la meilleure expérience utilisateur à travers notre site internet.
</p>
<p>
<strong>Ces données sont collectées aux fins suivantes : </strong>
</p>
<ul>
<li>Gestion des fonctionnalités et de la qualité du site internet</li>
<li>Gestion de votre inscription à notre Newsletter et de votre Compte utilisateur</li>
<li>Accès à votre Compte utilisateur</li>
<li>Traitement de vos demandes de renseignements</li>
<li>Communication des informations (promotions, nouveaux produits, activité de la marque)</li>
<li>Gestion de votre participation à nos jeux, tirages au sort, activités promotionnelles</li>
<li>Statistiques</li>
<li>Proposition de produits personnalisés et de communications adaptées ou pour recommandation de produits en fonction des besoins de consommateurs</li>
<li>Partages sur les réseaux sociaux</li>
</ul>
<h2 class="post-chapter">Quelles sont vos droits ?</h2>
<p>
Vous disposez d\'un certain nombre de droits concernant vos données personnelles listés ci-dessous.
</p>
<ul>
<li>
<strong>Droit d\'accès</strong>
<p>Vous pouvez demander la communication de vos données sous une forme accessible, la confirmation que vos données font ou ne font plus l\'objet d\'un traitement ainsi que la communication des informations relatives à vos données. Afin que vos données personnelles soient exactes, complètes, pertinentes et à jour, vous avez le droit d\'accéder, de corriger ou de mettre à jour vos données personnelles à tout moment.</p>
</li>
<li>
<strong>Droit de rectification</strong>
<p> Vous êtes en droit de demander à ce que vos données soient rectifiées, complétées ou mises à jour lorsqu\'elles sont inexactes, incomplètes, équivoques ou périmées.</p>
</li>
<li>
<strong>Droit à l\'effacement</strong>
<p>Vous avez le droit de demander la suppression de vos données personnelles notamment lorsqu\'elles ne sont plus nécessaires aux finalités pour lesquelles elles ont été collectées. Si les données personnelles collectées ne sont plus nécessaires à aucune finalité et qu\'aucune obligation légale nous impose de les conserver, nous mettrons tout en œuvre pour supprimer, détruire ou anonymiser ces données.</p>
</li>
<li>
<strong>Droit à la limitation du traitement</strong>
<p>Vous avez le droit de demander la limitation du traitement de vos données personnelles si :</p>
<ul>
<li>- vous contestez l\'exactitude des données ;</li>
<li>- vous souhaitez limiter le traitement des données qui n\'avaient pas été traitées de manière licite plutôt que leur suppression ;</li>
<li>- nous n\'avons plus besoin de vos données personnelles mais celles-ci sont nécessaires pour la constatation, l\'exercice ou la défense de vos droits en justice ;</li>
<li>- vous vous êtes opposés au traitement de vos données personnelles et attendez de savoir si vos intérêts liés à cette objection l\'emportent sur les motifs légitimes que nous poursuivons.</li>
</ul>
</li>
<li>
<strong>Droit à la portabilité de vos données</strong>
<p>Lorsque le traitement est effectué à partir de procédés automatisés et que vous avez librement consenti au traitement de vos données personnelles pour une finalité que nous vous avions communiqué au préalable ; ou lorsque nous traitons vos données dans le cadre d\'une opération commerciale (ex : vous fournir un produit) vous avez le droit de demander :</p>
<ul>
<li>- à recevoir vos données personnelles dans un format structuré, couramment utilisé et lisible par une machine ;</li>
<li>- à ce que vos données personnelles soient transmises directement à une autre responsable de traitement, lorsque que cela est techniquement possible.</li>
</ul>
</li>
<li>
<strong>Droit d\'opposition</strong>
<p>Lorsque vos données sont traitées à des fins de prospection commerciale, vous avez le droit, à tout moment, de vous opposer au traitement de vos données personnelles et ce sans motif.</p>
</li>
</ul>
<p><strong>Droit de porter réclamation auprès de la CNIL</strong></p>
<p>Si vous estimez que le traitement de vos données personnelles constitue une violation de la réglementation, vous pouvez introduire une réclamation auprès d\'une autorité de contrôle, telle que la CNIL.</p>
<p><strong>Vous pouvez exercer ces droits à tout moment en nous contactant via notre mail : contact.jeusselin@jeusselin.fr</strong></p>
<p><strong>Vous recevrez une réponse de notre part dans un délai d\'un mois à compter de la réception de la demande. En cas de demande complexe ou du nombre important de demandes, le délai peut être prolongé de deux mois.</strong></p>
<p><strong>L\'exercice des droits se fait à titre gratuit. Si toutefois la demande était manifestement infondée ou excessive, Jeusselin se réserve la possibilité d\'appliquer des frais administratifs ou de refuser de donner suite à la demande.</strong></p>
<h2 class="post-chapter">Comment vos données personnelles sont protégées ?</h2>
<p>Afin d\'assurer la sécurité de vos données personnelles nous avons mis en place des mesures de sécurité, notamment des contrôles d\'accès, des pares-feux, des serveurs sécurisés.</p>
<p>Ces mesures visent à protéger vos données personnelles contre toute perte, détournement, mauvaise utilisation, divulgation, modification, interférence, dommage, accès non autorisé des données.</p>
<p>En cas de violation de vos données personnelles susceptible d\'engendrer un risque pour vos droits et libertés, Jeusselin en notifie la violation à la CNIL dans les meilleurs délais et, si possible, 72 heures au plus tard après en avoir pris connaissance. Jeusselin vous en informera dans les meilleurs délais.</p>
<h2 class="post-chapter">Accès à vos données personnelles : </h2>
<p>Vos données personnelles sont destinées au personnel de Jeusselin habilité à les traiter, notamment le service marketing, développement durable, informatique, commercial, qualité et administratif.</p>
<h2 class="post-chapter">Partage de vos données personnelles : </h2>
<p>Conformément à la règlementation, quand nous sommes amenés à partager vos données personnelles avec des sous-traitants pour certaines finalités décrites dans la présente Charte, nous nous assurons que leur dispositif en matière de sécurité des données et de leur stockage est au moins aussi important que celui de notre organisation. Nous veillons également à ce qu\'ils respectent les lois relatives à la protection des données personnelles dans des conditions au moins équivalentes à celles que nous respectons.</p>
<p>Par ailleurs, les sous-traitants étant amenés à traiter de vos données personnelles pour notre compte et sur nos instructions, nous nous engageons à ce que vos données personnelles ne soient pas partagées ou divulguées pour des finalités autres que celles décrites dans la présente Charte. Nous pouvons cependant être amenés à transmettre vos données, pour des finalités autres que celles prévues dans la Charte, dans les cas où leur communication est requise par la loi et les autorités gouvernementales.</p>
<h2 class="post-chapter">Transfert de vos données en dehors de l\'espace economique européen : </h2>
<p>Nous nous informons que vos données personnelles ne sont pas traitées en dehors de l\'Espace Economique Européen (EEE). En cas de changement, nous vous en avertirons et mettrons tout en œuvre pour que ce transfert international de données dispose d\'un niveau et de garanties de sécurité adéquats.</p>
<h2 class="post-chapter">Utilisation des cookies : </h2>
<p>Les cookies sont des petits fichiers textes contenant des informations propres vous concernant stockés sur le disque dur de l\'équipement informatique avec lequel vous vous êtes connectés sur le site internet et pouvant être lus seulement par le serveur qui l\'a fourni. Les cookies sont gérés par votre navigateur internet.</p>
<p>Les cookies indiquent la fréquentation des pages que vous visitez, ce qui nous aide à connaître les informations qui vous intéressent. Cela permet de vous offrir plus de contenu qui vous intéresse et moins de contenu qui ne vous intéresse pas.</p>
<p>Les cookies vous permettent de stocker des préférences et les noms des utilisateurs, enregistrer des produits et des services, personnaliser des pages ou faire de la publicité ciblée.</p>
<p>A travers ces cookies, vos données personnelles peuvent être recueilles telles que :</p>
<ul>
<li>- des informations concernant le navigateur de votre appareil et le système d\'exploitation que vous utilisez ;</li>
<li>- l\'adresse IP de l\'appareil que vous utilisez ;</li>
<li>- les pages Web que vous consultez ;</li>
<li>- les liens sur lesquelles vous cliquez pendant que vous êtes en interaction avec nos services.</li>
</ul>
<p>Le site internet Jeusselin utilise trois types de cookies qui ont des finalités différentes. Tous les cookies de type non techniques doivent recueillir votre consentement avant d\'être installés. Vous pouvez donner votre consentement sur la page d\'accueil du site internet, en cliquant sur « accepter ».</p>
<ul>
<li>- Les cookies strictement nécessaires et fonctionnels
<p>Ces cookies sont nécessaires au bon fonctionnement du site internet. Ils vous permettent d\'utiliser les principales fonctionnalités. Sans ces cookies, vous ne pourrez pas utiliser normalement le site internet.</p>
</li>
<li>- Les cookies analytiques
<p>Ces cookies nous permettent de connaître l\'utilisation et les performances du site internet et d\'en améliorer son fonctionnement.</p>
</li>
<li>- Les cookies de ciblage
<p>Ces cookies sont utilisés pour vous présenter des publicités ou vous adresser des informations adaptées à vos centres d\'intérêts en dehors du site Jeusselin lors de votre navigation sur Internet. Le refus de ces cookies publicitaires n\'a pas d\'impact sur l\'utilisation du site internet. Cependant, le fait de refuser les cookies publicitaires n\'entraînera pas l\'arrêt de la publicité sur Internet. Cela aura seulement pour effet d\'afficher une publicité qui ne tiendra pas compte de vos centres d\'intérêt ou de vos préférences.</p>
</li>
</ul>
<p><strong>Vous avez le choix :</strong></p>
<ul>
<li>- <strong>d\'accepter</strong> ces cookies en cliquant sur le bouton « Accepter » présent sur le bandeau Cookies, apparaissant au bas de votre page internet. Tant que vous n\'avez pas accepté, les cookies ne pourront être stockés sur votre appareil ; ou
</li><li>- <strong>de refuser</strong> ces cookies via le bouton « Paramétrer » présent sur le bandeau Cookies ou les paramètres de votre navigateur internet.
</li></ul>
<p>Lorsque vous cliquez sur le bouton « Paramétrer » du bandeau Cookies, une liste s\'affiche présentant les différents types de cookies par finalité. Si vous consentez au dépôt des cookies, il vous suffit de les activer puis de cliquer sur le bouton « Enregistrer » en bas de la fenêtre. Si vous refusez le dépôt des cookies, il vous suffit de cliquer sur le bouton « Enregistrer », le dépôt des cookies étant par défaut désactivé.</p>
<p>Pour en savoir plus sur la désactivation des cookies via votre navigateur internet :</p>
<ul>
<li>Internet Explorer: <a target="_privacy" href="http://windows.microsoft.com/fr-FR/windows-vista/Block-or-allow-cookies">http://windows.microsoft.com/fr-FR/windows-vista/Block-or-allow-cookies</a></li>
<li>Chrome: <a target="_privacy" href="http://support.google.com/chrome/bin/answer.py?hl=fr&amp;hlrm=en&amp;answer=95647">http://support.google.com/chrome/bin/answer.py?hl=fr&amp;hlrm=en&amp;answer=95647</a></li>
<li>Firefox: <a target="_privacy" href="http://support.mozilla.org/fr/kb/Activer%20et%20d%C3%A9sactiver%20les%20cookies">http://support.mozilla.org/fr/kb/Activer%20et%20d%C3%A9sactiver%20les%20cookies</a></li>
<li>Safari: <a target="_privacy" href="https://support.apple.com/fr-fr/guide/safari/sfri11471/mac">https://support.apple.com/fr-fr/guide/safari/sfri11471/mac</a></li>
</ul>
<p>La durée de conservation de ces informations dans votre appareil est d\'une durée maximale de 13 mois. A l\'expiration de ce délai, nous recueillerons de nouveau votre consentement.</p>
<p>Pour plus d\'informations sur le paramétrage de vos cookies, vous pouvez consulter :</p>
<ul>
<li><a target="_privacy" href="http://www.youronlinechoices.com/fr/">http://www.youronlinechoices.com/fr/</a></li>
<li><a target="_privacy" href="https://www.allaboutcookies.org/">https://www.allaboutcookies.org/</a></li>
</ul>
<h2 class="post-chapter">Site internet de tiers</h2>
<p>Le site internet Jeusselin peut être associé à des sites internet tiers. Les sites hébergés par des tiers sont hors de notre contrôle. Lorsque vous renseignez des données personnelles sur un site internet tiers, le traitement de ces données n\'est pas effectué sur notre site internet, mais sur le site internet tiers concerné. Toute donnée personnelle que vous fournissez sera collectée et traitée conformément à la politique relative à la confidentialité des données du tiers concerné.</p>
<p>Par conséquent, nous n\'assumons aucune responsabilité concernant les pratiques exercées par lesdits tiers en matière de protection des données.</p>
<h2 class="post-chapter">Droit applicable</h2>
<p>La présente Charte de confidentialité est régie et sera interprétée conformément au droit français.</p>
<h2 class="post-chapter">Nous contacter</h2>
<p>Si vous avez des questions, remarques ou réclamations relatives à la présente Charte de confidentialité, ou le traitement de vos données personnelles, veuillez-nous contacter par mail à l\'adresse : contact.jeusselin@jeusselin.fr.</p>
',
]
);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('static_pages');
}
};

3
jsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"include": ["./resources/js/**/*"]
}

30
k8s-queues.sh Normal file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# From https://stackoverflow.com/a/76488746/17320077
work=true
function stopQueues() {
echo "Stopping queues..."
work=false
php artisan queue:restart
sleep 45s
exit 0
}
# handle graceful shutdown
trap "" SIGPIPE
trap stopQueues EXIT
echo "Starting PHP queue workers..."
while $work; do
php artisan queue:work --sleep=3 --tries=3 --max-time=3600 --no-interaction
sleep 1
done
# Si besoin de plusieurs threads puis terminer par un done sans &
#(while $work; do
# php artisan queue:work --queue="$(grep -oP 'SQS_QUEUE=\K.*' .env)" --no-interaction
# sleep 1
#done &)

View file

@ -30,6 +30,12 @@ data:
MAILJET_APISECRET: "${APP_MAILJET_APISECRET}"
MAILJET_SMSTOKEN: "${APP_MAILJET_SMSTOKEN}"
MATOMO_SITE_ID: "${APP_MATOMO_SITE_ID}"
UWIL_ADMIN_SECURE_CIDR_LIST: "${APP_GLOBAL_UWIL_ADMIN_SECURE_CIDR_LIST}"
SAML2_SP_CERT_x509: "${APP_SAML2_SP_CERT_x509}"
SAML2_SP_CERT_PRIVATEKEY: "${APP_SAML2_SP_CERT_PRIVATEKEY}"
SAML2_CONTACT_TECHNICAL_EMAIL: "${APP_SAML2_CONTACT_TECHNICAL_EMAIL}"
SAML2_CONTACT_SUPPORT_EMAIL: "${APP_SAML2_CONTACT_SUPPORT_EMAIL}"
SAML2_DEFAULT_TENANT_UUID: "${APP_SAML2_DEFAULT_TENANT_UUID}"
---
apiVersion: v1
@ -97,7 +103,7 @@ spec:
app: ${CI_PROJECT_PATH_SLUG}-app
ports:
- port: 80
targetPort: 443
targetPort: 80
---
apiVersion: traefik.io/v1alpha1

15073
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,42 +1,82 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
"vite": "vite --clearScreen false",
"prod": "vite build --mode production",
"dev": "NODE_ENV=\"development\" && vite build --mode development -d",
"watch": "NODE_ENV=\"development\" && vite build --mode development --watch",
"eslint": "node ./node_modules/.bin/eslint --max-warnings 0 -c .eslintrc.json ./resources",
"eslint-fix": "node ./node_modules/.bin/eslint --fix --max-warnings 0 -c .eslintrc.json ./resources",
"eslint-ci": "node ./node_modules/.bin/eslint --max-warnings 0 -c .eslintrc.json ./resources -f junit",
"stylelint": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --max-warnings 0 '**/resources/**/*.scss'",
"stylelint-fix": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --fix --max-warnings 0 '**/resources/**/*.scss'",
"stylelint-ci": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --max-warnings 0 '**/resources/**/*.scss' -f json"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@ldc-web/cookie-notice": "^6.0.3",
"@babel/core": "^7.29.0",
"@babel/types": "^7.29.0",
"@erbelion/vite-plugin-laravel-purgecss": "^0.2.3",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@ldc-web/cookie-notice": "^6.1.0",
"@popperjs/core": "^2.10.2",
"axios": "^0.21",
"bootstrap": "^4.6.0",
"clean-webpack-plugin": "^3.0.0",
"@types/bootstrap": "^5.2.10",
"@types/node": "^20.8.2",
"@types/toastify-js": "^1.12.4",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.36.2",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-vue": "^4.6.2",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0",
"autoprefixer": "^10.4.24",
"axios": "^1.5.1",
"bootstrap": "^5.3.2",
"cropperjs": "^1.6.1",
"eslint": "^8.23.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-vue": "^9.4.0",
"jquery": "^3.5.1",
"laravel-mix": "^6.0.6",
"laravel-mix-clean": "^0.1.0",
"lodash": "^4.17.19",
"popper.js": "^1.16.1",
"resolve-url-loader": "^3.1.2",
"sass": "^1.32.11",
"sass-loader": "^11.0.1",
"vue": "^2.5.17",
"vue-loader": "^15.10.1",
"vue-template-compiler": "^2.6.10"
"laravel-vite-plugin": "^0.8.1",
"postcss-discard-comments": "^6.0.0",
"prettier": "^2.7.1",
"purgecss": "^8.0.0",
"rollup-plugin-copy": "^3.5.0",
"sass": "^1.54.8",
"sass-loader": "^12.6.0",
"slugify": "^1.6.5",
"stylelint": "^15.11.0",
"stylelint-config-idiomatic-order": "github:ream88/stylelint-config-idiomatic-order",
"stylelint-config-sass-guidelines": "^10.0.0",
"stylelint-config-standard-scss": "^11.1.0",
"stylelint-scss": "^5.3.2",
"sweetalert2": "^11.7.31",
"swiper": "^12.1.2",
"terser": "^5.46.0",
"toastify-js": "^1.12.0",
"ts-loader": "^9.3.1",
"typescript": ">=3.3.1 <5.2.0",
"typescript-eslint": "latest",
"vite": "^4.5.1",
"vite-plugin-babel": "^1.4.1",
"vite-plugin-checker": "^0.6.4",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-stylelint": "^5.3.1",
"vue": "^3.2.38",
"vue-eslint-parser": "^9.3.2",
"vue-loader": "^17.3.1",
"vue-tsc": "1.8.11"
},
"dependencies": {
"@ckeditor/ckeditor5-build-classic": "^24.0.0",
"@ckeditor/ckeditor5-vue2": "^1.0.5",
"@fancyapps/fancybox": "^3.5.7",
"@fancyapps/ui": "^6.1.12",
"@sipec/vue3-tags-input": "^3.0.4",
"chart.js": "^2.9.4",
"swiper": "^6.3.4",
"v-clipboard": "^2.2.3",
"vue-select-image": "^1.9.0"
}

8
phpstan.neon Normal file
View file

@ -0,0 +1,8 @@
includes:
- vendor/larastan/larastan/extension.neon
parameters:
paths:
- app/
level: 5

View file

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 161 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 3 MiB

After

Width:  |  Height:  |  Size: 3 MiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 254 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 284 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 9 KiB

View file

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 1,015 KiB

After

Width:  |  Height:  |  Size: 1,015 KiB

View file

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 272 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View file

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View file

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View file

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View file

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View file

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View file

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View file

Before

Width:  |  Height:  |  Size: 1,015 B

After

Width:  |  Height:  |  Size: 1,015 B

View file

Before

Width:  |  Height:  |  Size: 788 B

After

Width:  |  Height:  |  Size: 788 B

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Some files were not shown because too many files have changed in this diff Show more