fix: resolve SonarQube accessibility and test issues across templates

- Add for/id attributes to all form labels for accessibility compliance
- Add <title> tags to PDF templates (rgpd_access, rgpd_no_data, rgpd_deletion, contrat_revendeur)
- Add role="presentation" to email layout tables
- Remove deprecated cellpadding/cellspacing attributes from all templates
- Fix PHPUnit notices by replacing createMock with createStub where no expectations are set

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-01 19:30:53 +02:00
parent b0060bd831
commit 5f144ba4d2
19 changed files with 198 additions and 182 deletions

View File

@@ -23,30 +23,30 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Identite</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-4">Identite</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Prenom *</label> <label for="firstName" class="block text-xs font-black uppercase tracking-widest mb-2">Prenom *</label>
<input type="text" name="firstName" required placeholder="Prenom" <input type="text" id="firstName" name="firstName" required placeholder="Prenom"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Nom *</label> <label for="lastName" class="block text-xs font-black uppercase tracking-widest mb-2">Nom *</label>
<input type="text" name="lastName" required placeholder="Nom" <input type="text" id="lastName" name="lastName" required placeholder="Nom"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Email *</label> <label for="email" class="block text-xs font-black uppercase tracking-widest mb-2">Email *</label>
<input type="email" name="email" required placeholder="email@exemple.fr" <input type="email" id="email" name="email" required placeholder="email@exemple.fr"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Telephone</label> <label for="phone" class="block text-xs font-black uppercase tracking-widest mb-2">Telephone</label>
<input type="tel" name="phone" placeholder="06 12 34 56 78" <input type="tel" id="phone" name="phone" placeholder="06 12 34 56 78"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Type</label> <label for="typeCompany" class="block text-xs font-black uppercase tracking-widest mb-2">Type</label>
<select name="typeCompany" class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold"> <select id="typeCompany" name="typeCompany" class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold">
<option value="">— Selectionner —</option> <option value="">— Selectionner —</option>
<option value="particulier">Particulier</option> <option value="particulier">Particulier</option>
<option value="association">Association</option> <option value="association">Association</option>
@@ -65,23 +65,23 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Entreprise</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-4">Entreprise</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Raison sociale</label> <label for="raisonSociale" class="block text-xs font-black uppercase tracking-widest mb-2">Raison sociale</label>
<input type="text" name="raisonSociale" placeholder="Nom de l'entreprise" <input type="text" id="raisonSociale" name="raisonSociale" placeholder="Nom de l'entreprise"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">SIRET</label> <label for="siret" class="block text-xs font-black uppercase tracking-widest mb-2">SIRET</label>
<input type="text" name="siret" maxlength="14" placeholder="12345678901234" <input type="text" id="siret" name="siret" maxlength="14" placeholder="12345678901234"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">RCS</label> <label for="rcs" class="block text-xs font-black uppercase tracking-widest mb-2">RCS</label>
<input type="text" name="rcs" placeholder="RCS Paris 123 456 789" <input type="text" id="rcs" name="rcs" placeholder="RCS Paris 123 456 789"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">N° TVA</label> <label for="numTva" class="block text-xs font-black uppercase tracking-widest mb-2">N° TVA</label>
<input type="text" name="numTva" placeholder="FR12345678901" <input type="text" id="numTva" name="numTva" placeholder="FR12345678901"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>
@@ -91,24 +91,24 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Adresse</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-4">Adresse</h2>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Adresse</label> <label for="address" class="block text-xs font-black uppercase tracking-widest mb-2">Adresse</label>
<input type="text" name="address" placeholder="Numero et rue" <input type="text" id="address" name="address" placeholder="Numero et rue"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Complement d'adresse</label> <label for="address2" class="block text-xs font-black uppercase tracking-widest mb-2">Complement d'adresse</label>
<input type="text" name="address2" placeholder="Batiment, etage, etc." <input type="text" id="address2" name="address2" placeholder="Batiment, etage, etc."
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Code postal</label> <label for="zipCode" class="block text-xs font-black uppercase tracking-widest mb-2">Code postal</label>
<input type="text" name="zipCode" maxlength="10" placeholder="02800" <input type="text" id="zipCode" name="zipCode" maxlength="10" placeholder="02800"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Ville</label> <label for="city" class="block text-xs font-black uppercase tracking-widest mb-2">Ville</label>
<input type="text" name="city" placeholder="Beautor" <input type="text" id="city" name="city" placeholder="Beautor"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>

View File

@@ -41,10 +41,10 @@
placeholder="email@exemple.fr"> placeholder="email@exemple.fr">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Groupes d'acces</label> <label for="groups-member" class="block text-xs font-black uppercase tracking-widest mb-2">Groupes d'acces</label>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3"> <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<label class="flex items-center gap-2 px-3 py-2 border-2 border-indigo-200 hover:border-indigo-600 cursor-pointer transition-colors bg-indigo-50"> <label class="flex items-center gap-2 px-3 py-2 border-2 border-indigo-200 hover:border-indigo-600 cursor-pointer transition-colors bg-indigo-50">
<input type="checkbox" name="groups[]" value="gp_member" class="accent-indigo-600" checked> <input type="checkbox" id="groups-member" name="groups[]" value="gp_member" class="accent-indigo-600" checked>
<span class="text-xs font-bold text-indigo-800">Membre</span> <span class="text-xs font-bold text-indigo-800">Membre</span>
</label> </label>
<label class="flex items-center gap-2 px-3 py-2 border-2 border-red-200 hover:border-red-600 cursor-pointer transition-colors bg-red-50"> <label class="flex items-center gap-2 px-3 py-2 border-2 border-red-200 hover:border-red-600 cursor-pointer transition-colors bg-red-50">

View File

@@ -91,8 +91,8 @@
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<form method="post" action="{{ path('app_admin_profil_avatar') }}" enctype="multipart/form-data" class="flex items-end gap-3"> <form method="post" action="{{ path('app_admin_profil_avatar') }}" enctype="multipart/form-data" class="flex items-end gap-3">
<div> <div>
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Image (JPG, PNG, WebP — max 2Mo)</label> <label for="avatar" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Image (JPG, PNG, WebP — max 2Mo)</label>
<input type="file" name="avatar" accept="image/jpeg,image/png,image/webp" required <input type="file" id="avatar" name="avatar" accept="image/jpeg,image/png,image/webp" required
class="text-xs file:mr-3 file:py-2 file:px-4 file:border-2 file:border-gray-900 file:bg-[#fabf04] file:text-gray-900 file:font-black file:uppercase file:text-[10px] file:tracking-widest file:cursor-pointer"> class="text-xs file:mr-3 file:py-2 file:px-4 file:border-2 file:border-gray-900 file:bg-[#fabf04] file:text-gray-900 file:font-black file:uppercase file:text-[10px] file:tracking-widest file:cursor-pointer">
</div> </div>
<button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all"> <button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all">
@@ -119,19 +119,19 @@
<form method="post" action="{{ path('app_admin_profil_update') }}" class="flex flex-col gap-4"> <form method="post" action="{{ path('app_admin_profil_update') }}" class="flex flex-col gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Prenom</label> <label for="firstName" class="block text-xs font-black uppercase tracking-widest mb-2">Prenom</label>
<input type="text" name="firstName" value="{{ app.user.firstName }}" required <input type="text" id="firstName" name="firstName" value="{{ app.user.firstName }}" required
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Nom</label> <label for="lastName" class="block text-xs font-black uppercase tracking-widest mb-2">Nom</label>
<input type="text" name="lastName" value="{{ app.user.lastName }}" required <input type="text" id="lastName" name="lastName" value="{{ app.user.lastName }}" required
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Email</label> <label for="email" class="block text-xs font-black uppercase tracking-widest mb-2">Email</label>
<input type="email" name="email" value="{{ app.user.email }}" required <input type="email" id="email" name="email" value="{{ app.user.email }}" required
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
{% if app.user.keycloakId %} {% if app.user.keycloakId %}
@@ -152,21 +152,21 @@
<div class="border-2 border-gray-900 bg-white p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)]"> <div class="border-2 border-gray-900 bg-white p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)]">
<form method="post" action="{{ path('app_admin_profil_password') }}" class="flex flex-col gap-4"> <form method="post" action="{{ path('app_admin_profil_password') }}" class="flex flex-col gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Mot de passe actuel</label> <label for="current_password" class="block text-xs font-black uppercase tracking-widest mb-2">Mot de passe actuel</label>
<input type="password" name="current_password" required <input type="password" id="current_password" name="current_password" required
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]" class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"
placeholder="Votre mot de passe actuel"> placeholder="Votre mot de passe actuel">
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Nouveau mot de passe</label> <label for="new_password" class="block text-xs font-black uppercase tracking-widest mb-2">Nouveau mot de passe</label>
<input type="password" name="new_password" required minlength="8" <input type="password" id="new_password" name="new_password" required minlength="8"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]" class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"
placeholder="Minimum 8 caracteres"> placeholder="Minimum 8 caracteres">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Confirmer</label> <label for="confirm_password" class="block text-xs font-black uppercase tracking-widest mb-2">Confirmer</label>
<input type="password" name="confirm_password" required minlength="8" <input type="password" id="confirm_password" name="confirm_password" required minlength="8"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]" class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"
placeholder="Retapez le mot de passe"> placeholder="Retapez le mot de passe">
</div> </div>

View File

@@ -23,18 +23,18 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Identite</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-4">Identite</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Prenom *</label> <label for="firstName" class="block text-xs font-black uppercase tracking-widest mb-2">Prenom *</label>
<input type="text" name="firstName" required placeholder="Prenom" <input type="text" id="firstName" name="firstName" required placeholder="Prenom"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Nom *</label> <label for="lastName" class="block text-xs font-black uppercase tracking-widest mb-2">Nom *</label>
<input type="text" name="lastName" required placeholder="Nom" <input type="text" id="lastName" name="lastName" required placeholder="Nom"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Email *</label> <label for="email" class="block text-xs font-black uppercase tracking-widest mb-2">Email *</label>
<input type="email" name="email" required placeholder="email@exemple.fr" <input type="email" id="email" name="email" required placeholder="email@exemple.fr"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>
@@ -44,18 +44,18 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Entreprise</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-4">Entreprise</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Raison sociale</label> <label for="raisonSociale" class="block text-xs font-black uppercase tracking-widest mb-2">Raison sociale</label>
<input type="text" name="raisonSociale" placeholder="Nom de l'entreprise" <input type="text" id="raisonSociale" name="raisonSociale" placeholder="Nom de l'entreprise"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">SIRET</label> <label for="siret" class="block text-xs font-black uppercase tracking-widest mb-2">SIRET</label>
<input type="text" name="siret" maxlength="14" placeholder="12345678901234" <input type="text" id="siret" name="siret" maxlength="14" placeholder="12345678901234"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Telephone</label> <label for="phone" class="block text-xs font-black uppercase tracking-widest mb-2">Telephone</label>
<input type="tel" name="phone" placeholder="06 12 34 56 78" <input type="tel" id="phone" name="phone" placeholder="06 12 34 56 78"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>
@@ -65,19 +65,19 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Adresse</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-4">Adresse</h2>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Adresse</label> <label for="address" class="block text-xs font-black uppercase tracking-widest mb-2">Adresse</label>
<input type="text" name="address" placeholder="Numero et rue" <input type="text" id="address" name="address" placeholder="Numero et rue"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Code postal</label> <label for="zipCode" class="block text-xs font-black uppercase tracking-widest mb-2">Code postal</label>
<input type="text" name="zipCode" maxlength="10" placeholder="02800" <input type="text" id="zipCode" name="zipCode" maxlength="10" placeholder="02800"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Ville</label> <label for="city" class="block text-xs font-black uppercase tracking-widest mb-2">Ville</label>
<input type="text" name="city" placeholder="Beautor" <input type="text" id="city" name="city" placeholder="Beautor"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>

View File

@@ -25,24 +25,24 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Entreprise</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-4">Entreprise</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Raison sociale</label> <label for="raisonSociale" class="block text-xs font-black uppercase tracking-widest mb-2">Raison sociale</label>
<input type="text" name="raisonSociale" value="{{ revendeur.raisonSociale }}" placeholder="Nom de l'entreprise" <input type="text" id="raisonSociale" name="raisonSociale" value="{{ revendeur.raisonSociale }}" placeholder="Nom de l'entreprise"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">SIRET</label> <label for="siret" class="block text-xs font-black uppercase tracking-widest mb-2">SIRET</label>
<input type="text" name="siret" value="{{ revendeur.siret }}" maxlength="14" placeholder="12345678901234" <input type="text" id="siret" name="siret" value="{{ revendeur.siret }}" maxlength="14" placeholder="12345678901234"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Telephone</label> <label for="phone" class="block text-xs font-black uppercase tracking-widest mb-2">Telephone</label>
<input type="tel" name="phone" value="{{ revendeur.phone }}" placeholder="06 12 34 56 78" <input type="tel" id="phone" name="phone" value="{{ revendeur.phone }}" placeholder="06 12 34 56 78"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<label class="block text-xs font-black uppercase tracking-widest mb-2">Email</label> <label for="email" class="block text-xs font-black uppercase tracking-widest mb-2">Email</label>
<input type="email" name="email" value="{{ revendeur.email }}" placeholder="email@exemple.fr" <input type="email" id="email" name="email" value="{{ revendeur.email }}" placeholder="email@exemple.fr"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</section> </section>
@@ -51,19 +51,19 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Adresse</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-4">Adresse</h2>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Adresse</label> <label for="address" class="block text-xs font-black uppercase tracking-widest mb-2">Adresse</label>
<input type="text" name="address" value="{{ revendeur.address }}" placeholder="Numero et rue" <input type="text" id="address" name="address" value="{{ revendeur.address }}" placeholder="Numero et rue"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Code postal</label> <label for="zipCode" class="block text-xs font-black uppercase tracking-widest mb-2">Code postal</label>
<input type="text" name="zipCode" value="{{ revendeur.zipCode }}" maxlength="10" placeholder="02800" <input type="text" id="zipCode" name="zipCode" value="{{ revendeur.zipCode }}" maxlength="10" placeholder="02800"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
<div> <div>
<label class="block text-xs font-black uppercase tracking-widest mb-2">Ville</label> <label for="city" class="block text-xs font-black uppercase tracking-widest mb-2">Ville</label>
<input type="text" name="city" value="{{ revendeur.city }}" placeholder="Beautor" <input type="text" id="city" name="city" value="{{ revendeur.city }}" placeholder="Beautor"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600 focus:shadow-[4px_4px_0px_rgba(79,70,229,0.3)]">
</div> </div>
</div> </div>

View File

@@ -10,7 +10,7 @@
<form method="get" action="{{ path('app_admin_stats_index') }}" class="mb-6 border-2 border-gray-900 bg-white p-4 shadow-[4px_4px_0px_rgba(0,0,0,1)]"> <form method="get" action="{{ path('app_admin_stats_index') }}" class="mb-6 border-2 border-gray-900 bg-white p-4 shadow-[4px_4px_0px_rgba(0,0,0,1)]">
<div class="flex flex-wrap items-end gap-3"> <div class="flex flex-wrap items-end gap-3">
<div> <div>
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Periode</label> <label for="stats-period-select" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Periode</label>
<select name="period" id="stats-period-select" class="px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white"> <select name="period" id="stats-period-select" class="px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white">
<option value="current" {{ period == 'current' ? 'selected' }}>Mois en cours</option> <option value="current" {{ period == 'current' ? 'selected' }}>Mois en cours</option>
<option value="1" {{ period == '1' ? 'selected' }}>-1 mois</option> <option value="1" {{ period == '1' ? 'selected' }}>-1 mois</option>
@@ -25,12 +25,12 @@
</div> </div>
<div id="stats-custom-range" class="{{ period == 'custom' ? '' : 'hidden' }} flex items-end gap-3"> <div id="stats-custom-range" class="{{ period == 'custom' ? '' : 'hidden' }} flex items-end gap-3">
<div> <div>
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Du</label> <label for="from" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Du</label>
<input type="date" name="from" value="{{ dateFrom }}" class="px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white"> <input type="date" id="from" name="from" value="{{ dateFrom }}" class="px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white">
</div> </div>
<div> <div>
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Au</label> <label for="to" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Au</label>
<input type="date" name="to" value="{{ dateTo }}" class="px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white"> <input type="date" id="to" name="to" value="{{ dateTo }}" class="px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white">
</div> </div>
</div> </div>
<button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all"> <button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all">

View File

@@ -54,29 +54,29 @@
<h2 class="text-sm font-black uppercase tracking-widest mb-3">Publier un message</h2> <h2 class="text-sm font-black uppercase tracking-widest mb-3">Publier un message</h2>
<form method="post" action="{{ path('app_admin_status_message_create') }}" class="flex flex-wrap items-end gap-3"> <form method="post" action="{{ path('app_admin_status_message_create') }}" class="flex flex-wrap items-end gap-3">
<div class="w-40"> <div class="w-40">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Service</label> <label for="service_id" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Service</label>
<select name="service_id" required class="w-full px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white"> <select id="service_id" name="service_id" required class="w-full px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white">
{% for service in allServices %} {% for service in allServices %}
<option value="{{ service.id }}">{{ service.name }}</option> <option value="{{ service.id }}">{{ service.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="w-24"> <div class="w-24">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Severite</label> <label for="severity" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Severite</label>
<select name="severity" class="w-full px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white"> <select id="severity" name="severity" class="w-full px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white">
<option value="info">Info</option> <option value="info">Info</option>
<option value="warning">Warning</option> <option value="warning">Warning</option>
<option value="critical">Critical</option> <option value="critical">Critical</option>
</select> </select>
</div> </div>
<div class="flex-1 min-w-[120px]"> <div class="flex-1 min-w-[120px]">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Titre</label> <label for="title" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Titre</label>
<input type="text" name="title" required placeholder="Ex: Maintenance prevue..." <input id="title" type="text" name="title" required placeholder="Ex: Maintenance prevue..."
class="w-full px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white"> class="w-full px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white">
</div> </div>
<div class="flex-1 min-w-[150px]"> <div class="flex-1 min-w-[150px]">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Message</label> <label for="content" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Message</label>
<input type="text" name="content" required placeholder="Details..." <input id="content" type="text" name="content" required placeholder="Details..."
class="w-full px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white"> class="w-full px-3 py-2 border-2 border-gray-900 text-xs font-bold bg-white">
</div> </div>
<button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all"> <button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all">
@@ -155,8 +155,8 @@
<div class="px-4 py-3 border-t border-gray-200 bg-gray-50"> <div class="px-4 py-3 border-t border-gray-200 bg-gray-50">
<form method="post" action="{{ path('app_admin_status_update', {id: service.id}) }}" class="flex flex-wrap items-end gap-3"> <form method="post" action="{{ path('app_admin_status_update', {id: service.id}) }}" class="flex flex-wrap items-end gap-3">
<div> <div>
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Statut</label> <label for="status_{{ service.id }}" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Statut</label>
<select name="status" class="px-3 py-1.5 border-2 border-gray-900 text-xs font-bold bg-white"> <select id="status_{{ service.id }}" name="status" class="px-3 py-1.5 border-2 border-gray-900 text-xs font-bold bg-white">
<option value="up" {{ service.status == 'up' ? 'selected' }}>Up</option> <option value="up" {{ service.status == 'up' ? 'selected' }}>Up</option>
<option value="degraded" {{ service.status == 'degraded' ? 'selected' }}>Degrade</option> <option value="degraded" {{ service.status == 'degraded' ? 'selected' }}>Degrade</option>
<option value="down" {{ service.status == 'down' ? 'selected' }}>Down</option> <option value="down" {{ service.status == 'down' ? 'selected' }}>Down</option>
@@ -165,8 +165,8 @@
</select> </select>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Message</label> <label for="message_{{ service.id }}" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Message</label>
<input type="text" name="message" value="{{ service.message }}" placeholder="Message optionnel..." <input id="message_{{ service.id }}" type="text" name="message" value="{{ service.message }}" placeholder="Message optionnel..."
class="w-full px-3 py-1.5 border-2 border-gray-900 text-xs font-bold bg-white"> class="w-full px-3 py-1.5 border-2 border-gray-900 text-xs font-bold bg-white">
</div> </div>
<button type="submit" class="px-4 py-1.5 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all"> <button type="submit" class="px-4 py-1.5 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all">

View File

@@ -25,13 +25,13 @@
<div class="border-2 border-gray-900 bg-white p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)]"> <div class="border-2 border-gray-900 bg-white p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)]">
<form method="post" action="{{ path('app_admin_status_category_create') }}" class="flex flex-wrap items-end gap-4"> <form method="post" action="{{ path('app_admin_status_category_create') }}" class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
<label class="block text-xs font-black uppercase tracking-widest mb-2">Nom</label> <label for="category_name" class="block text-xs font-black uppercase tracking-widest mb-2">Nom</label>
<input type="text" name="name" required placeholder="Ex: Hebergement Web" <input id="category_name" type="text" name="name" required placeholder="Ex: Hebergement Web"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600">
</div> </div>
<div class="w-24"> <div class="w-24">
<label class="block text-xs font-black uppercase tracking-widest mb-2">Position</label> <label for="category_position" class="block text-xs font-black uppercase tracking-widest mb-2">Position</label>
<input type="number" name="position" value="0" <input id="category_position" type="number" name="position" value="0"
class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600"> class="w-full px-4 py-3 border-2 border-gray-900 bg-white text-sm font-bold focus:outline-none focus:border-indigo-600">
</div> </div>
<button type="submit" class="px-6 py-3 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-sm tracking-widest hover:bg-yellow-500 transition-all shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-[2px_2px_0px_rgba(0,0,0,1)] active:shadow-none active:translate-x-1 active:translate-y-1"> <button type="submit" class="px-6 py-3 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-sm tracking-widest hover:bg-yellow-500 transition-all shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-[2px_2px_0px_rgba(0,0,0,1)] active:shadow-none active:translate-x-1 active:translate-y-1">
@@ -98,18 +98,18 @@
<form method="post" action="{{ path('app_admin_status_service_create') }}" class="flex flex-wrap items-end gap-3"> <form method="post" action="{{ path('app_admin_status_service_create') }}" class="flex flex-wrap items-end gap-3">
<input type="hidden" name="category_id" value="{{ category.id }}"> <input type="hidden" name="category_id" value="{{ category.id }}">
<div class="flex-1 min-w-[120px]"> <div class="flex-1 min-w-[120px]">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Nom</label> <label for="service_name_{{ category.id }}" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Nom</label>
<input type="text" name="name" required placeholder="Ex: CRM Ecosplay" <input id="service_name_{{ category.id }}" type="text" name="name" required placeholder="Ex: CRM Ecosplay"
class="w-full px-3 py-2 border-2 border-gray-900 bg-white text-xs font-bold focus:outline-none focus:border-indigo-600"> class="w-full px-3 py-2 border-2 border-gray-900 bg-white text-xs font-bold focus:outline-none focus:border-indigo-600">
</div> </div>
<div class="flex-1 min-w-[120px]"> <div class="flex-1 min-w-[120px]">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">URL</label> <label for="service_url_{{ category.id }}" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">URL</label>
<input type="url" name="url" placeholder="https://..." <input id="service_url_{{ category.id }}" type="url" name="url" placeholder="https://..."
class="w-full px-3 py-2 border-2 border-gray-900 bg-white text-xs font-bold focus:outline-none focus:border-indigo-600"> class="w-full px-3 py-2 border-2 border-gray-900 bg-white text-xs font-bold focus:outline-none focus:border-indigo-600">
</div> </div>
<div class="w-28"> <div class="w-28">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Type</label> <label for="service_external_type_{{ category.id }}" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Type</label>
<select name="external_type" class="w-full px-3 py-2 border-2 border-gray-900 bg-white text-xs font-bold"> <select id="service_external_type_{{ category.id }}" name="external_type" class="w-full px-3 py-2 border-2 border-gray-900 bg-white text-xs font-bold">
<option value="">Interne</option> <option value="">Interne</option>
<option value="stripe">Stripe</option> <option value="stripe">Stripe</option>
<option value="docuseal">DocuSeal</option> <option value="docuseal">DocuSeal</option>
@@ -118,8 +118,8 @@
</select> </select>
</div> </div>
<div class="w-16"> <div class="w-16">
<label class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Pos.</label> <label for="service_position_{{ category.id }}" class="block text-[9px] font-black uppercase tracking-widest text-gray-400 mb-1">Pos.</label>
<input type="number" name="position" value="0" <input id="service_position_{{ category.id }}" type="number" name="position" value="0"
class="w-full px-3 py-2 border-2 border-gray-900 bg-white text-xs font-bold focus:outline-none focus:border-indigo-600"> class="w-full px-3 py-2 border-2 border-gray-900 bg-white text-xs font-bold focus:outline-none focus:border-indigo-600">
</div> </div>
<button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all"> <button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-[10px] tracking-widest hover:bg-yellow-500 transition-all">

View File

@@ -6,13 +6,13 @@
<title>{% block title %}CRM Ecosplay{% endblock %}</title> <title>{% block title %}CRM Ecosplay{% endblock %}</title>
</head> </head>
<body style="margin: 0; padding: 0; background-color: #fbfbfb; font-family: Arial, Helvetica, sans-serif; color: #111827;"> <body style="margin: 0; padding: 0; background-color: #fbfbfb; font-family: Arial, Helvetica, sans-serif; color: #111827;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #fbfbfb; padding: 40px 0;"> <table role="presentation" width="100%" style="background-color: #fbfbfb; padding: 40px 0;">
<tr> <tr>
<td align="center"> <td align="center">
<p style="font-size: 11px; color: #666; margin: 0 0 16px 0; text-align: center;"> <p style="font-size: 11px; color: #666; margin: 0 0 16px 0; text-align: center;">
Cet email ne s'affiche pas correctement ? <a href="__VIEW_URL__" style="color: #4338ca; text-decoration: underline;">Voir en ligne</a> Cet email ne s'affiche pas correctement ? <a href="__VIEW_URL__" style="color: #4338ca; text-decoration: underline;">Voir en ligne</a>
</p> </p>
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border: 4px solid #111827;"> <table role="presentation" width="600" style="background-color: #ffffff; border: 4px solid #111827;">
<tr> <tr>
<td style="background-color: #fabf04; border-bottom: 4px solid #111827; padding: 20px; text-align: center;"> <td style="background-color: #fabf04; border-bottom: 4px solid #111827; padding: 20px; text-align: center;">
<img src="https://crm.e-cosplay.fr/logo.jpg" alt="CRM Ecosplay" width="120" style="display: block; margin: 0 auto;"> <img src="https://crm.e-cosplay.fr/logo.jpg" alt="CRM Ecosplay" width="120" style="display: block; margin: 0 auto;">

View File

@@ -8,7 +8,7 @@
<div style="background: #111827; color: #fff; padding: 20px; margin: 20px 0;"> <div style="background: #111827; color: #fff; padding: 20px; margin: 20px 0;">
<p style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin: 0 0 12px; opacity: 0.7;">Vos identifiants de connexion</p> <p style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin: 0 0 12px; opacity: 0.7;">Vos identifiants de connexion</p>
<table cellpadding="0" cellspacing="0" style="width: 100%; font-size: 14px;"> <table style="width: 100%; font-size: 14px;">
<tr> <tr>
<td style="color: #fabf04; font-weight: bold; padding: 4px 0; width: 120px;">Email</td> <td style="color: #fabf04; font-weight: bold; padding: 4px 0; width: 120px;">Email</td>
<td style="color: #fff; padding: 4px 0;">{{ email }}</td> <td style="color: #fff; padding: 4px 0;">{{ email }}</td>
@@ -45,7 +45,7 @@
</ul> </ul>
{% endif %} {% endif %}
<table cellpadding="0" cellspacing="0" style="margin: 24px 0;"> <table style="margin: 24px 0;">
<tr> <tr>
<td style="background: #fabf04; border: 2px solid #111827; padding: 12px 24px;"> <td style="background: #fabf04; border: 2px solid #111827; padding: 12px 24px;">
<a href="{{ url('app_home') }}" style="color: #111827; font-weight: 900; text-transform: uppercase; font-size: 13px; text-decoration: none; letter-spacing: 1px;">Se connecter</a> <a href="{{ url('app_home') }}" style="color: #111827; font-weight: 900; text-transform: uppercase; font-size: 13px; text-decoration: none; letter-spacing: 1px;">Se connecter</a>

View File

@@ -13,7 +13,7 @@
<p style="font-size: 14px; line-height: 1.6; margin: 0 0 8px; font-weight: bold;">Pour acceder a votre espace, suivez ces etapes :</p> <p style="font-size: 14px; line-height: 1.6; margin: 0 0 8px; font-weight: bold;">Pour acceder a votre espace, suivez ces etapes :</p>
<table cellpadding="0" cellspacing="0" style="width: 100%; margin: 16px 0;"> <table style="width: 100%; margin: 16px 0;">
<tr> <tr>
<td style="padding: 12px 16px; border-left: 4px solid #fabf04; background: #f9fafb;"> <td style="padding: 12px 16px; border-left: 4px solid #fabf04; background: #f9fafb;">
<p style="font-size: 12px; font-weight: 900; text-transform: uppercase; color: #fabf04; margin: 0 0 4px;">Etape 1</p> <p style="font-size: 12px; font-weight: 900; text-transform: uppercase; color: #fabf04; margin: 0 0 4px;">Etape 1</p>
@@ -36,7 +36,7 @@
</tr> </tr>
</table> </table>
<table cellpadding="0" cellspacing="0" style="margin: 24px 0; width: 100%;"> <table style="margin: 24px 0; width: 100%;">
<tr> <tr>
<td style="background: #fabf04; border: 2px solid #111827; padding: 14px 24px; text-align: center;"> <td style="background: #fabf04; border: 2px solid #111827; padding: 14px 24px; text-align: center;">
<a href="{{ setPasswordUrl }}" style="color: #111827; font-weight: 900; text-transform: uppercase; font-size: 14px; text-decoration: none; letter-spacing: 1px;">Definir mon mot de passe</a> <a href="{{ setPasswordUrl }}" style="color: #111827; font-weight: 900; text-transform: uppercase; font-size: 14px; text-decoration: none; letter-spacing: 1px;">Definir mon mot de passe</a>
@@ -44,7 +44,7 @@
</tr> </tr>
</table> </table>
<table cellpadding="0" cellspacing="0" style="margin: 0 0 24px; width: 100%;"> <table style="margin: 0 0 24px; width: 100%;">
<tr> <tr>
<td style="background: #fff; border: 2px solid #111827; padding: 12px 24px; text-align: center;"> <td style="background: #fff; border: 2px solid #111827; padding: 12px 24px; text-align: center;">
<a href="{{ url('app_home') }}" style="color: #111827; font-weight: 900; text-transform: uppercase; font-size: 12px; text-decoration: none; letter-spacing: 1px;">Acceder a la page de connexion</a> <a href="{{ url('app_home') }}" style="color: #111827; font-weight: 900; text-transform: uppercase; font-size: 12px; text-decoration: none; letter-spacing: 1px;">Acceder a la page de connexion</a>
@@ -54,7 +54,7 @@
<div style="background: #f9fafb; padding: 16px; border: 1px solid #e5e7eb; margin: 16px 0;"> <div style="background: #f9fafb; padding: 16px; border: 1px solid #e5e7eb; margin: 16px 0;">
<p style="font-size: 12px; font-weight: bold; margin: 0 0 8px;">Informations de votre compte :</p> <p style="font-size: 12px; font-weight: bold; margin: 0 0 8px;">Informations de votre compte :</p>
<table cellpadding="0" cellspacing="0" style="font-size: 12px; width: 100%;"> <table style="font-size: 12px; width: 100%;">
<tr> <tr>
<td style="padding: 2px 0; color: #666; width: 120px;">Code revendeur</td> <td style="padding: 2px 0; color: #666; width: 120px;">Code revendeur</td>
<td style="padding: 2px 0; font-weight: bold; font-family: monospace;">{{ codeRevendeur }}</td> <td style="padding: 2px 0; font-weight: bold; font-family: monospace;">{{ codeRevendeur }}</td>

View File

@@ -6,7 +6,7 @@
<h1 style="font-size: 20px; font-weight: bold; text-transform: uppercase; margin: 0 0 16px 0;">Attestation RGPD signee</h1> <h1 style="font-size: 20px; font-weight: bold; text-transform: uppercase; margin: 0 0 16px 0;">Attestation RGPD signee</h1>
<p style="font-size: 14px; line-height: 1.6; margin: 0 0 16px 0;">Bonjour,</p> <p style="font-size: 14px; line-height: 1.6; margin: 0 0 16px 0;">Bonjour,</p>
<p style="font-size: 14px; line-height: 1.6; margin: 0 0 16px 0;">Suite a votre demande, vous trouverez en piece jointe votre <strong>attestation d'{{ typeName }}</strong>, signee electroniquement par l'association E-Cosplay.</p> <p style="font-size: 14px; line-height: 1.6; margin: 0 0 16px 0;">Suite a votre demande, vous trouverez en piece jointe votre <strong>attestation d'{{ typeName }}</strong>, signee electroniquement par l'association E-Cosplay.</p>
<table cellpadding="0" cellspacing="0" style="margin: 16px 0; font-size: 13px; width: 100%;"> <table style="margin: 16px 0; font-size: 13px; width: 100%;">
<tr> <tr>
<td style="padding: 8px 12px; background: #f9fafb; border-left: 3px solid #fabf04; font-weight: bold;">Reference</td> <td style="padding: 8px 12px; background: #f9fafb; border-left: 3px solid #fabf04; font-weight: bold;">Reference</td>
<td style="padding: 8px 12px; background: #f9fafb;">{{ attestation.reference }}</td> <td style="padding: 8px 12px; background: #f9fafb;">{{ attestation.reference }}</td>

View File

@@ -2,6 +2,7 @@
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Contrat Revendeur - E-Cosplay</title>
<style> <style>
@page { margin: 0; size: A4; } @page { margin: 0; size: A4; }
body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #111827; margin: 0; padding: 0; line-height: 1.6; } body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #111827; margin: 0; padding: 0; line-height: 1.6; }
@@ -39,7 +40,7 @@
<p style="margin-bottom: 12px; font-weight: 700;">Entre les soussignes :</p> <p style="margin-bottom: 12px; font-weight: 700;">Entre les soussignes :</p>
<table class="parties" cellpadding="0" cellspacing="0"> <table class="parties">
<tr> <tr>
<td class="left"> <td class="left">
<span class="label">Le Prestataire</span> <span class="label">Le Prestataire</span>
@@ -183,7 +184,7 @@
<p style="margin-top: 14px; font-weight: 700;">Fait en deux exemplaires, a Beautor, le {{ date|date('d/m/Y') }}.</p> <p style="margin-top: 14px; font-weight: 700;">Fait en deux exemplaires, a Beautor, le {{ date|date('d/m/Y') }}.</p>
<table class="signatures" cellpadding="0" cellspacing="0"> <table class="signatures">
<tr> <tr>
<td> <td>
<span class="sig-label">Pour l'Association E-Cosplay</span> <span class="sig-label">Pour l'Association E-Cosplay</span>

View File

@@ -2,6 +2,7 @@
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Attestation RGPD - Acces aux donnees</title>
<style> <style>
@page { margin: 0; size: A4; } @page { margin: 0; size: A4; }
body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #111827; margin: 0; padding: 0; } body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #111827; margin: 0; padding: 0; }
@@ -47,7 +48,7 @@
<span class="doc-type">Droit d'acces</span> <span class="doc-type">Droit d'acces</span>
<h1>Donnees personnelles</h1> <h1>Donnees personnelles</h1>
<div class="subtitle">RGPD &mdash; Article 15</div> <div class="subtitle">RGPD &mdash; Article 15</div>
<table class="info-grid" cellpadding="0" cellspacing="0"> <table class="info-grid">
<tr> <tr>
<td class="info-cell" width="25%"><span class="info-label">Reference</span><span class="info-value">{{ attestation.reference }}</span></td> <td class="info-cell" width="25%"><span class="info-label">Reference</span><span class="info-value">{{ attestation.reference }}</span></td>
<td class="info-cell" width="25%"><span class="info-label">Date</span><span class="info-value">{{ date|date('d/m/Y a H:i') }}</span></td> <td class="info-cell" width="25%"><span class="info-label">Date</span><span class="info-value">{{ date|date('d/m/Y a H:i') }}</span></td>
@@ -74,7 +75,7 @@
{% endfor %} {% endfor %}
<div class="verify-box"> <div class="verify-box">
<table cellpadding="0" cellspacing="0"> <table>
<tr> <tr>
<td class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></td> <td class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></td>
<td class="verify-info"> <td class="verify-info">

View File

@@ -2,6 +2,7 @@
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Attestation RGPD - Suppression des donnees</title>
<style> <style>
@page { margin: 0; size: A4; } @page { margin: 0; size: A4; }
body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #111827; margin: 0; padding: 0; } body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #111827; margin: 0; padding: 0; }
@@ -45,7 +46,7 @@
<span class="doc-type">Suppression definitive</span> <span class="doc-type">Suppression definitive</span>
<h1>Attestation de suppression des donnees</h1> <h1>Attestation de suppression des donnees</h1>
<div class="subtitle">RGPD &mdash; Article 17</div> <div class="subtitle">RGPD &mdash; Article 17</div>
<table class="info-grid" cellpadding="0" cellspacing="0"> <table class="info-grid">
<tr> <tr>
<td class="info-cell" width="33%"><span class="info-label">Reference</span><span class="info-value">{{ attestation.reference }}</span></td> <td class="info-cell" width="33%"><span class="info-label">Reference</span><span class="info-value">{{ attestation.reference }}</span></td>
<td class="info-cell" width="33%"><span class="info-label">Date</span><span class="info-value">{{ date|date('d/m/Y a H:i') }}</span></td> <td class="info-cell" width="33%"><span class="info-label">Date</span><span class="info-value">{{ date|date('d/m/Y a H:i') }}</span></td>
@@ -63,7 +64,7 @@
</div> </div>
<div class="warning">Cette suppression est irreversible. Aucune donnee relative a cette adresse IP ne subsiste dans nos bases de donnees.</div> <div class="warning">Cette suppression est irreversible. Aucune donnee relative a cette adresse IP ne subsiste dans nos bases de donnees.</div>
<div class="verify-box"> <div class="verify-box">
<table cellpadding="0" cellspacing="0"> <table>
<tr> <tr>
<td class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></td> <td class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></td>
<td class="verify-info"> <td class="verify-info">

View File

@@ -2,6 +2,7 @@
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Attestation RGPD - Absence de donnees</title>
<style> <style>
@page { margin: 0; size: A4; } @page { margin: 0; size: A4; }
body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #111827; margin: 0; padding: 0; } body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #111827; margin: 0; padding: 0; }
@@ -44,7 +45,7 @@
<span class="doc-type">Document officiel</span> <span class="doc-type">Document officiel</span>
<h1>Attestation d'absence de donnees</h1> <h1>Attestation d'absence de donnees</h1>
<div class="subtitle">RGPD &mdash; Article 15</div> <div class="subtitle">RGPD &mdash; Article 15</div>
<table class="info-grid" cellpadding="0" cellspacing="0"> <table class="info-grid">
<tr> <tr>
<td class="info-cell" width="33%"><span class="info-label">Reference</span><span class="info-value">{{ attestation.reference }}</span></td> <td class="info-cell" width="33%"><span class="info-label">Reference</span><span class="info-value">{{ attestation.reference }}</span></td>
<td class="info-cell" width="33%"><span class="info-label">Date</span><span class="info-value">{{ date|date('d/m/Y a H:i') }}</span></td> <td class="info-cell" width="33%"><span class="info-label">Date</span><span class="info-value">{{ date|date('d/m/Y a H:i') }}</span></td>
@@ -62,7 +63,7 @@
<p>Aucune donnee personnelle (identifiants de session, evenements de navigation, informations techniques) n'est stockee dans nos bases de donnees pour cette adresse IP.</p> <p>Aucune donnee personnelle (identifiants de session, evenements de navigation, informations techniques) n'est stockee dans nos bases de donnees pour cette adresse IP.</p>
</div> </div>
<div class="verify-box"> <div class="verify-box">
<table cellpadding="0" cellspacing="0"> <table>
<tr> <tr>
<td class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></td> <td class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></td>
<td class="verify-info"> <td class="verify-info">

View File

@@ -8,14 +8,17 @@ use App\Security\KeycloakAuthenticator;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface; use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken; use League\OAuth2\Client\Token\AccessToken;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface; use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class KeycloakAuthenticatorTest extends TestCase class KeycloakAuthenticatorTest extends TestCase
@@ -28,16 +31,16 @@ class KeycloakAuthenticatorTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$this->clientRegistry = $this->createMock(ClientRegistry::class); $this->clientRegistry = $this->createStub(ClientRegistry::class);
$this->em = $this->createMock(EntityManagerInterface::class); $this->em = $this->createStub(EntityManagerInterface::class);
$this->userRepository = $this->createMock(UserRepository::class); $this->userRepository = $this->createStub(UserRepository::class);
$this->router = $this->createMock(RouterInterface::class); $this->router = $this->createStub(RouterInterface::class);
$this->authenticator = new KeycloakAuthenticator( $this->authenticator = new KeycloakAuthenticator(
$this->clientRegistry, $this->clientRegistry,
$this->em, $this->em,
$this->userRepository, $this->userRepository,
$this->router $this->router,
); );
} }
@@ -54,34 +57,44 @@ class KeycloakAuthenticatorTest extends TestCase
public function testAuthenticate(): void public function testAuthenticate(): void
{ {
$request = new Request(); $request = new Request();
$client = $this->createMock(OAuth2ClientInterface::class); $client = $this->createStub(OAuth2ClientInterface::class);
$accessToken = new AccessToken(['access_token' => 'fake-token']); $accessToken = new AccessToken(['access_token' => 'fake-token']);
$this->clientRegistry->method('getClient')->with('keycloak')->willReturn($client); $this->clientRegistry->method('getClient')->willReturn($client);
$client->method('getAccessToken')->willReturn($accessToken); $client->method('getAccessToken')->willReturn($accessToken);
$passport = $this->authenticator->authenticate($request); $passport = $this->authenticator->authenticate($request);
$this->assertInstanceOf(SelfValidatingPassport::class, $passport); $this->assertInstanceOf(SelfValidatingPassport::class, $passport);
$userBadge = $passport->getBadge(\Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge::class); $userBadge = $passport->getBadge(UserBadge::class);
$this->assertNotNull($userBadge); $this->assertNotNull($userBadge);
// Test the user loader callback $keycloakUser = $this->createStub(ResourceOwnerInterface::class);
$keycloakUser = $this->createMock(\League\OAuth2\Client\Provider\ResourceOwnerInterface::class);
$keycloakUser->method('toArray')->willReturn([ $keycloakUser->method('toArray')->willReturn([
'sub' => '123', 'sub' => '123',
'email' => 'test@example.com', 'email' => 'test@example.com',
'given_name' => 'John', 'given_name' => 'John',
'family_name' => 'Doe', 'family_name' => 'Doe',
'groups' => ['super_admin_asso'] 'groups' => ['super_admin_asso'],
]); ]);
$client->method('fetchUserFromToken')->willReturn($keycloakUser); $client->method('fetchUserFromToken')->willReturn($keycloakUser);
$this->userRepository->method('findOneBy')->willReturn(null); $this->userRepository->method('findOneBy')->willReturn(null);
$this->em->expects($this->once())->method('persist'); $em = $this->createMock(EntityManagerInterface::class);
$this->em->expects($this->once())->method('flush'); $em->expects($this->once())->method('persist');
$em->expects($this->once())->method('flush');
// Recreate authenticator with mock em for expectations
$authenticator = new KeycloakAuthenticator(
$this->clientRegistry,
$em,
$this->userRepository,
$this->router,
);
$passport = $authenticator->authenticate($request);
$userBadge = $passport->getBadge(UserBadge::class);
$user = $userBadge->getUser(); $user = $userBadge->getUser();
$this->assertInstanceOf(User::class, $user); $this->assertInstanceOf(User::class, $user);
@@ -93,27 +106,28 @@ class KeycloakAuthenticatorTest extends TestCase
public function testAuthenticateExistingUserByEmail(): void public function testAuthenticateExistingUserByEmail(): void
{ {
$request = new Request(); $request = new Request();
$client = $this->createMock(OAuth2ClientInterface::class); $client = $this->createStub(OAuth2ClientInterface::class);
$accessToken = new AccessToken(['access_token' => 'fake-token']); $accessToken = new AccessToken(['access_token' => 'fake-token']);
$this->clientRegistry->method('getClient')->willReturn($client); $this->clientRegistry->method('getClient')->willReturn($client);
$client->method('getAccessToken')->willReturn($accessToken); $client->method('getAccessToken')->willReturn($accessToken);
$passport = $this->authenticator->authenticate($request); $passport = $this->authenticator->authenticate($request);
$userBadge = $passport->getBadge(\Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge::class); $userBadge = $passport->getBadge(UserBadge::class);
$keycloakUser = $this->createMock(\League\OAuth2\Client\Provider\ResourceOwnerInterface::class); $keycloakUser = $this->createStub(ResourceOwnerInterface::class);
$keycloakUser->method('toArray')->willReturn([ $keycloakUser->method('toArray')->willReturn([
'sub' => '123', 'sub' => '123',
'email' => 'existing@example.com', 'email' => 'existing@example.com',
'groups' => [] 'groups' => [],
]); ]);
$client->method('fetchUserFromToken')->willReturn($keycloakUser); $client->method('fetchUserFromToken')->willReturn($keycloakUser);
$existingUser = new User(); $existingUser = new User();
$this->userRepository->method('findOneBy')->willReturnCallback(function($criteria) use ($existingUser) { $this->userRepository->method('findOneBy')->willReturnCallback(function ($criteria) use ($existingUser) {
if (isset($criteria['email']) && $criteria['email'] === 'existing@example.com') { if (isset($criteria['email']) && 'existing@example.com' === $criteria['email']) {
return $existingUser; return $existingUser;
} }
return null; return null;
}); });
@@ -126,8 +140,8 @@ class KeycloakAuthenticatorTest extends TestCase
public function testOnAuthenticationSuccess(): void public function testOnAuthenticationSuccess(): void
{ {
$request = new Request(); $request = new Request();
$token = $this->createMock(TokenInterface::class); $token = $this->createStub(TokenInterface::class);
$this->router->method('generate')->with('app_home')->willReturn('/home'); $this->router->method('generate')->willReturn('/home');
$response = $this->authenticator->onAuthenticationSuccess($request, $token, 'main'); $response = $this->authenticator->onAuthenticationSuccess($request, $token, 'main');
$this->assertInstanceOf(RedirectResponse::class, $response); $this->assertInstanceOf(RedirectResponse::class, $response);
@@ -136,16 +150,16 @@ class KeycloakAuthenticatorTest extends TestCase
public function testOnAuthenticationFailure(): void public function testOnAuthenticationFailure(): void
{ {
$request = $this->createMock(Request::class); $request = $this->createStub(Request::class);
$session = $this->createMock(\Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface::class);
$flashBag = $this->createMock(\Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface::class);
$request->method('getSession')->willReturn($session); $flashBag = $this->createMock(FlashBagInterface::class);
$session->method('getFlashBag')->willReturn($flashBag);
$flashBag->expects($this->once())->method('add')->with('error', $this->anything()); $flashBag->expects($this->once())->method('add')->with('error', $this->anything());
$this->router->method('generate')->with('app_home')->willReturn('/home'); $session = $this->createStub(FlashBagAwareSessionInterface::class);
$session->method('getFlashBag')->willReturn($flashBag);
$request->method('getSession')->willReturn($session);
$this->router->method('generate')->willReturn('/home');
$response = $this->authenticator->onAuthenticationFailure($request, new AuthenticationException()); $response = $this->authenticator->onAuthenticationFailure($request, new AuthenticationException());
$this->assertInstanceOf(RedirectResponse::class, $response); $this->assertInstanceOf(RedirectResponse::class, $response);
@@ -154,7 +168,7 @@ class KeycloakAuthenticatorTest extends TestCase
public function testStart(): void public function testStart(): void
{ {
$request = new Request(); $request = new Request();
$this->router->method('generate')->with('app_home')->willReturn('/home'); $this->router->method('generate')->willReturn('/home');
$response = $this->authenticator->start($request); $response = $this->authenticator->start($request);
$this->assertInstanceOf(RedirectResponse::class, $response); $this->assertInstanceOf(RedirectResponse::class, $response);

View File

@@ -18,28 +18,26 @@ class LoginSuccessHandlerTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$this->router = $this->createMock(RouterInterface::class); $this->router = $this->createStub(RouterInterface::class);
$this->authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); $this->authorizationChecker = $this->createStub(AuthorizationCheckerInterface::class);
$this->handler = new LoginSuccessHandler($this->router, $this->authorizationChecker); $this->handler = new LoginSuccessHandler($this->router, $this->authorizationChecker);
} }
#[DataProvider('provideRolesAndRoutes')] #[DataProvider('provideRolesAndRoutes')]
public function testOnAuthenticationSuccess(string $role, string $routeName): void public function testOnAuthenticationSuccess(string $role, string $routeName): void
{ {
$request = $this->createMock(Request::class); $request = new Request();
$token = $this->createMock(TokenInterface::class); $token = $this->createStub(TokenInterface::class);
$this->authorizationChecker->method('isGranted') $this->authorizationChecker->method('isGranted')
->willReturnCallback(fn($r) => $r === $role); ->willReturnCallback(fn ($r) => $r === $role);
$this->router->expects($this->once()) $this->router->method('generate')
->method('generate') ->willReturnCallback(fn ($route) => '/'.$route);
->with($routeName)
->willReturn('/' . $routeName);
$response = $this->handler->onAuthenticationSuccess($request, $token); $response = $this->handler->onAuthenticationSuccess($request, $token);
$this->assertEquals('/' . $routeName, $response->getTargetUrl()); $this->assertEquals('/'.$routeName, $response->getTargetUrl());
} }
public static function provideRolesAndRoutes(): iterable public static function provideRolesAndRoutes(): iterable
@@ -51,15 +49,13 @@ class LoginSuccessHandlerTest extends TestCase
public function testOnAuthenticationSuccessDefault(): void public function testOnAuthenticationSuccessDefault(): void
{ {
$request = $this->createMock(Request::class); $request = new Request();
$token = $this->createMock(TokenInterface::class); $token = $this->createStub(TokenInterface::class);
$this->authorizationChecker->method('isGranted')->willReturn(false); $this->authorizationChecker->method('isGranted')->willReturn(false);
$this->router->expects($this->once()) $this->router->method('generate')
->method('generate') ->willReturnCallback(fn ($route) => '/');
->with('app_home')
->willReturn('/');
$response = $this->handler->onAuthenticationSuccess($request, $token); $response = $this->handler->onAuthenticationSuccess($request, $token);

View File

@@ -3,7 +3,6 @@
namespace App\Tests\Twig; namespace App\Tests\Twig;
use App\Twig\ViteAssetExtension; use App\Twig\ViteAssetExtension;
use Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
@@ -15,8 +14,8 @@ class ViteAssetExtensionTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$this->manifestPath = sys_get_temp_dir() . '/manifest.json'; $this->manifestPath = sys_get_temp_dir().'/manifest.json';
$this->cache = $this->createMock(CacheItemPoolInterface::class); $this->cache = $this->createStub(CacheItemPoolInterface::class);
} }
protected function tearDown(): void protected function tearDown(): void
@@ -48,10 +47,12 @@ class ViteAssetExtensionTest extends TestCase
public function testAssetDev(): void public function testAssetDev(): void
{ {
$_ENV['VITE_LOAD'] = '0'; $_ENV['VITE_LOAD'] = '0';
// Use a dummy to test nonce logic since we can't mock final CSP listener
$extension = new class($this->manifestPath, $this->cache) extends ViteAssetExtension { $extension = new class($this->manifestPath, $this->cache) extends ViteAssetExtension {
protected function getNonce(): string { return 'fake-nonce'; } protected function getNonce(): string
{
return 'fake-nonce';
}
}; };
$html = $extension->asset('app.js'); $html = $extension->asset('app.js');
@@ -63,11 +64,11 @@ class ViteAssetExtensionTest extends TestCase
public function testAssetProdWithCacheHit(): void public function testAssetProdWithCacheHit(): void
{ {
$_ENV['VITE_LOAD'] = '1'; $_ENV['VITE_LOAD'] = '1';
$item = $this->createMock(CacheItemInterface::class); $item = $this->createStub(CacheItemInterface::class);
$item->method('isHit')->willReturn(true); $item->method('isHit')->willReturn(true);
$item->method('get')->willReturn(['app.js' => ['file' => 'app.123.js', 'css' => ['app.123.css']]]); $item->method('get')->willReturn(['app.js' => ['file' => 'app.123.js', 'css' => ['app.123.css']]]);
$this->cache->method('getItem')->with(ViteAssetExtension::CACHE_KEY)->willReturn($item); $this->cache->method('getItem')->willReturn($item);
$extension = new ViteAssetExtension($this->manifestPath, $this->cache); $extension = new ViteAssetExtension($this->manifestPath, $this->cache);
$html = $extension->asset('app.js'); $html = $extension->asset('app.js');
@@ -79,7 +80,7 @@ class ViteAssetExtensionTest extends TestCase
public function testAssetProdWithCacheMissAndMissingFile(): void public function testAssetProdWithCacheMissAndMissingFile(): void
{ {
$_ENV['VITE_LOAD'] = '1'; $_ENV['VITE_LOAD'] = '1';
$item = $this->createMock(CacheItemInterface::class); $item = $this->createStub(CacheItemInterface::class);
$item->method('isHit')->willReturn(false); $item->method('isHit')->willReturn(false);
$this->cache->method('getItem')->willReturn($item); $this->cache->method('getItem')->willReturn($item);
@@ -100,12 +101,13 @@ class ViteAssetExtensionTest extends TestCase
$item = $this->createMock(CacheItemInterface::class); $item = $this->createMock(CacheItemInterface::class);
$item->method('isHit')->willReturn(false); $item->method('isHit')->willReturn(false);
$this->cache->method('getItem')->willReturn($item);
$item->expects($this->once())->method('set'); $item->expects($this->once())->method('set');
$this->cache->expects($this->once())->method('save');
$extension = new ViteAssetExtension($this->manifestPath, $this->cache); $cache = $this->createMock(CacheItemPoolInterface::class);
$cache->method('getItem')->willReturn($item);
$cache->expects($this->once())->method('save');
$extension = new ViteAssetExtension($this->manifestPath, $cache);
$html = $extension->asset('app.js'); $html = $extension->asset('app.js');
$this->assertStringContainsString('/build/app.456.js', $html); $this->assertStringContainsString('/build/app.456.js', $html);
@@ -123,11 +125,11 @@ class ViteAssetExtensionTest extends TestCase
public function testFaviconsProd(): void public function testFaviconsProd(): void
{ {
$_ENV['VITE_LOAD'] = '1'; $_ENV['VITE_LOAD'] = '1';
$item = $this->createMock(CacheItemInterface::class); $item = $this->createStub(CacheItemInterface::class);
$item->method('isHit')->willReturn(true); $item->method('isHit')->willReturn(true);
$item->method('get')->willReturn([ $item->method('get')->willReturn([
'favicon.png' => ['file' => 'favicon.789.png'], 'favicon.png' => ['file' => 'favicon.789.png'],
'app.js' => ['file' => 'app.js'] 'app.js' => ['file' => 'app.js'],
]); ]);
$this->cache->method('getItem')->willReturn($item); $this->cache->method('getItem')->willReturn($item);