feat/cnx_api #11

Merged
maurine merged 15 commits from feat/cnx_api into main 2026-03-30 12:08:52 +02:00
19 changed files with 534 additions and 73 deletions
Showing only changes of commit d81e450a0e - Show all commits

2
.env
View File

@@ -1,5 +1,5 @@
APP_ENV=dev APP_ENV=dev
APP_SECRET= APP_SECRET=je_te_remplis_parce_que_tu_me_mets_des_messages_d_erreur
APP_SHARE_DIR=var/share APP_SHARE_DIR=var/share
APP_VERSION=0.0.1 APP_VERSION=0.0.1
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"

5
.gitignore vendored
View File

@@ -33,3 +33,8 @@ Thumbs.db
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
###< symfony/webpack-encore-bundle ### ###< symfony/webpack-encore-bundle ###
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###

View File

@@ -43,6 +43,7 @@
"symfony/web-link": "8.0.*", "symfony/web-link": "8.0.*",
"symfony/webpack-encore-bundle": "^2.4", "symfony/webpack-encore-bundle": "^2.4",
"symfony/yaml": "8.0.*", "symfony/yaml": "8.0.*",
"symfonycasts/tailwind-bundle": "^0.12.0",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0" "twig/twig": "^2.12|^3.0"
}, },
@@ -80,7 +81,8 @@
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {
"cache:clear": "symfony-cmd", "cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd" "assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
}, },
"post-install-cmd": [ "post-install-cmd": [
"@auto-scripts" "@auto-scripts"

228
composer.lock generated
View File

@@ -4,8 +4,85 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "4597facec97a6ff342cba0371179ad02", "content-hash": "b2f5377481dc83317c4b7c49f2dd183e",
"packages": [ "packages": [
{
"name": "composer/semver",
"version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2025-08-20T19:15:30+00:00"
},
{ {
"name": "doctrine/collections", "name": "doctrine/collections",
"version": "2.6.0", "version": "2.6.0",
@@ -1950,6 +2027,87 @@
], ],
"time": "2026-01-13T13:06:50+00:00" "time": "2026-01-13T13:06:50+00:00"
}, },
{
"name": "symfony/asset-mapper",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset-mapper.git",
"reference": "80635c3722b9bb5481e0282497ae23796dcd3712"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/asset-mapper/zipball/80635c3722b9bb5481e0282497ae23796dcd3712",
"reference": "80635c3722b9bb5481e0282497ae23796dcd3712",
"shasum": ""
},
"require": {
"composer/semver": "^3.0",
"php": ">=8.4",
"symfony/filesystem": "^7.4|^8.0",
"symfony/http-client": "^7.4|^8.0"
},
"require-dev": {
"symfony/asset": "^7.4|^8.0",
"symfony/browser-kit": "^7.4|^8.0",
"symfony/console": "^7.4|^8.0",
"symfony/event-dispatcher-contracts": "^3.0",
"symfony/finder": "^7.4|^8.0",
"symfony/framework-bundle": "^7.4|^8.0",
"symfony/http-foundation": "^7.4|^8.0",
"symfony/http-kernel": "^7.4|^8.0",
"symfony/process": "^7.4|^8.0",
"symfony/runtime": "^7.4|^8.0",
"symfony/web-link": "^7.4|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\AssetMapper\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Maps directories of assets & makes them available in a public directory with versioned filenames.",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/asset-mapper/tree/v8.0.6"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-02-17T13:07:04+00:00"
},
{ {
"name": "symfony/cache", "name": "symfony/cache",
"version": "v8.0.5", "version": "v8.0.5",
@@ -3293,16 +3451,16 @@
}, },
{ {
"name": "symfony/form", "name": "symfony/form",
"version": "v8.0.4", "version": "v8.0.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/form.git", "url": "https://github.com/symfony/form.git",
"reference": "c34ec2c2648e2dfedab3ce7e3c6c86f8d89c3092" "reference": "954e17b053dad9fb227ebd90260752e3a46bb06a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/form/zipball/c34ec2c2648e2dfedab3ce7e3c6c86f8d89c3092", "url": "https://api.github.com/repos/symfony/form/zipball/954e17b053dad9fb227ebd90260752e3a46bb06a",
"reference": "c34ec2c2648e2dfedab3ce7e3c6c86f8d89c3092", "reference": "954e17b053dad9fb227ebd90260752e3a46bb06a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3364,7 +3522,7 @@
"description": "Allows to easily create, process and reuse HTML forms", "description": "Allows to easily create, process and reuse HTML forms",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/form/tree/v8.0.4" "source": "https://github.com/symfony/form/tree/v8.0.7"
}, },
"funding": [ "funding": [
{ {
@@ -3384,7 +3542,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-23T11:07:10+00:00" "time": "2026-03-06T13:17:40+00:00"
}, },
{ {
"name": "symfony/framework-bundle", "name": "symfony/framework-bundle",
@@ -7473,6 +7631,62 @@
], ],
"time": "2025-12-04T18:17:06+00:00" "time": "2025-12-04T18:17:06+00:00"
}, },
{
"name": "symfonycasts/tailwind-bundle",
"version": "v0.12.0",
"source": {
"type": "git",
"url": "https://github.com/SymfonyCasts/tailwind-bundle.git",
"reference": "17c85e25d3ceb54b8599e8ca4c5b67c485f2a48a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SymfonyCasts/tailwind-bundle/zipball/17c85e25d3ceb54b8599e8ca4c5b67c485f2a48a",
"reference": "17c85e25d3ceb54b8599e8ca4c5b67c485f2a48a",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/asset-mapper": "^6.3|^7.0|^8.0",
"symfony/cache": "^6.3|^7.0|^8.0",
"symfony/console": "^5.4|^6.3|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.2|^3.0",
"symfony/http-client": "^5.4|^6.3|^7.0|^8.0",
"symfony/process": "^5.4|^6.3|^7.0|^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
"symfony/filesystem": "^6.3|^7.0|^8.0",
"symfony/framework-bundle": "^6.3|^7.0|^8.0",
"symfony/phpunit-bridge": "^6.3.9|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfonycasts\\TailwindBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ryan Weaver",
"homepage": "https://symfonycasts.com"
}
],
"description": "Delightful Tailwind Support for Symfony + AssetMapper",
"keywords": [
"asset-mapper",
"tailwind"
],
"support": {
"issues": "https://github.com/SymfonyCasts/tailwind-bundle/issues",
"source": "https://github.com/SymfonyCasts/tailwind-bundle/tree/v0.12.0"
},
"time": "2025-11-24T10:14:04+00:00"
},
{ {
"name": "twig/extra-bundle", "name": "twig/extra-bundle",
"version": "v3.23.0", "version": "v3.23.0",

View File

@@ -15,4 +15,5 @@ return [
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true], Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true],
]; ];

View File

@@ -0,0 +1,11 @@
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/
missing_import_mode: strict
when@prod:
framework:
asset_mapper:
missing_import_mode: warn

View File

@@ -32,7 +32,17 @@ security:
check_path: app_login check_path: app_login
enable_csrf: true enable_csrf: true
logout: logout:
# où rediriger après la déconnexion
path: app_logout path: app_logout
# custom_authenticator: App\Security\AppCustomAuthenticator
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
path: /
# by default, the feature is enabled by checking a checkbox in the
# login form, uncomment the following line to always enable it.
#always_remember_me: true
# where to redirect after logout # where to redirect after logout
# target: app_any_route # target: app_any_route
@@ -45,8 +55,8 @@ security:
# Note: Only the *first* matching rule is applied # Note: Only the *first* matching rule is applied
# autorisations # autorisations
access_control: access_control:
# - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER } - { path: ^/user, roles: ROLE_USER }
when@test: when@test:
security: security:

View File

@@ -0,0 +1,6 @@
symfonycasts_tailwind:
# Specify the EXACT version of Tailwind CSS you want to use
binary_version: 'v4.1.11'
# Alternatively, you can specify the path to the binary that you manage yourself
#binary: 'node_modules/.bin/tailwindcss'

View File

@@ -280,7 +280,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }>, * }>,
* }, * },
* asset_mapper?: bool|array{ // Asset Mapper configuration * asset_mapper?: bool|array{ // Asset Mapper configuration
* enabled?: bool|Param, // Default: false * enabled?: bool|Param, // Default: true
* paths?: array<string, scalar|Param|null>, * paths?: array<string, scalar|Param|null>,
* excluded_patterns?: list<scalar|Param|null>, * excluded_patterns?: list<scalar|Param|null>,
* exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true
@@ -1465,6 +1465,15 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }, * },
* default_transport?: scalar|Param|null, // Default: "default" * default_transport?: scalar|Param|null, // Default: "default"
* } * }
* @psalm-type SymfonycastsTailwindConfig = array{
* input_css?: list<scalar|Param|null>,
* config_file?: scalar|Param|null, // Path to the tailwind.config.js file // Default: "%kernel.project_dir%/tailwind.config.js"
* binary?: scalar|Param|null, // The tailwind binary to use instead of downloading a new one // Default: null
* binary_version?: scalar|Param|null, // Tailwind CLI version to download - null means the latest version // Default: null
* binary_platform?: "auto"|"linux-arm64"|"linux-arm64-musl"|"linux-x64"|"linux-x64-musl"|"macos-arm64"|"macos-x64"|"windows-x64"|Param, // Tailwind CLI platform to download - "auto" will try to detect the platform automatically // Default: "auto"
* postcss_config_file?: scalar|Param|null, // Path to PostCSS config file which is passed to the Tailwind CLI // Default: null
* strict_mode?: bool|Param|null, // When enabled, an exception will be thrown if there are no built assets (default: false in `test` env, true otherwise) // Default: null
* }
* @psalm-type ConfigType = array{ * @psalm-type ConfigType = array{
* imports?: ImportsConfig, * imports?: ImportsConfig,
* parameters?: ParametersConfig, * parameters?: ParametersConfig,
@@ -1479,6 +1488,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* webpack_encore?: WebpackEncoreConfig, * webpack_encore?: WebpackEncoreConfig,
* stimulus?: StimulusConfig, * stimulus?: StimulusConfig,
* turbo?: TurboConfig, * turbo?: TurboConfig,
* symfonycasts_tailwind?: SymfonycastsTailwindConfig,
* "when@dev"?: array{ * "when@dev"?: array{
* imports?: ImportsConfig, * imports?: ImportsConfig,
* parameters?: ParametersConfig, * parameters?: ParametersConfig,
@@ -1496,6 +1506,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* webpack_encore?: WebpackEncoreConfig, * webpack_encore?: WebpackEncoreConfig,
* stimulus?: StimulusConfig, * stimulus?: StimulusConfig,
* turbo?: TurboConfig, * turbo?: TurboConfig,
* symfonycasts_tailwind?: SymfonycastsTailwindConfig,
* }, * },
* "when@prod"?: array{ * "when@prod"?: array{
* imports?: ImportsConfig, * imports?: ImportsConfig,
@@ -1511,6 +1522,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* webpack_encore?: WebpackEncoreConfig, * webpack_encore?: WebpackEncoreConfig,
* stimulus?: StimulusConfig, * stimulus?: StimulusConfig,
* turbo?: TurboConfig, * turbo?: TurboConfig,
* symfonycasts_tailwind?: SymfonycastsTailwindConfig,
* }, * },
* "when@test"?: array{ * "when@test"?: array{
* imports?: ImportsConfig, * imports?: ImportsConfig,
@@ -1527,6 +1539,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* webpack_encore?: WebpackEncoreConfig, * webpack_encore?: WebpackEncoreConfig,
* stimulus?: StimulusConfig, * stimulus?: StimulusConfig,
* turbo?: TurboConfig, * turbo?: TurboConfig,
* symfonycasts_tailwind?: SymfonycastsTailwindConfig,
* }, * },
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias * ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
* imports?: ImportsConfig, * imports?: ImportsConfig,

28
importmap.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
/**
* Returns the importmap for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*/
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
],
];

View File

@@ -2,38 +2,63 @@
namespace App\Controller; namespace App\Controller;
use LogicException; use App\Repository\UserRepository; // 👈 AJOUTE CETTE LIGNE ICI !
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; // 👈 ET CELLE-CI AUSSI !
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController class SecurityController extends AbstractController
{ {
#[Route(path: '/login', name: 'app_login', methods: ['GET','POST'])] #[Route(path: '/test-password', name: 'test_password')]
public function login(AuthenticationUtils $authenticationUtils): Response public function testPassword(UserRepository $userRepo, UserPasswordHasherInterface $hasher): Response
{ {
// 1. On va chercher ton admin directement en base
$admin = $userRepo->findOneBy(['email' => 'admin@kaz.fr']);
if (!$admin) {
dd("L'utilisateur n'existe pas dans la base lue par ce contrôleur !");
}
// 2. On vérifie si "password" est bien le bon mot de passe
$isValid = $hasher->isPasswordValid($admin, 'password');
// 3. On affiche le résultat brut
dd([
'email' => $admin->getEmail(),
'mot_de_passe_hache' => $admin->getPassword(),
'est_ce_que_password_est_valide' => $isValid
]);
}
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils, Request $request): Response // 👈 2. ON AJOUTE $request ICI
{
// 🚨 3. NOTRE CODE DE DÉTECTIVE POUR VOIR CE QUI EST ENVOYÉ :
if ($request->isMethod('POST')) {
dd($request->request->all());
}
// 🚨 FIN DU CODE DE DÉTECTIVE
// si on a un utilisateur déjà connecté, alors on le redirige sur la page d'accueil
if ($this->getUser()) { if ($this->getUser()) {
return $this->redirectToRoute('app_home'); return $this->redirectToRoute('app_home');
} }
// get the login error if there is one // Récupération de l'erreur de connexion (s'il y en a une)
$error = $authenticationUtils->getLastAuthenticationError(); $error = $authenticationUtils->getLastAuthenticationError();
// Récupération du dernier nom d'utilisateur saisi par l'adhérent
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername(); $lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', [ return $this->render('security/login.html.twig', [
'last_username' => $lastUsername, 'last_username' => $lastUsername,
'error' => $error, 'error' => $error
]); ]);
} }
#[Route(path: '/logout', name: 'app_logout', methods: ['POST'])] #[Route(path: '/logout', name: 'app_logout')]
public function logout(): void public function logout(): void
{ {
throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
} }
} }

View File

@@ -2,9 +2,14 @@
namespace App\Controller; namespace App\Controller;
use App\Form\ChangePasswordType;
use App\Service\KazApiService; use App\Service\KazApiService;
use Doctrine\ORM\EntityManagerInterface;
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\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
@@ -14,9 +19,6 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
class UserController extends AbstractController class UserController extends AbstractController
{ {
// TODO : UserPasswordHasherInterface
// voir : https://symfony.com/doc/current/security/passwords.html#hashing-the-password
/** /**
* Permet de vérifier si un utilisateur existe dans le ldap. * Permet de vérifier si un utilisateur existe dans le ldap.
* *
@@ -30,6 +32,7 @@ class UserController extends AbstractController
* @throws ServerExceptionInterface * @throws ServerExceptionInterface
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
*/ */
#[Route('/user/{email}', name: 'app_user', methods: ['GET'])] #[Route('/user/{email}', name: 'app_user', methods: ['GET'])]
public function index(string $email, KazApiService $apiClient): Response public function index(string $email, KazApiService $apiClient): Response
{ {
@@ -39,4 +42,45 @@ class UserController extends AbstractController
'exist' => $exist, 'exist' => $exist,
]); ]);
} }
#[Route('/user/mot-de-passe', name: 'app_user_edit_password', methods: ['GET', 'POST'])]
public function editPassword(Request $request, UserPasswordHasherInterface $hasher, EntityManagerInterface $entityManager): Response
{
# Récupération de l'adhérent actuellement connecté
$user = $this->getUser();
# Création du formulaire
$form = $this->createForm(ChangePasswordType::class);
# Liaison du formulaire à la requête HTTP
$form->handleRequest($request);
# Vérification du formulaire, s'il est bien soumis et valide
if ($form->isSubmitted() && $form->isValid()) {
# Récupération des données du formulaire
$user = $this->getUser();
$plainOldPassword = $form->get('oldPassword')->getData();
$newPassword = $form->get('newPassword')->getData();
# Vérification de l'ancien mot de passe
if (!$hasher->isPasswordValid($user, $plainOldPassword)) {
$form->get('oldPassword')->addError(new FormError('L\'ancien mot de passe est incorrect.'));
} else {
# Si tout est OK : Hachage du mot de passe
$hashedPassword = $hasher->hashPassword($user, $newPassword);
$user->setPassword($hashedPassword);
# Sauvegarde en BDD
$entityManager->flush();
# Message de succès pour l'adhérent
$this->addFlash('success', 'Votre mot de passe a bien été mis à jour !');
return $this->redirectToRoute('app_user_edit_password');
}
}
return $this->render('user/edit_password.html.twig', [
'form' => $form->createView(),
]);
}
} }

View File

@@ -29,20 +29,15 @@ class AppFixtures extends Fixture
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']); $user->setRoles(['ROLE_USER']);
# 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');
@@ -52,10 +47,31 @@ 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());
}
// Création d'un compte de test fixe
$admin = new User();
$admin->setEmail('admin@kaz.bzh');
$admin->setRoles(['ROLE_USER', 'ROLE_ADMIN']);
$admin->setPassword($this->hasher->hashPassword($admin, 'password'));
$admin->setFirstName('Admin');
$admin->setLastName('KAZ');
// Remplissage des champs obligatoires restants pour éviter les erreurs SQL
$admin->setEmailDeSecours('secours@kaz.bzh');
$admin->setIdentifiantKaz('ADMIN-KAZ-001');
$admin->setQuota('5G');
$admin->setEmailQuota('1G');
$admin->setNextcloudQuota('10G');
$admin->setHasNextcloudAccess(true);
$admin->setHasMobilizon(true);
$admin->setHasAgoraAccess(true);
$manager->persist($admin);
# 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);
}
# Exécution réelle des requêtes SQL (envoi vers la base), une fois la bouche finie # Exécution réelle des requêtes SQL (envoi vers la base), une fois la bouche finie
$manager->flush(); $manager->flush();
} }

View File

@@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
@@ -11,6 +12,7 @@ use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')] #[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
class User implements UserInterface, PasswordAuthenticatedUserInterface class User implements UserInterface, PasswordAuthenticatedUserInterface
{ {
#[ORM\Id] #[ORM\Id]
@@ -35,7 +37,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private ?string $password = null; private ?string $password = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $emailQuota = null; private ?string $emailQuota = '1G';
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $emailDeSecours = null; private ?string $emailDeSecours = null;
@@ -50,7 +52,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private ?bool $hasNextcloudAccess = null; private ?bool $hasNextcloudAccess = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $nextcloudQuota = '1G'; private ?string $nextcloudQuota = null;
#[ORM\Column] #[ORM\Column]
private ?bool $hasMobilizon = null; private ?bool $hasMobilizon = null;
@@ -104,9 +106,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
public function getRoles(): array public function getRoles(): array
{ {
$roles = $this->roles; $roles = $this->roles;
// guarantee every user at least has ROLE_USER $roles[] = 'ROLE_USER'; // garantit qu'il a au moins ce rôle
$roles[] = 'ROLE_USER';
return array_unique($roles); return array_unique($roles);
} }
@@ -120,6 +120,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
/**
* @see UserInterface
* Ajout de cette fonction, car obligatoire pour faire fonctionner UserInterface correctement
*/
public function eraseCredentials(): void
{
// Si vous stockez des données temporaires sensibles sur l'utilisateur, nettoyez-les ici
// $this->plainPassword = null;
}
/** /**
* @see PasswordAuthenticatedUserInterface * @see PasswordAuthenticatedUserInterface
*/ */

View File

@@ -62,6 +62,21 @@
"bin/phpunit" "bin/phpunit"
] ]
}, },
"symfony/asset-mapper": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/asset_mapper.yaml",
"importmap.php"
]
},
"symfony/console": { "symfony/console": {
"version": "8.0", "version": "8.0",
"recipe": { "recipe": {
@@ -341,6 +356,18 @@
"webpack.config.js" "webpack.config.js"
] ]
}, },
"symfonycasts/tailwind-bundle": {
"version": "0.12",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "0.8",
"ref": "d0bd0276f74de90adfaa4c6cd74cc0caacd77e0a"
},
"files": [
"config/packages/symfonycasts_tailwind.yaml"
]
},
"twig/extra-bundle": { "twig/extra-bundle": {
"version": "v3.23.0" "version": "v3.23.0"
} }

View File

@@ -1,11 +1,23 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: [ content: [
"./templates/**/*.html.twig",
"./assets/**/*.js", "./assets/**/*.js",
"./templates/**/*.html.twig",
], ],
theme: { theme: {
extend: {}, extend: {
colors: {
'brand-teal': '#4DD5C8', // Boutons
'brand-gold': '#E6A638', // Accent
'brand-dark': '#000000', // Police
'bg-primary': '#F9FCF7', // Fond principal
'bg-secondary': '#23978B', // Fond secondaire
},
fontFamily: {
'sans': ['Sora', 'system-ui', 'sans-serif'],
'title': ['Caveat', 'cursive'],
},
},
}, },
plugins: [], plugins: [],
} }

View File

@@ -2,16 +2,30 @@
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title> <title>{% block title %}Association KAZ{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
{% block stylesheets %} {% block stylesheets %}
<link rel="stylesheet" href="/assets/styles/app.css"> <link rel="stylesheet" href="{{ asset('styles/app.css') }}">
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
<main class="container mx-auto mt-4 px-4">
{# Section des notifications #}
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="p-4 mb-4 rounded-lg shadow-md border-l-4 {{ label == 'success' ? 'bg-brand-teal text-brand-dark border-bg-secondary' : 'bg-brand-gold text-brand-dark border-red-700' }}">
{{ message }}
</div>
{% endfor %}
{% endfor %}
{% block body %}{% endblock %} {% block body %}{% endblock %}
</main>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,28 @@
{% extends 'base.html.twig' %}
{% block title %}Accueil | Tableau de bord KAZ{% endblock %}
{% block body %}
<div class="mt-8 p-6 bg-white rounded-lg shadow-md border border-gray-200">
<h1 class="text-3xl font-bold mb-4">Bienvenue sur ton tableau de bord KAZ </h1>
{# Vérification si un utilisateur est connecté #}
{% if app.user %}
<div class="p-4 bg-green-100 text-green-800 rounded mb-4">
<p class="font-semibold">Succès ! Tu es connecté avec l'adresse :</p>
<p class="text-xl">{{ app.user.userIdentifier }}</p>
</div>
<p class="mt-4">
<a href="{{ path('app_logout') }}" class="inline-block px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">
Se déconnecter
</a>
</p>
{% else %}
<p class="mb-4">Tu n'es pas encore connecté à ton espace.</p>
<a href="{{ path('app_login') }}" class="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Aller à la page de connexion
</a>
{% endif %}
</div>
{% endblock %}

View File

@@ -1,39 +1,34 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Log in!{% endblock %} {% block title %}Se connecter | {{ parent() }}{% endblock %}
{% block body %} {% block body %}
<form method="post"> <form method="post">
{% if error %} {% if error %}
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 border border-red-200"> <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
Email ou mot de passe invalide.
</div>
{% endif %} {% endif %}
{% if app.user %} {% if app.user %}
<div class="mb-3"> <div class="mb-3">
You are logged in as {{ app.user.userIdentifier }}, <a href="{{ logout_path() }}">Se déconnecter</a> You are logged in as {{ app.user.userIdentifier }}, <a href="{{ logout_path() }}">Logout</a>
</div> </div>
{% endif %} {% endif %}
<h1 class="h3 mb-3 font-weight-normal">Se connecter à mon compte KAZ</h1> <h1 class="h3 mb-3 font-weight-normal">Se connecter à mon tableau de bord KAZ</h1>
<label for="username">Email</label> <label for="inputEmail">Email :</label>
<input type="email" value="{{ last_username }}" name="_username" id="username" class="form-control" autocomplete="email" required autofocus> <input type="email" value="{{ last_username }}" name="_username" id="inputEmail" class="form-control" autocomplete="email" required autofocus>
<label for="password">Mot de passe</label> <label for="inputPassword">Mot de passe : </label>
<input type="password" name="_password" id="password" class="form-control" autocomplete="current-password" required> <input type="password" name="_password" id="inputPassword" class="form-control" autocomplete="current-password" required>
<input type="hidden" name="_csrf_token" data-controller="csrf-protection" value="{{ csrf_token('authenticate') }}"> <input type="hidden" name="_csrf_token" data-controller="csrf-protection" value="{{ csrf_token('authenticate') }}">
{# TODO : vérifier qu'il y a bien une option "Se souvenir de moi" dans mon firewall pour activer la fonctionnalité. #}
{# pour en savoir plus : https://symfony.com/doc/current/security/remember_me.html #}
<div class="checkbox mb-3"> <div class="checkbox mb-3">
<input type="checkbox" name="_remember_me" id="_remember_me"> <label>
<label for="_remember_me">Se souvenir de moi</label> <input type="checkbox" name="_remember_me"> Se souvenir de moi
</label>
</div> </div>
<button class="btn btn-lg btn-primary" type="submit"> <button class="btn btn-lg btn-primary" type="submit">
Se connecter Se connecter
</button> </button>
</form> </form>
{% endblock %} {% endblock %}