8 Commits

Author SHA1 Message Date
4f7dc49f13 feat: refonte du design global (messages flash, navbar, pied-de-page) et simplification du code Twig 2026-04-09 19:46:01 +02:00
1bb651b7e4 feat: suppression d'une migration et amélioration des formulaires + messages d'erreur (révision design + placeholders ajoutés) 2026-04-09 19:36:55 +02:00
MLeveque
6f9523f9e7 fix(user): Appel à la route de modification d'un utilisateur dans l'API. (PATCH /ldap/user/update/{email})
- Adaptation de l'objet kazUser pour qu'il corresponde a l'attente de l'api.
2026-04-05 14:07:50 +02:00
7400d0d418 feat: mise à jour du profil utilisateur et intégration API Kaz (synchro des données, gestion des erreurs et succès) + divers correctifs (liens externes, simplifications de commentaires) 2026-04-04 12:53:48 +02:00
7a25779c9c feat: amélioration de la gestion des profils utilisateurs (suppression de profil_infos.html.twig, mise à jour des formulaires, meilleur affichage des quotas et permissions, et support des fichiers GIF) 2026-04-01 11:58:01 +02:00
b57236c4e7 feat : refonte complète des migrations de base de données et amorce de mise en page de la page de profil utilisateur (pseudo + autres infos en mode "modification".
Suppression des commentaires inutiles
2026-04-01 10:00:26 +02:00
f3822a60aa Merge branch 'main' of ssh://git.kaz.bzh:2202/melvin-leveque/interface-kaznautes into feat/login 2026-03-30 14:19:33 +02:00
d46b61a0ad Merge pull request 'feat/cnx_api' (#12) from feat/cnx_api into feat/login
Reviewed-on: #12
2026-03-30 12:10:12 +02:00
28 changed files with 623 additions and 756 deletions

17
.env
View File

@@ -1,10 +1,11 @@
APP_ENV=dev APP_ENV=
APP_SECRET=je_te_remplis_parce_que_tu_me_mets_des_messages_d_erreur APP_SECRET=
APP_SHARE_DIR=var/share APP_SHARE_DIR=
APP_VERSION=0.0.1 APP_VERSION=
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" DATABASE_URL=
MESSENGER_TRANSPORT_DSN="doctrine://default" MESSENGER_TRANSPORT_DSN=
MAILER_DSN="smtp://localhost:1025" MAILER_DSN=
DEFAULT_URI="http://localhost:8000" DEFAULT_URI=
KAZ_API_BASE_URL=
KAZ_API_USER= KAZ_API_USER=
KAZ_API_PASSWORD= KAZ_API_PASSWORD=

View File

@@ -30,6 +30,6 @@
--color-gris-fonce: #4B5563; --color-gris-fonce: #4B5563;
/* Polices */ /* Polices */
--font-sora: "Sora", system-ui, sans-serif; --font-sora: "Sora", sans-serif;
--font-caveat: "Caveat", cursive; --font-caveat: "Caveat", cursive;
} }

12
composer.lock generated
View File

@@ -3686,16 +3686,16 @@
}, },
{ {
"name": "symfony/http-client", "name": "symfony/http-client",
"version": "v8.0.5", "version": "v8.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-client.git", "url": "https://github.com/symfony/http-client.git",
"reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4" "reference": "356e43d6994ae9d7761fd404d40f78691deabe0e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/f9fdd372473e66469c6d32a4ed12efcffdea38c4", "url": "https://api.github.com/repos/symfony/http-client/zipball/356e43d6994ae9d7761fd404d40f78691deabe0e",
"reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4", "reference": "356e43d6994ae9d7761fd404d40f78691deabe0e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3758,7 +3758,7 @@
"http" "http"
], ],
"support": { "support": {
"source": "https://github.com/symfony/http-client/tree/v8.0.5" "source": "https://github.com/symfony/http-client/tree/v8.0.8"
}, },
"funding": [ "funding": [
{ {
@@ -3778,7 +3778,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-27T16:18:07+00:00" "time": "2026-03-30T15:14:47+00:00"
}, },
{ {
"name": "symfony/http-client-contracts", "name": "symfony/http-client-contracts",

View File

@@ -16,7 +16,7 @@ services:
$apiUser: '%env(KAZ_API_USER)%' $apiUser: '%env(KAZ_API_USER)%'
$apiPassword: '%env(KAZ_API_PASSWORD)%' $apiPassword: '%env(KAZ_API_PASSWORD)%'
# Gestion de l'enregistrement de la photo de profil # Gestion de l'enregistrement de l'image de profil
App\Service\FileUploader: App\Service\FileUploader:
arguments: arguments:
$targetDirectory: '%images_directory%' $targetDirectory: '%images_directory%'

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260313151403 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, lastname VARCHAR(255) NOT NULL, firstname VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260316103235 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER alternate_email DROP NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER alternate_email SET NOT NULL');
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260316104254 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER identifiant_kaz DROP NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER identifiant_kaz SET NOT NULL');
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260316104335 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER quota DROP NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER quota SET NOT NULL');
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260316104505 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER has_agora_access DROP NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER has_agora_access SET NOT NULL');
}
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260316104557 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER has_nextcloud_access DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_mobilizon DROP NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER has_nextcloud_access SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_mobilizon SET NOT NULL');
}
}

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260316114715 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD last_name VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "user" ADD first_name VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "user" DROP lastname');
$this->addSql('ALTER TABLE "user" DROP firstname');
$this->addSql('ALTER TABLE "user" ALTER alternate_email SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER identifiant_kaz SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER quota SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_nextcloud_access SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_mobilizon SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_agora_access SET NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD lastname VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "user" ADD firstname VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "user" DROP last_name');
$this->addSql('ALTER TABLE "user" DROP first_name');
$this->addSql('ALTER TABLE "user" ALTER alternate_email DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER identifiant_kaz DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER quota DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_nextcloud_access DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_mobilizon DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_agora_access DROP NOT NULL');
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260326214353 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD photo VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" DROP photo');
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260326231417 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD telephone VARCHAR(20) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" DROP telephone');
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260328101039 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, telephone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260328101220 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, telephone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260329084928 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, telephone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -7,10 +7,11 @@ use App\Form\UserProfileType;
use App\Service\FileUploader; use App\Service\FileUploader;
use App\Service\KazApiService; use App\Service\KazApiService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -37,16 +38,6 @@ class UserController extends AbstractController
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
*/ */
#[Route('/user/{email}', name: 'app_user_by_mail', methods: ['GET'])]
public function index(string $email, KazApiService $apiKazService): Response
{
$user = $apiKazService->getUserData($email);
return $this->render('user/profil_infos.html.twig', [
'user' => $user,
]);
}
/* TODO : Param l'API avec un Serializer pour la lecture du fichier JSON ? */
#[Route('/mon-profil', name: 'app_user', methods: ['GET', 'POST'])] #[Route('/mon-profil', name: 'app_user', methods: ['GET', 'POST'])]
#[IsGranted('ROLE_USER')] #[IsGranted('ROLE_USER')]
public function showProfile( public function showProfile(
@@ -54,66 +45,63 @@ class UserController extends AbstractController
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
FileUploader $fileUploader, FileUploader $fileUploader,
KazApiService $apiKazService KazApiService $apiKazService
): Response { ): Response
# Récupération de l'utilisateur actuellement connecté {
// Récupération de l'utilisateur actuellement connecté
$user = $this->getUser(); $user = $this->getUser();
// Vérification si l'URL est en mode édition
$isEditMode = $request->query->getBoolean('edit', false);
$kazUser = $apiKazService->getUserData($user->getEmail()); try {
// Récupération des données de l'utilisateur sur l'API grâce à son email
$kazUser = $apiKazService->getUserData($user->getEmail());
// Initialisation de la variable $userData
$user = $user->updateFromKazUser($kazUser);
} catch (Exception $e) {
$this->addFlash('error', 'Impossible de charger vos données.' . $e->getMessage());
}
$user = $user->updateFromKazUser($kazUser); // Création du formulaire lié à l'utilisateur connecté
//TODO: modifier pour que ça communique avec l'API */
# Création du formulaire lié à l'utilisateur connecté
$form = $this->createForm(UserProfileType::class, $user); $form = $this->createForm(UserProfileType::class, $user);
$form->handleRequest($request); $form->handleRequest($request);
# Traitement si l'utilisateur clique sur "Valider" // Affichage du formulaire si les données sont valides
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile|null $imageFile */
/** @var UploadedFile $imageFile */
$imageFile = $form->get('image')->getData(); $imageFile = $form->get('image')->getData();
// --- Gestion de l'image de profil ---
if ($imageFile) { if ($imageFile) {
# Suppression de l'ancienne photo du serveur // Suppression de l'ancienne image via le service
$fileUploader->delete($user->getPhoto()); if ($user->getImage()) {
$fileUploader->delete($user->getImage());
# Dépot de la nouvelle photo }
// Dépôt de la nouvelle image et mise à jour de son nom dans l'entité
$newFilename = $fileUploader->upload($imageFile); $newFilename = $fileUploader->upload($imageFile);
$user->setImage($newFilename);
# Mise à jour de l'utilisateur avec le nouveau nom
$user->setPhoto($newFilename);
} }
// --- Fin gestion de l'image de profil ---
$alternateEmail = $form->get('alternateEmail')->getData(); // Synchronisation des données avec l'API
$regexEmail = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'; $kazUser = $user->convertToKazUser();
if(isset($alternateEmail) && preg_match($regexEmail, $alternateEmail)) {
$user->setAlternateEmail($form->get('alternateEmail')->getData()); try {
} else { $apiKazService->updateUserData($user->getEmail(), $kazUser);
$alternateEmail->addError(new FormError('L\'adresse e-mail n\'est pas valide.')); } catch (Exception $e) {
$this->addFlash('error', 'Impossible de mettre à jour votre profil' . $e->getMessage());
} }
// Sauvegarde en base de données
$telephone = $form->get('telephone')->getData();
$regexTelephone = '/^[0-9\+\s\.\-\(\)]+$/';
if(isset($telephone) && preg_match($regexTelephone, $telephone)) {
$user->setTelephone($telephone);
} else {
$telephone->addError(new FormError('Le numéro de téléphone n\'est pas valide.'));
}
# Sauvegarde en base de données
$entityManager->flush(); $entityManager->flush();
// Message de confirmation et rechargement de la page
# Message de confirmation et rechargement de la page
$this->addFlash('success', 'Votre profil a été mis à jour avec succès !'); $this->addFlash('success', 'Votre profil a été mis à jour avec succès !');
// Redirection de l'utilisateur
return $this->redirectToRoute('app_user'); return $this->redirectToRoute('app_user');
} }
// Affichage de la page
# Affichage de la page
return $this->render('user/index.html.twig', [ return $this->render('user/index.html.twig', [
'form' => $form->createView(), 'form' => $form->createView(),
'userData' => $user, # TODO : Mettre $userData quand connexion avec API OK 'userData' => $user,
'isEditMode' => $isEditMode,
]); ]);
} }
@@ -124,31 +112,30 @@ class UserController extends AbstractController
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
): Response ): Response
{ {
# Création du formulaire // Création du formulaire
$form = $this->createForm(ChangePasswordType::class); $form = $this->createForm(ChangePasswordType::class);
# Liaison du formulaire à la requête HTTP // Liaison du formulaire à la requête HTTP
$form->handleRequest($request); $form->handleRequest($request);
# Vérification du formulaire, s'il est bien soumis et valide // Vérification du formulaire, s'il est bien soumis et valide
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
# Récupération des données du formulaire // Récupération des données du formulaire
$user = $this->getUser(); $user = $this->getUser();
$plainOldPassword = $form->get('oldPassword')->getData(); $plainOldPassword = $form->get('oldPassword')->getData();
$newPassword = $form->get('newPassword')->getData(); $newPassword = $form->get('newPassword')->getData();
# Vérification de l'ancien mot de passe // Vérification de l'ancien mot de passe
if (!$hasher->isPasswordValid($user, $plainOldPassword)) { if (!$hasher->isPasswordValid($user, $plainOldPassword)) {
$form->get('oldPassword')->addError(new FormError('L\'ancien mot de passe est incorrect.')); $form->get('oldPassword')->addError(new FormError('L\'ancien mot de passe est incorrect.'));
} else { } else {
# Si tout est OK : Hachage du mot de passe // Si tout est OK : Hachage du mot de passe
$hashedPassword = $hasher->hashPassword($user, $newPassword); $hashedPassword = $hasher->hashPassword($user, $newPassword);
$user->setPassword($hashedPassword); $user->setPassword($hashedPassword);
# Sauvegarde en BDD // Sauvegarde en BDD
$entityManager->flush(); $entityManager->flush();
// Message de succès pour l'utilisateur
# Message de succès pour l'utilisateur
$this->addFlash('success', 'Votre mot de passe a bien été mis à jour !'); $this->addFlash('success', 'Votre mot de passe a bien été mis à jour !');
return $this->redirectToRoute('app_user_edit_password'); return $this->redirectToRoute('app_user_edit_password');

View File

@@ -10,35 +10,36 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class AppFixtures extends Fixture class AppFixtures extends Fixture
{ {
# Initialisation de l'outil de hachage de Symfony // Initialisation de l'outil de hachage de Symfony
private UserPasswordHasherInterface $hasher; private UserPasswordHasherInterface $hasher;
# Injection de dépendance pour récupérer le service de sécurité // Injection de dépendance pour récupérer le service de sécurité
public function __construct(UserPasswordHasherInterface $hasher) public function __construct(UserPasswordHasherInterface $hasher)
{ {
$this->hasher = $hasher; $this->hasher = $hasher;
} }
# Méthode principale qui génère les données en base // Méthode principale qui génère des données de test en BDD locale
public function load(ObjectManager $manager): void public function load(ObjectManager $manager): void
{ {
# Initialisation de Faker en français // --- Création de 10 utilisateurs avec Faker --- //
// Initialisation de Faker en français
$faker = Factory::create('fr_FR'); $faker = Factory::create('fr_FR');
# Boucle pour créer 10 utilisateurs // Boucle pour créer 10 utilisateurs
for ($i = 0; $i < 10; $i++) { for ($i = 0; $i < 10; $i++) {
# Instanciation d'un nouvel utilisateur (Adhérent) // Instanciation d'un nouvel utilisateur (Adhérent)
$user = new User(); $user = new User();
# Attribution d'un email aléatoire et unique // Attribution d'un email aléatoire et unique
$user->setEmail($faker->unique()->safeEmail()); $user->setEmail($faker->unique()->safeEmail());
# Définition des droits d'accès de l'utilisateur // Définition des droits d'accès de l'utilisateur
$user->setRoles(['ROLE_USER', 'ROLE_ORGANISATION']); $user->setRoles(['ROLE_USER', 'ROLE_ORGANISATION']);
# Hachage sécurisé du mot de passe "password" // Hachage sécurisé du mot de passe "password"
$user->setPassword($this->hasher->hashPassword($user, 'password')); $user->setPassword($this->hasher->hashPassword($user, 'password'));
# Définition d'un NOM et Prénom // Définition d'un NOM et Prénom
$user->setFirstname($faker->firstName()); $user->setFirstname($faker->firstName());
$user->setLastname($faker->lastName()); $user->setLastname($faker->lastName());
# autres fixtures à modifier plus tard // autres fixtures à modifier plus tard
$user->setNextcloudQuota($faker->numberBetween(1, 20) . 'G'); $user->setNextcloudQuota($faker->numberBetween(1, 20) . 'G');
$user->setQuota($faker->numberBetween(1, 10) . 'G'); $user->setQuota($faker->numberBetween(1, 10) . 'G');
$user->setEmailQuota('1G'); $user->setEmailQuota('1G');
@@ -47,7 +48,7 @@ class AppFixtures extends Fixture
$user->setHasMobilizon($faker->boolean(50)); $user->setHasMobilizon($faker->boolean(50));
$user->setHasNextcloudAccess($faker->boolean(90)); $user->setHasNextcloudAccess($faker->boolean(90));
$user->setIdentifiantKaz($faker->uuid()); $user->setIdentifiantKaz($faker->uuid());
# Préparation de l'enregistrement de l'objet en base de données // Préparation de l'enregistrement de l'objet en base de données
$manager->persist($user); $manager->persist($user);
} }
@@ -56,10 +57,9 @@ class AppFixtures extends Fixture
$admin->setEmail('admin@kaz.bzh'); $admin->setEmail('admin@kaz.bzh');
$admin->setRoles(['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ORGANISATION']); $admin->setRoles(['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ORGANISATION']);
$admin->setPassword($this->hasher->hashPassword($admin, 'password')); $admin->setPassword($this->hasher->hashPassword($admin, 'password'));
// Remplissage des champs obligatoires restants pour éviter les erreurs SQL
$admin->setFirstName('Admin'); $admin->setFirstName('Admin');
$admin->setLastName('KAZ'); $admin->setLastName('KAZ');
// Remplissage des champs obligatoires restants pour éviter les erreurs SQL
$admin->setAlternateEmail('secours@kaz.bzh'); $admin->setAlternateEmail('secours@kaz.bzh');
$admin->setIdentifiantKaz('ADMIN-KAZ-001'); $admin->setIdentifiantKaz('ADMIN-KAZ-001');
$admin->setQuota('5G'); $admin->setQuota('5G');
@@ -71,26 +71,24 @@ class AppFixtures extends Fixture
$manager->persist($admin); $manager->persist($admin);
// Création d'un compte de test fixe // Création d'un compte de test fixe présent dans le LDAP pour ma présentation
$melvin = new User(); $toto = new User();
$melvin->setEmail('melvin.leveque@kazkouil.fr'); $toto->setEmail('toto@kazkouil.fr');
$melvin->setRoles(['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ORGANISATION']); $toto->setRoles(['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ORGANISATION']);
$melvin->setPassword($this->hasher->hashPassword($melvin, 'password')); $toto->setPassword($this->hasher->hashPassword($toto, 'password'));
$melvin->setFirstName(''); $toto->setFirstName('');
$melvin->setLastName(''); $toto->setLastName('');
$toto->setAlternateEmail('');
$toto->setIdentifiantKaz('');
$toto->setQuota('5G');
$toto->setEmailQuota('1G');
$toto->setNextcloudQuota('10G');
$toto->setHasNextcloudAccess(true);
$toto->setHasMobilizon(true);
$toto->setHasAgoraAccess(true);
$manager->persist($toto);
$melvin->setAlternateEmail(''); // Exécution réelle des requêtes SQL (envoi vers la base), une fois la bouche finie
$melvin->setIdentifiantKaz('MELVIN-KAZ-001');
$melvin->setQuota('5G');
$melvin->setEmailQuota('1G');
$melvin->setNextcloudQuota('10G');
$melvin->setHasNextcloudAccess(true);
$melvin->setHasMobilizon(true);
$melvin->setHasAgoraAccess(true);
$manager->persist($melvin);
# Exécution réelle des requêtes SQL (envoi vers la base), une fois la bouche finie
$manager->flush(); $manager->flush();
} }
} }

View File

@@ -3,7 +3,6 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
@@ -69,13 +68,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(length: 255, name: 'first_name')] #[ORM\Column(length: 255, name: 'first_name')]
private ?string $firstName = null; private ?string $firstName = null;
// TODO: Modifier "photo" par "image" #[ORM\Column(length: 255, nullable: true, name: 'image')]
#[ORM\Column(length: 255, nullable: true, name: 'photo')] private ?string $image = null;
private ?string $photo = null;
#[ORM\Column(length: 20, nullable: true, name: 'telephone')] #[ORM\Column(length: 20, nullable: true, name: 'telephone')]
private ?string $telephone = null; private ?string $telephone = null;
private ?string $numeroMembre = null;
private ?bool $mailEnabled = null;
private ?string $mailAlias = null;
public function __construct() { public function __construct() {
$this->emailQuota = self::EMAIL_QUOTA_DEFAULT; $this->emailQuota = self::EMAIL_QUOTA_DEFAULT;
} }
@@ -290,14 +294,14 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function getPhoto(): ?string public function getImage(): ?string
{ {
return $this->photo; return $this->image;
} }
public function setPhoto(?string $photo): static public function setImage(?string $image): static
{ {
$this->photo = $photo; $this->image = $image;
return $this; return $this;
} }
@@ -314,18 +318,80 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function getNumeroMembre(): ?string
{
return $this->numeroMembre;
}
public function setNumeroMembre(?string $numeroMembre): static
{
$this->numeroMembre = $numeroMembre;
return $this;
}
public function isMailEnabled(): ?bool
{
return $this->mailEnabled;
}
public function setMailEnabled(?bool $mailEnabled): static
{
$this->mailEnabled = $mailEnabled;
return $this;
}
public function getMailAlias(): ?string
{
return $this->mailAlias;
}
public function setMailAlias(?string $mailAlias): static
{
$this->mailAlias = $mailAlias;
return $this;
}
// Fonction qui permet d'afficher les données de l'API sur la page de profil
public function updateFromKazUser($kazUser) : User public function updateFromKazUser($kazUser) : User
{ {
// Récupération et conversion des données de l'API pour les afficher
$this->setEmail($kazUser['mail']); $this->setEmail($kazUser['mail']);
// Création du firstname et lastname // Création du firstname et lastname (une seule donnée sur l'API)
$name = explode(' ', $kazUser['sn']); $name = explode(' ', $kazUser['sn']);
$this->setFirstName($name[0]); $this->setFirstName($name[0]);
// Récupération des valeurs du tableau moins la première // Récupération des valeurs du tableau moins la première
$aLastname = array_slice($name, 1); $aLastname = array_slice($name, 1);
$this->setLastName(implode(' ', $aLastname)); $this->setLastName(implode(' ', $aLastname));
// Récupération du mail de secours
//TODO: Ajouter les champs manquants de l'objet User dans l'api kaz. $this->setAlternateEmail($kazUser['mailDeSecours']);
$this->setEmailQuota($kazUser['mailQuota']);
$this->setHasAgoraAccess($kazUser['agoraEnabled']);
$this->setHasMobilizon($kazUser['mobilizonEnabled']);
$this->setHasNextcloudAccess($kazUser['nextcloudEnabled']);
$this->setNextcloudQuota($kazUser['nextcloudQuota']);
$this->setQuota($kazUser['quota']);
$this->setIdentifiantKaz($kazUser['identifiantKaz']);
return $this; return $this;
} }
// Fonction qui permet de convertir les données de l'API vers $kazUser
public function convertToKazUser() : array
{
$data = [
'numeroMembre' => $this->getNumeroMembre(),
'mailDeSecours' => $this->getAlternateEmail(),
'mailEnabled' => $this->isMailEnabled(),
'nextcloudEnabled' => $this->hasNextcloudAccess(),
'mobilizonEnabled' => $this->hasMobilizon(),
'agoraEnabled' => $this->hasAgoraAccess(),
'identifiantKaz' => $this->getIdentifiantKaz(),
'mailAlias' => $this->getMailAlias(),
'quota' => $this->getQuota(),
];
return array_filter($data, fn($value) => $value !== null);
}
} }

View File

@@ -3,11 +3,12 @@
namespace App\Form; namespace App\Form;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\Image;
@@ -27,22 +28,53 @@ class UserProfileType extends AbstractType
'label' => 'Nom', 'label' => 'Nom',
'disabled' => true, 'disabled' => true,
]) ])
->add('identifiantKaz', TextType::class, [
'label' => 'Identifiant KAZ : ',
'disabled' => true,
])
->add('email', EmailType::class, [ ->add('email', EmailType::class, [
'label' => 'E-mail', 'label' => 'E-mail',
'disabled' => true, 'disabled' => true,
]) ])
->add('alternateEmail', EmailType::class, ['label' => 'E-mail de secours']) ->add('alternateEmail', EmailType::class, [
'label' => 'E-mail de secours',
'constraints' => [
new Regex(
pattern: '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/',
message: 'L\'adresse e-mail n\'est pas valide.'
)
]
])
->add('emailQuota', TextType::class, [
'label' => 'Espace de stockage de votre boîte mail : ',
'disabled' => true,
])
->add('hasNextcloudAccess', CheckboxType::class, [
'label' => 'Accès au Nextcloud : ',
'disabled' => true,
])
->add('nextcloudQuota', TextType::class, [
'label' => 'Espace de stockage de votre Nextcloud : ',
'disabled' => true,
])
->add('hasMobilizon', CheckboxType::class, [
'label' => 'Accès à Mobilizon : ',
'disabled' => true,
])
->add('hasAgoraAccess', CheckboxType::class, [
'label' => 'Accès à l\'Agora : ',
'disabled' => true,
])
->add('telephone', TelType::class, [ ->add('telephone', TelType::class, [
'label'=>'Téléphone', 'label'=>'Téléphone',
'required' => false, 'required' => false,
'attr' => [ 'attr' => [
'placeholder'=>'06 00 00 00 00', 'placeholder'=>'06 00 00 00 00',
'class'=> 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton transition-shadow'
], ],
'constraints' => [ 'constraints' => [
new Regex( new Regex(
pattern: '/^[0-9\+\s\.\-\(\)]+$/', pattern: '/^[0-9\+\s\.\-\(\)]+$/',
message: 'Le numéro de téléphone contient des caractères non valides' message: 'Le numéro de téléphone n\'est pas valide.'
), ),
new Length( new Length(
max: 20, max: 20,
@@ -51,14 +83,14 @@ class UserProfileType extends AbstractType
], ],
]) ])
->add('image', FileType::class, [ ->add('image', FileType::class, [
'label' => 'Ma photo de profil', 'label' => 'Mon image de profil',
'mapped' => false, 'mapped' => false,
'required' => false, 'required' => false,
'constraints' => [ 'constraints' => [
new Image( new Image(
maxSize: '2M', maxSize: '8M',
extensions: ['jpg', 'jpeg', 'png'], extensions: ['jpg', 'jpeg', 'png', 'gif'],
extensionsMessage: 'Veuillez déposer une image JPG, JPEG ou PNG valide',) extensionsMessage: 'Veuillez déposer une image JPG, JPEG, GIF ou PNG valide')
], ],
]) ])
; ;

View File

@@ -2,45 +2,76 @@
namespace App\Service; namespace App\Service;
use RuntimeException;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Component\String\Slugger\SluggerInterface;
/**
* Service de gestion des envois et suppressions de fichiers.
*/
class FileUploader class FileUploader
{ {
// On utilise la promotion de constructeur (PHP 8) : ultra moderne et concis /**
* @param string $targetDirectory Le chemin absolu vers le dossier de dépôt.
* @param SluggerInterface $slugger Le service de nettoyage des chaînes de caractères.
*/
public function __construct( public function __construct(
private string $targetDirectory, #[Autowire('%kernel.project_dir%/public/uploads/img')]
private SluggerInterface $slugger, private readonly string $targetDirectory,
) { private readonly SluggerInterface $slugger,
)
{
} }
/**
* Traite, sécurise et déplace un fichier déposé.
*
* @param UploadedFile $file Le fichier physique à déposer.
* @return string Le nom final sécurisé et unique du fichier.
* @throws RuntimeException En cas d'échec de l'écriture sur le disque.
*/
public function upload(UploadedFile $file): string public function upload(UploadedFile $file): string
{ {
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $this->slugger->slug($originalFilename); $safeFilename = $this->slugger->slug($originalFilename);
$fileName = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension();
// Utilisation de uniqid('', true) pour garantir une unicité absolue en production
$fileName = sprintf('%s-%s.%s', $safeFilename, uniqid('', true), $file->guessExtension());
try { try {
$file->move($this->getTargetDirectory(), $fileName); $file->move($this->getTargetDirectory(), $fileName);
} catch (FileException $e) { } catch (FileException $e) {
// Ici tu peux logguer l'erreur si besoin throw new RuntimeException('Erreur lors du transfert de l\'image : ' . $e->getMessage(), 0, $e);
throw new \Exception('Erreur lors du transfert de l\'image : ' . $e->getMessage());
} }
return $fileName; return $fileName;
} }
/**
* Supprime physiquement un fichier du serveur.
*
* @param string|null $fileName Le nom du fichier à supprimer.
*/
public function delete(?string $fileName): void public function delete(?string $fileName): void
{ {
if ($fileName) { if (null === $fileName) {
$filePath = $this->getTargetDirectory() . 'FileUploader.php/' . $fileName; return;
if (file_exists($filePath)) { }
unlink($filePath);
} $filePath = rtrim($this->getTargetDirectory(), '/') . '/' . $fileName;
if (file_exists($filePath)) {
unlink($filePath);
} }
} }
/**
* Retourne le chemin du répertoire de dépôt.
*
* @return string
*/
public function getTargetDirectory(): string public function getTargetDirectory(): string
{ {
return $this->targetDirectory; return $this->targetDirectory;

View File

@@ -4,6 +4,8 @@ namespace App\Service;
use Exception; use Exception;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
@@ -50,7 +52,7 @@ class KazApiService
} }
$data = $response->toArray(); $data = $response->toArray();
$this->token = $data['access_token']; // Ajustez la clé selon le format de votre API $this->token = $data['access_token'];
return $this->token; return $this->token;
} }
@@ -80,4 +82,26 @@ class KazApiService
return $response->toArray(); return $response->toArray();
} }
/**
* Envoie les nouvelles données saisies par l'utilisateur vers l'API
*
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @throws Exception
*/
public function updateUserData(string $email, array $kazUser): void
{
$options['headers']['Authorization'] = 'Bearer ' . $this->getToken();
$options['headers']['Content-Type'] = 'application/json';
$options['json'] = $kazUser;
$response = $this->kazApiClient->request('PATCH', "/ldap/user/update/$email", $options);
if ($response->getStatusCode() !== 200) {
throw new Exception('Erreur lors de l\'appel API : ' . $response->getStatusCode());
}
}
} }

View File

@@ -12,11 +12,11 @@
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %} {{ importmap('app') }}
{% endblock %} {% endblock %}
</head> </head>
<body> <body class="min-h-screen flex flex-col font-sora antialiased text-text">
{% block navbar %} {% block navbar %}
{{ include('_navbar.html.twig') }} {{ include('_navbar.html.twig') }}
@@ -28,67 +28,58 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
{% for label, messages in app.flashes %} {% for label, messages in app.flashes %}
{% for message in messages %} {% for message in messages %}
<div class="flex items-center p-4 mb-4 rounded-lg border shadow-sm {% set colorClasses = {
{{ label == 'success' ? 'bg-green-50 border-green-200 text-green-800' : 'success': 'bg-green-50 border-green-200 text-green-800',
label == 'error' ? 'bg-red-50 border-red-200 text-red-800' : 'error': 'bg-red-50 border-red-200 text-red-800',
label == 'warning' ? 'bg-yellow-50 border-yellow-200 text-yellow-800' : 'warning': 'bg-yellow-50 border-yellow-200 text-yellow-800',
'bg-blue-50 border-blue-200 text-blue-800' }}" 'info': 'bg-blue-50 border-blue-200 text-blue-800'
role="alert"> } %}
{# Icône dynamique #} <div
<svg class="flex-shrink-0 w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20"> class="flex items-center p-4 mb-4 rounded-lg border shadow-sm {{ colorClasses[label] ?? colorClasses['info'] }}"
{% if label == 'success' %} role="alert">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path> <div class="text-sm font-semibold flex-grow">
{% else %}
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
{% endif %}
</svg>
<div class="text-sm font-bold">
{{ message }} {{ message }}
</div> </div>
{# Bouton Fermer #}
<button type="button" <button type="button"
onclick="this.parentElement.remove()" onclick="this.parentElement.remove()"
class="text-lg font-bold hover:opacity-70 transition-opacity ml-4"> class="ml-4 hover:opacity-50 transition-opacity"
&times; aria-label="Fermer">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button> </button>
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</div> </div>
{# Contenu principal de chaque page #}
{% block body %}{% endblock %} {% block body %}{% endblock %}
</main> </main>
{# Gestion du pied-de-page du site #}
<footer class="bg-white border-t border-gris-clair py-6 sm:py-8 mt-auto w-full font-sora">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-4">
<div class="text-sm text-gris-fonce flex items-center gap-2 justify-center md:justify-start">
{# Logo de l'association #}
<img src="{{ asset('img/logo.svg') }}"
alt="Logo de l'association"
class="h-6 w-auto object-contain opacity-80 hover:opacity-100 transition-opacity">
{# Le texte et les liens #} {# Gestion du pied-de-page du site #}
<span> <footer class="bg-white border-t border-gris-clair py-8 w-full">
&copy; {{ 'now'|date('Y') }} | Kaz, le numérique sobre, libre, éthique et local. <div
</span> class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-6">
<div class="flex items-center gap-3 text-sm text-gris-fonce text-center md:text-left">
{# Logo de l'association #}
<img src="{{ asset('img/logo.svg') }}" alt="Logo" class="h-6 w-auto opacity-80">
{# Le texte et les liens #}
<p>&copy; {{ 'now'|date('Y') }} — Kaz. Le numérique sobre, libre, éthique et local.</p>
</div> </div>
<ul class="flex flex-wrap justify-center gap-4 sm:gap-6 text-sm text-gris-fonce"> <nav>
<li> <ul class="flex flex-wrap justify-center gap-6 text-sm text-gris-fonce">
<a href="https://kaz.bzh/mentions-legales/" class="hover:text-bouton transition-colors">Mentions légales et statuts</a> <li><a href="https://kaz.bzh/mentions-legales/" target="_blank"
</li> class="hover:text-bouton transition-colors">Mentions légales</a></li>
<li> <li><a href="https://status.kaz.bzh/status/kaz" target="_blank"
<a href="https://status.kaz.bzh/status/kaz" class="hover:text-bouton transition-colors">Santé des services Kaz</a> class="hover:text-bouton transition-colors">État des services</a></li>
</li> <li><a href="https://kaz.bzh/contact/" target="_blank" class="hover:text-bouton transition-colors">Contact</a>
<li>
<a href="https://kaz.bzh/contact/" class="hover:text-bouton transition-colors">Contact</a>
</li> </li>
</ul> </ul>
</nav>
</div> </div>
</footer> </footer>
</body> </body>

View File

@@ -16,23 +16,23 @@
Bienvenue sur ton espace kaznaute <span class="text-bouton">{{ app.user ? app.user.userIdentifier : 'visiteur' }}</span> ! Bienvenue sur ton espace kaznaute <span class="text-bouton">{{ app.user ? app.user.userIdentifier : 'visiteur' }}</span> !
</h2> </h2>
{# Zone réservée pour les futures données de l'API {# Zone réservée pour les futures données de Pahéko
TODO : Gérer les données de l'API #} TODO : Gérer les données avec Pahéko, mise en service en cours par un des développeurs de l'association. Cela sera vu à posteriori du stage #}
<div class="bg-bouton/10 border border-bouton/30 rounded-lg p-5"> <div class="bg-bouton/10 border border-bouton/30 rounded-lg p-5">
<h3 class="font-semibold text-title mb-3 flex items-center gap-2"> <h3 class="font-semibold text-title mb-3 flex items-center gap-2">
Votre abonnement actuellement : Ton abonnement actuellement :
</h3> </h3>
<ul class="space-y-2 text-sm text-text"> <ul class="space-y-2 text-sm text-text">
<li class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2"> <li class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span class="font-semibold text-gris-fonce">Formule souscrite :</span> <span class="font-semibold text-gris-fonce">Formule souscrite :</span>
{# TODO API : Remplacer par la vraie variable #} {# TODO API : Remplacer par la vraie variable quand connexion Pahéko OK #}
<span class="italic opacity-70">Ajouter la vraie valeur</span> <span class="italic opacity-70">Ici s'affichera la donnée récupérée grâce à l'API</span>
</li> </li>
<li class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2"> <li class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span class="font-semibold text-gris-fonce">Date de validité :</span> <span class="font-semibold text-gris-fonce">Date de validité :</span>
{# TODO API : Remplacer par la vraie variable #} {# TODO API : Remplacer par la vraie variable quand connexion Pahéko OK #}
<span class="italic opacity-70">Ajouter la vraie valeur</span> <span class="italic opacity-70">Ici s'affichera la donnée récupérée grâce à l'API</span>
</li> </li>
</ul> </ul>
</div> </div>
@@ -91,7 +91,7 @@
</div> </div>
</a> </a>
{% else %} {% else %}
<a href="https://kaz.bzh/contact/" class="group flex items-center p-5 bg-white border border-gris-clair rounded-xl shadow-sm hover:shadow-md hover:border-bouton transition-all duration-200"> <a href="https://kaz.bzh/contact/" target="_blank" class="group flex items-center p-5 bg-white border border-gris-clair rounded-xl shadow-sm hover:shadow-md hover:border-bouton transition-all duration-200">
<div class="flex-shrink-0 bg-purple-50 text-purple-600 rounded-lg p-3 group-hover:bg-purple-600 group-hover:text-white transition-colors"> <div class="flex-shrink-0 bg-purple-50 text-purple-600 rounded-lg p-3 group-hover:bg-purple-600 group-hover:text-white transition-colors">
<span class="text-2xl block">✉️</span> <span class="text-2xl block">✉️</span>
</div> </div>

View File

@@ -6,19 +6,22 @@
<div class="min-h-screen bg-bg-primaire flex items-center justify-center p-4 font-sora"> <div class="min-h-screen bg-bg-primaire flex items-center justify-center p-4 font-sora">
<div class="max-w-md w-full bg-white rounded-2xl shadow-xl p-8 border-t-4 border-bouton"> <div class="max-w-md w-full bg-white rounded-2xl shadow-xl p-8 border-t-4 border-bouton">
<h1 class="font-caveat text-4xl text-text mb-6 text-center"> <h1 class="font-caveat text-4xl text-text mb-12 text-center">
Sécurité du compte Modifier le mot de passe de mon espace kaznaute
</h1> </h1>
{{ form_start(form) }} {{ form_start(form) }}
<div class="space-y-4"> <div class="space-y-6">
{# Champ Ancien Mot de Passe #} {# Champ Ancien Mot de Passe #}
<div class="space-y-1"> <div class="space-y-1">
{{ form_label(form.oldPassword, 'Mot de passe actuel', { {{ form_label(form.oldPassword, 'Mon mot de passe actuel', {
'label_attr': {'class': 'block text-sm font-semibold text-text'} 'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }} }) }}
{{ form_widget(form.oldPassword, { {{ form_widget(form.oldPassword, {
'attr': {'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-shadow'} 'attr': {
'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-shadow',
'placeholder':'Saisissez votre ancien mot de passe'
}
}) }} }) }}
{# Affichage message pour les erreurs de saisie de l'ancien mot de passe #} {# Affichage message pour les erreurs de saisie de l'ancien mot de passe #}
@@ -28,44 +31,47 @@
</div> </div>
{# Champs Nouveau Mot de Passe #} {# Champs Nouveau Mot de Passe #}
<div class="space-y-4 pt-2"> <div class="space-y-6">
{# Affichage de l'erreur si les deux champs ne correspondent pas #} {# Affichage d'un message d'erreur si les deux champs ne correspondent pas #}
<div class="text-red-500 text-xs italic"> <div class="text-red-500 text-xs italic">
{{ form_errors(form.newPassword) }} {{ form_errors(form.newPassword) }}
</div> </div>
<div class="space-y-1">
{{ form_label(form.newPassword.first, 'Nouveau mot de passe', { <div class="flex flex-col gap-1.5">
{{ form_label(form.newPassword.first, 'Mon nouveau mot de passe', {
'label_attr': {'class': 'block text-sm font-semibold text-text'} 'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }} }) }}
{{ form_widget(form.newPassword.first, { {{ form_widget(form.newPassword.first, {
'attr': {'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton transition-shadow'} 'attr': {
'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-all',
'placeholder':'Saisissez un nouveau mot de passe'
}
}) }} }) }}
<div class="text-red-500 text-xs italic">
{# Affichage de l'erreur de longueur (min 8 caractères) #}
<div class="text-red-500 text-xs mt-1 italic">
{{ form_errors(form.newPassword.first) }} {{ form_errors(form.newPassword.first) }}
</div> </div>
</div> </div>
<div class="space-y-1"> <div class="flex flex-col gap-1.5">
{{ form_label(form.newPassword.second, 'Confirmer le nouveau mot de passe', { {{ form_label(form.newPassword.second, 'Confirmer mon nouveau mot de passe', {
'label_attr': {'class': 'block text-sm font-semibold text-text'} 'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }} }) }}
{{ form_widget(form.newPassword.second, { {{ form_widget(form.newPassword.second, {
'attr': {'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton transition-shadow'} 'attr': {
'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-all',
'placeholder': 'Confirmez votre saisie'
}
}) }} }) }}
</div> </div>
<div class="text-red-500 text-xs mt-1">
{{ form_errors(form.newPassword) }}
</div>
</div> </div>
{# Bouton de validation #} {# Bouton de validation #}
<button type="submit" <div class="flex justify-center pt-12">
class="w-full bg-bouton hover:bg-bouton-hover text-text font-bold py-3 rounded-lg shadow-md transition-colors mt-6"> <button type="submit"
Mettre à jour mon mot de passe class="w-full sm:w-auto px-8 py-3 bg-bouton hover:bg-bouton-hover text-text text-sm font-bold rounded-lg shadow transition-all active:scale-95">
</button> Mettre à jour mon mot de passe
</button>
</div>
</div> </div>
{{ form_end(form) }} {{ form_end(form) }}

View File

@@ -1,135 +1,283 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Accueil | {{ parent() }}{% endblock %} {# @var userData \App\Entity\User #}
{# @var form \Symfony\Component\Form\FormView #}
{# @var isEditMode bool #}
{% block title %}Ma page de profil | {{ parent() }}{% endblock %}
{% block body %} {% block body %}
<div class="min-h-screen bg-bg-primaire py-8 w-full font-sora"> <div class="min-h-screen bg-bg-primaire py-8 w-full font-sora">
{# Affichage du formulaire (seulement en mode édition) #}
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 grid md:grid-cols-3 gap-8">
{% if isEditMode %}
{{ form_start(form, {'attr': {'class': 'contents'}}) }}
{% endif %}
{{ form_start(form, {'attr': {'class': 'max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 grid md:grid-cols-3 gap-8'}}) }} {# Colonne Photo #}
<div class="flex flex-col text-text items-center">
{# TODO: voir si c'est pertinent avec l'API et s'il y a l'utilité d'une photo de profil #} <div class="mt-16 flex-shrink-0">
{# Gestion de la colone avec le choix de la photo de profil #} {% if userData.image %}
<div class="flex flex-col text-text items-center"> {# Design avec photo #}
<img src="{{ asset('uploads/images/' ~ userData.image) }}"
{# Affichage de la photo de profil #}
<div class="w-full md:w-64 flex-shrink-0 mt-20">
<div class="border-2 border-black p-1 bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
{% if userData.photo %}
<img src="{{ asset('uploads/images/' ~ userData.photo) }}"
alt="Photo de profil" alt="Photo de profil"
class="w-full aspect-[4/3] object-cover"> class="w-48 h-48 md:w-56 md:h-56 p-1 rounded-full ring-2 ring-gris-clair object-cover">
{% else %} {% else %}
<div class="w-full aspect-[4/3] bg-gray-50 flex items-center justify-center text-6xl"> {# Design sans photo #}
👤 <div
class="relative w-48 h-48 md:w-56 md:h-56 overflow-hidden bg-neutral-200 rounded-full flex items-end justify-center shadow-sm">
<svg class="w-5/6 h-5/6 text-neutral-500" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"></path>
</svg>
</div>
{% endif %}
</div>
<p class="text-2xl text-title font-caveat mt-6"> Ma photo</p>
{# Gestion du dépôt d'un fichier image (Uniquement en mode édition) #}
{% if isEditMode %}
<div class="w-full mt-4">
{{ form_label(form.image, 'Choisir un fichier', {
'label_attr': {'class': 'block mb-2.5 text-sm font-medium text-gris-fonce'}
}) }}
{{ form_widget(form.image, {
'attr': {
'class': 'cursor-pointer bg-white border border-gris-clair text-text text-sm rounded-lg focus:outline-none focus:ring-1 focus:ring-bouton focus:border-bouton block w-full shadow-sm placeholder-gris-moyen file:mr-4 file:py-2.5 file:px-4 file:border-0 file:border-r file:border-gris-clair file:bg-gris-clair file:text-gris-fonce hover:file:bg-gris-moyen transition-colors',
'aria-describedby': 'file_input_help'
}
}) }}
<p class="mt-1 text-sm text-gris-moyen" id="file_input_help">
JPG, JPEG, GIF ou PNG (Taille max : 8Mo).
</p>
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.image) }}
</div>
</div>
{% endif %}
</div>
{# Colonne Infos Persos #}
<div class="md:col-span-2">
{# Gestion des boutons d'action (Modifier / Annuler) et des titres #}
<div class="relative flex items-center justify-center mb-6">
<h1 class="text-4xl font-caveat text-text m-0">Mon profil</h1>
<div class="absolute right-0">
{% if isEditMode %}
<a href="{{ path('app_user') }}"
class="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-gris-clair hover:bg-gray-50 text-text text-sm font-bold rounded-md shadow-sm transition-colors">
❌ Annuler
</a>
{% else %}
<a href="{{ path('app_user', {'edit': 1}) }}"
class="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-gris-clair hover:bg-gray-50 text-text text-sm font-bold rounded-md shadow-sm transition-colors">
✏️ Modifier
</a>
{% endif %}
</div>
</div>
<h2 class="text-2xl font-caveat text-text mb-6 text-center">Mes informations personnelles</h2>
{# --- Gestion de l'affichage des informations personnelles --- #}
<div class="flex flex-col gap-6">
{# Identifiant #}
<div class="space-y-1">
<label class="block text-base font-semibold text-text">
Identifiant Kaz :
</label>
{% if isEditMode %}
{{ form_widget(form.identifiantKaz, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.identifiantKaz ?? 'Non défini' }}
</div>
{% endif %}
</div>
{# --- NOM et Prénom --- #}
<div class="grid grid-cols-2 gap-4">
{# NOM #}
<div class="space-y-1">
<label class="block text-base font-semibold text-text">
NOM :
</label>
{% if isEditMode %}
{{ form_widget(form.lastName, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.lastName) }}
</div>
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.lastName }}
</div>
{% endif %}
</div>
{# Prénom #}
<div class="space-y-1">
<label class="block text-base font-semibold text-text">
Prénom :
</label>
{% if isEditMode %}
{{ form_widget(form.firstName, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.firstName) }}
</div>
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.firstName }}
</div>
{% endif %}
</div>
</div>
{# Téléphone #}
<div class="space-y-1">
<label class="block text-base font-semibold text-text">
Numéro de téléphone :
</label>
{% if isEditMode %}
{{ form_widget(form.telephone, {'attr': {'class': 'w-full px-4 py-3 text-base bg-white border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-shadow'}}) }}
<div class="text-red-500 text-xs mt-1 italic">
{{ form_errors(form.telephone) }}
</div>
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.telephone ?? 'Non renseigné' }}
</div>
{% endif %}
</div>
{# E-mail #}
<div class="space-y-1">
<label class="block text-base font-semibold text-text">
E-mail :
</label>
{% if isEditMode %}
{{ form_widget(form.email, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.email) }}
</div>
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.email }}
</div>
{% endif %}
</div>
{# E-mail de secours #}
<div class="space-y-1">
<label class="block text-base font-semibold text-text">
E-mail de secours :
</label>
{% if isEditMode %}
{{ form_widget(form.alternateEmail, {'attr': {'class': 'w-full px-4 py-3 text-base bg-white border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-shadow'}}) }}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.alternateEmail) }}
</div>
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.alternateEmail ?? 'Non renseigné' }}
</div>
{% endif %}
</div>
{# Quota Email #}
<div class="space-y-1">
<label class="block text-base font-semibold text-text">
Espace disponible dans votre boîte mail :
</label>
{% if isEditMode %}
{{ form_widget(form.emailQuota, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.emailQuota ?? 'Non défini' }}
</div>
{% endif %}
</div>
{# Quota Nextcloud #}
<div class="space-y-1 sm:col-span-2">
<label class="block text-base font-semibold text-text">
Quota Nextcloud :
</label>
{% if isEditMode %}
{{ form_widget(form.nextcloudQuota, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.nextcloudQuota ?? 'Non défini' }}
</div>
{% endif %}
</div>
{# --- Gestion de l'affichage des checkbox des différents accès --- #}
<div class="sm:col-span-2 grid grid-cols-1 sm:grid-cols-3 gap-4 pt-4 border-t border-gris-clair">
{# Accès Nextcloud #}
<div class="flex items-center gap-3">
{% if isEditMode %}
{{ form_widget(form.hasNextcloudAccess, {'attr': {'class': 'w-5 h-5 text-bouton border-gris-clair rounded focus:ring-bouton cursor-not-allowed opacity-60'}}) }}
{{ form_label(form.hasNextcloudAccess, null, {'label_attr': {'class': 'text-base font-medium text-text'}}) }}
{% else %}
<span class="text-l">{% if userData.hasNextcloudAccess %}{% else %}{% endif %}</span>
<span class="text-base font-medium text-text">Accès Nextcloud</span>
{% endif %}
</div>
{# Accès Mobilizon #}
<div class="flex items-center gap-3">
{% if isEditMode %}
{{ form_widget(form.hasMobilizon, {'attr': {'class': 'w-5 h-5 text-bouton border-gris-clair rounded focus:ring-bouton cursor-not-allowed opacity-60'}}) }}
{{ form_label(form.hasMobilizon, null, {'label_attr': {'class': 'text-base font-medium text-text'}}) }}
{% else %}
<span class="text-l">{% if userData.hasMobilizon %}{% else %}{% endif %}</span>
<span class="text-base font-medium text-text">Accès Mobilizon</span>
{% endif %}
</div>
{# Accès Agora #}
<div class="flex items-center gap-3">
{% if isEditMode %}
{{ form_widget(form.hasAgoraAccess, {'attr': {'class': 'w-5 h-5 text-bouton border-gris-clair rounded focus:ring-bouton cursor-not-allowed opacity-60'}}) }}
{{ form_label(form.hasAgoraAccess, null, {'label_attr': {'class': 'text-base font-medium text-text'}}) }}
{% else %}
<span class="text-l">{% if userData.hasAgoraAccess %}{% else %}{% endif %}</span>
<span class="text-base font-medium text-text">Accès Agora</span>
{% endif %}
</div>
</div>
{# Affichage du bouton "Valider" (seulement en mode édition) #}
{% if isEditMode %}
<div class="flex justify-center pt-8 pb-4">
<button type="submit"
class="px-8 py-2.5 bg-bouton hover:bg-bouton-hover text-text text-sm font-bold rounded-lg shadow transition-colors">
Enregistrer les modifications
</button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<p class="text-2xl text-title font-caveat mt-4"> Ma photo</p> {# Fermuture du formulaire (seulement en mode édition) #}
{% if isEditMode %}
{{ form_end(form) }}
{% endif %}
{# Gestion du dépôt d'un fichier image #}
<div class="w-full mt-2">
{{ form_label(form.image, 'Choisir un fichier', {
'label_attr': {'class': 'block mb-2.5 text-sm font-medium text-gris-fonce'}
}) }}
{{ form_widget(form.image, {
'attr': {
'class': 'cursor-pointer bg-white border border-gris-clair text-text text-sm rounded-lg focus:outline-none focus:ring-1 focus:ring-bouton focus:border-bouton block w-full shadow-sm placeholder-gris-moyen file:mr-4 file:py-2.5 file:px-4 file:border-0 file:border-r file:border-gris-clair file:bg-gris-clair file:text-gris-fonce hover:file:bg-gris-moyen file:cursor-pointer transition-colors',
'aria-describedby': 'file_input_help'
}
}) }}
<p class="mt-1 text-sm text-gris-moyen" id="file_input_help">
JPG, JPEG ou PNG (Taille max : 2Mo).
</p>
</div>
</div> </div>
{# Gestion de la colonne avec les "infos persos" #}
<div class="md:col-span-2">
<h1 class="text-4xl font-caveat text-text mb-6 text-center sm:text-center">Mon profil</h1>
<h2 class="text-2xl font-caveat text-text mb-6 text-center sm:text-center">Mes informations personnelles</h2>
{# Gestion du formulaire qui regroupe toutes les infos perso #}
<div class="flex flex-col gap-6">
{# Champ NOM et Prénom #}
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1">
{{ form_label(form.firstName, 'NOM :', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.firstName, {
'attr': {'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-shadow'}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.firstName) }}
</div>
</div>
<div class="space-y-1">
{{ form_label(form.lastName, 'Prénom :', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.lastName, {
'attr': {'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-shadow'}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.lastName) }}
</div>
</div>
</div>
{# Champ Téléphone #}
<div class="space-y-1">
{{ form_label(form.telephone, 'Numéro de téléphone', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.telephone) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic">
{{ form_errors(form.telephone) }}
</div>
</div>
{# Champ E-mail #}
<div class="space-y-1">
{{ form_label(form.email, 'E-mail :', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.email, {
'attr': {'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-shadow'}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.email) }}
</div>
</div>
{# Champ E-mail de secours #}
<div class="space-y-1">
{{ form_label(form.alternateEmail, 'E-mail de secours :', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.alternateEmail, {
'attr': {'class': 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton placeholder-gris-moyen transition-shadow'}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.alternateEmail) }}
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4 pt-2">
<button type="submit"
class="flex-1 py-3 bg-bouton hover:bg-bouton-hover text-text font-bold rounded-lg shadow transition-colors">
Valider
</button>
</div>
</div>
</div>
{{ form_end(form) }}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,11 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %}Accueil | {{ parent() }}{% endblock %}
{% block body %}
<div class="min-h-screen bg-bg-primaire py-8 w-full font-sora">
<div>
<span>Identifiant : {{ user.identifiantKaz }}</span>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,2 @@
# translations/security.fr.yaml
"Invalid credentials.": "Identifiants invalides. Veuillez vérifier votre email ou votre mot de passe."