Compare commits
2 Commits
v0.0.55
...
feat/202-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e1b431e57 | |||
| ce49785c79 |
7
.env
7
.env
@@ -39,3 +39,10 @@ DEFAULT_URI=http://localhost
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
JWT_SECRET_KEY=
|
||||
JWT_PUBLIC_KEY=
|
||||
JWT_PASSPHRASE=
|
||||
COOKIE_SECURE=1
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
/var/
|
||||
/vendor/
|
||||
/LOG/
|
||||
/config/jwt/*.pem
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
@@ -23,3 +24,7 @@
|
||||
###> docker ###
|
||||
docker/.env.docker.local
|
||||
###< docker ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
/config/jwt/*.pem
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
6
.idea/data_source_mapping.xml
generated
Normal file
6
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/ferme.iml
generated
3
.idea/ferme.iml
generated
@@ -145,6 +145,9 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sabberworm/php-css-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/thecodingmachine/safe" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/lcobucci/jwt" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||
<excludePattern pattern="reference.php" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
2
.idea/php.xml
generated
2
.idea/php.xml
generated
@@ -151,6 +151,8 @@
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
||||
|
||||
69
.idea/workspace.xml
generated
69
.idea/workspace.xml
generated
@@ -4,13 +4,8 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package-lock.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" afterDir="false" />
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout de l'authentification avec lexik">
|
||||
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -195,6 +190,8 @@
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
@@ -207,28 +204,31 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"git-widget-placeholder": "feat/reception-generation-bon",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "reference.webide.settings.project.settings.php.debug",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"git-widget-placeholder": "feat/202-connexion-utilisateur",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "editor.preferences.tabs",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"vue.recent.templates": [
|
||||
"Vue Composition API Component"
|
||||
"keyToStringList": {
|
||||
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||
"TEXT"
|
||||
],
|
||||
"vue.recent.templates": [
|
||||
"Vue Composition API Component"
|
||||
]
|
||||
}
|
||||
}</component>
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
||||
@@ -331,7 +331,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768555180530</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="10" />
|
||||
<task id="LOCAL-00010" summary="feat : ajout de l'authentification avec lexik">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768832208350</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768832208350</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="11" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -387,6 +395,11 @@
|
||||
<MESSAGE value="test : ajout de TU sur les services et providers" />
|
||||
<MESSAGE value="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global" />
|
||||
<MESSAGE value="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur" />
|
||||
<MESSAGE value="feat : ajout de l'authentification avec lexik" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout de l'authentification avec lexik" />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
<select />
|
||||
</component>
|
||||
</project>
|
||||
@@ -9,12 +9,17 @@ Ajouter dans le fichier .env
|
||||
- DATABASE_URL
|
||||
- PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||
- PONT_BASCULE_URL
|
||||
- JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
||||
- JWT_PUBLIC_KEY
|
||||
- JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
||||
- COOKIE_SECURE=0 (en dev 0 et en prod 1)
|
||||
|
||||
Ajouter dans le fichier .env du frontend
|
||||
- NUXT_PUBLIC_API_BASE
|
||||
|
||||
### Added
|
||||
* [#203] Réceptions — Parcours de pesée multi-étapes (début)
|
||||
* [#202] Authentification — Connexion utilisateur (JWT)
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
37
README.md
37
README.md
@@ -21,13 +21,17 @@ Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifi
|
||||
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
||||
|
||||
Vérifier que dans le .env.local, vous avez :
|
||||
* APP_SECRET (doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
||||
* DATABASE_URL
|
||||
* APP_SECRET (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));" et doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
||||
* DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||
* PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||
* PONT_BASCULE_URL
|
||||
* JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
||||
* JWT_PUBLIC_KEY
|
||||
* JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
||||
* COOKIE_SECURE=0 (en dev 0 et en prod 1)
|
||||
|
||||
Vérifier que dans le .env du dossier frontend, vous avez :
|
||||
* NUXT_PUBLIC_API_BASE
|
||||
* NUXT_PUBLIC_API_BASE="http://localhost:8080/api"
|
||||
|
||||
### Configuration xdebug
|
||||
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||
@@ -54,6 +58,19 @@ make dev-nuxt
|
||||
```
|
||||
Le front sera accessible sur http://localhost:3000
|
||||
|
||||
### Authentification
|
||||
Ce projet utilise l'authentification JWT avec un cookie httpOnly (LexikJWTAuthenticationBundle).
|
||||
Le frontend ne lit jamais directement le token, le navigateur envoie automatiquement le cookie.
|
||||
|
||||
### Login flow
|
||||
- Frontend envoie les identifiants à:
|
||||
- `POST /api/login_check`
|
||||
- Backend returns:
|
||||
- `204 No Content` (normal)
|
||||
- `Set-Cookie: BEARER=...; HttpOnly`
|
||||
- Le cookie est automatiquement envoyé pour les futures requêtes.
|
||||
- La déconnexion utilise `POST /api/logout` et redirige vers `/login`.
|
||||
|
||||
## Commandes utiles
|
||||
Pour restart le container
|
||||
```bash
|
||||
@@ -71,3 +88,17 @@ Pour clear le cache Symfony
|
||||
```bash
|
||||
make cache-clear
|
||||
```
|
||||
Faire une migration
|
||||
```bash
|
||||
make migration-migrate
|
||||
```
|
||||
Pour générer un password pour un user
|
||||
```bash
|
||||
make shell
|
||||
php bin/console security:hash-password
|
||||
```
|
||||
Sélectionner entity User, taper sont mdp, le copier et l'ajouter dans l'insert de bdd suivant :
|
||||
```sql
|
||||
INSERT INTO "user" (username, roles, password)
|
||||
VALUES ('Mon user', '["ROLE_USER"]', 'Mon mdp hashé');
|
||||
```
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||
"doctrine/orm": "^3.6",
|
||||
"dompdf/dompdf": "^3.1",
|
||||
"lexik/jwt-authentication-bundle": "*",
|
||||
"nelmio/cors-bundle": "^2.6",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
|
||||
191
composer.lock
generated
191
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "5cd56256b984963ecd4eaa17f2612f57",
|
||||
"content-hash": "f619208e7dd3272e671e7c2b139afa87",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/doctrine-common",
|
||||
@@ -2516,6 +2516,195 @@
|
||||
},
|
||||
"time": "2026-01-02T16:01:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lcobucci/jwt",
|
||||
"version": "5.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lcobucci/jwt.git",
|
||||
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
|
||||
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"ext-sodium": "*",
|
||||
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"psr/clock": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"infection/infection": "^0.29",
|
||||
"lcobucci/clock": "^3.2",
|
||||
"lcobucci/coding-standard": "^11.0",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/extension-installer": "^1.2",
|
||||
"phpstan/phpstan": "^1.10.7",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.3.10",
|
||||
"phpstan/phpstan-strict-rules": "^1.5.0",
|
||||
"phpunit/phpunit": "^11.1"
|
||||
},
|
||||
"suggest": {
|
||||
"lcobucci/clock": ">= 3.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lcobucci\\JWT\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Luís Cobucci",
|
||||
"email": "lcobucci@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
|
||||
"keywords": [
|
||||
"JWS",
|
||||
"jwt"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/lcobucci/jwt/issues",
|
||||
"source": "https://github.com/lcobucci/jwt/tree/5.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/lcobucci",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/lcobucci",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-17T11:30:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lexik/jwt-authentication-bundle",
|
||||
"version": "v3.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git",
|
||||
"reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/60df75dc70ee6f597929cb2f0812adda591dfa4b",
|
||||
"reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"lcobucci/jwt": "^5.0",
|
||||
"php": ">=8.2",
|
||||
"symfony/clock": "^6.4|^7.0|^8.0",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/deprecation-contracts": "^2.4|^3.0",
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-bundle": "^6.4|^7.0|^8.0",
|
||||
"symfony/translation-contracts": "^1.0|^2.0|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"api-platform/core": "^3.0|^4.0",
|
||||
"rector/rector": "^1.2",
|
||||
"symfony/browser-kit": "^6.4|^7.0|^8.0",
|
||||
"symfony/console": "^6.4|^7.0|^8.0",
|
||||
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
|
||||
"symfony/filesystem": "^6.4|^7.0|^8.0",
|
||||
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
|
||||
"symfony/phpunit-bridge": "^6.4|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0|^8.0",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
|
||||
"spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jeremy Barthe",
|
||||
"email": "j.barthe@lexik.fr",
|
||||
"homepage": "https://github.com/jeremyb"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Cabot",
|
||||
"email": "n.cabot@lexik.fr",
|
||||
"homepage": "https://github.com/slashfan"
|
||||
},
|
||||
{
|
||||
"name": "Cedric Girard",
|
||||
"email": "c.girard@lexik.fr",
|
||||
"homepage": "https://github.com/cedric-g"
|
||||
},
|
||||
{
|
||||
"name": "Dev Lexik",
|
||||
"email": "dev@lexik.fr",
|
||||
"homepage": "https://github.com/lexik"
|
||||
},
|
||||
{
|
||||
"name": "Robin Chalas",
|
||||
"email": "robin.chalas@gmail.com",
|
||||
"homepage": "https://github.com/chalasr"
|
||||
},
|
||||
{
|
||||
"name": "Lexik Community",
|
||||
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "This bundle provides JWT authentication for your Symfony REST API",
|
||||
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"JWS",
|
||||
"api",
|
||||
"bundle",
|
||||
"jwt",
|
||||
"rest",
|
||||
"symfony"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues",
|
||||
"source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v3.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/chalasr",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-20T17:47:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.0",
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
||||
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
FrameworkBundle::class => ['all' => true],
|
||||
TwigBundle::class => ['all' => true],
|
||||
SecurityBundle::class => ['all' => true],
|
||||
DoctrineBundle::class => ['all' => true],
|
||||
DoctrineMigrationsBundle::class => ['all' => true],
|
||||
NelmioCorsBundle::class => ['all' => true],
|
||||
LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||
ApiPlatformBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
20
config/packages/lexik_jwt_authentication.yaml
Normal file
20
config/packages/lexik_jwt_authentication.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
lexik_jwt_authentication:
|
||||
secret_key: '%kernel.project_dir%/config/jwt/private.pem'
|
||||
public_key: '%kernel.project_dir%/config/jwt/public.pem'
|
||||
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||
token_ttl: 86400
|
||||
token_extractors:
|
||||
authorization_header:
|
||||
enabled: true
|
||||
prefix: Bearer
|
||||
name: Authorization
|
||||
cookie:
|
||||
enabled: true
|
||||
name: BEARER
|
||||
set_cookies:
|
||||
BEARER:
|
||||
lifetime: 86400
|
||||
path: /
|
||||
samesite: lax
|
||||
secure: '%env(bool:COOKIE_SECURE)%'
|
||||
httpOnly: true
|
||||
@@ -4,6 +4,7 @@ nelmio_cors:
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
allow_credentials: true
|
||||
expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
paths:
|
||||
|
||||
4
config/packages/prod/api_platform.yaml
Normal file
4
config/packages/prod/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
api_platform:
|
||||
enable_docs: false
|
||||
enable_swagger: false
|
||||
enable_swagger_ui: false
|
||||
@@ -1,20 +1,43 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
App\Entity\User: 'auto'
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: username
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
# Ensure dev tools and static assets are always allowed
|
||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
login:
|
||||
pattern: ^/login_check
|
||||
stateless: true
|
||||
provider: app_user_provider
|
||||
json_login:
|
||||
check_path: /login_check
|
||||
username_path: username
|
||||
password_path: password
|
||||
success_handler: lexik_jwt_authentication.handler.authentication_success
|
||||
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
||||
api:
|
||||
pattern: ^/
|
||||
stateless: true
|
||||
provider: app_user_provider
|
||||
jwt: ~
|
||||
logout:
|
||||
path: /logout
|
||||
target: /login
|
||||
enable_csrf: false
|
||||
delete_cookies:
|
||||
BEARER:
|
||||
path: /
|
||||
|
||||
# Activate different ways to authenticate:
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
@@ -24,8 +47,9 @@ security:
|
||||
|
||||
# Note: Only the *first* matching rule is applied
|
||||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
- { path: ^/login_check, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/users, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
|
||||
@@ -770,6 +770,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* property?: scalar|null|Param, // Default: null
|
||||
* manager_name?: scalar|null|Param, // Default: null
|
||||
* },
|
||||
* lexik_jwt?: array{
|
||||
* class?: scalar|null|Param, // Default: "Lexik\\Bundle\\JWTAuthenticationBundle\\Security\\User\\JWTUser"
|
||||
* },
|
||||
* }>,
|
||||
* firewalls: array<string, array{ // Default: []
|
||||
* pattern?: scalar|null|Param,
|
||||
@@ -828,6 +831,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* provider?: scalar|null|Param,
|
||||
* user?: scalar|null|Param, // Default: "REMOTE_USER"
|
||||
* },
|
||||
* jwt?: array{
|
||||
* provider?: scalar|null|Param, // Default: null
|
||||
* authenticator?: scalar|null|Param, // Default: "lexik_jwt_authentication.security.jwt_authenticator"
|
||||
* },
|
||||
* login_link?: array{
|
||||
* check_route: scalar|null|Param, // Route that will validate the login link - e.g. "app_login_link_verify".
|
||||
* check_post_only?: scalar|null|Param, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false
|
||||
@@ -1261,6 +1268,91 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* skip_same_as_origin?: bool|Param,
|
||||
* }>,
|
||||
* }
|
||||
* @psalm-type LexikJwtAuthenticationConfig = array{
|
||||
* public_key?: scalar|null|Param, // The key used to sign tokens (useless for HMAC). If not set, the key will be automatically computed from the secret key. // Default: null
|
||||
* additional_public_keys?: list<scalar|null|Param>,
|
||||
* secret_key?: scalar|null|Param, // The key used to sign tokens. It can be a raw secret (for HMAC), a raw RSA/ECDSA key or the path to a file itself being plaintext or PEM. // Default: null
|
||||
* pass_phrase?: scalar|null|Param, // The key passphrase (useless for HMAC) // Default: ""
|
||||
* token_ttl?: scalar|null|Param, // Default: 3600
|
||||
* allow_no_expiration?: bool|Param, // Allow tokens without "exp" claim (i.e. indefinitely valid, no lifetime) to be considered valid. Caution: usage of this should be rare. // Default: false
|
||||
* clock_skew?: scalar|null|Param, // Default: 0
|
||||
* encoder?: array{
|
||||
* service?: scalar|null|Param, // Default: "lexik_jwt_authentication.encoder.lcobucci"
|
||||
* signature_algorithm?: scalar|null|Param, // Default: "RS256"
|
||||
* },
|
||||
* user_id_claim?: scalar|null|Param, // Default: "username"
|
||||
* token_extractors?: array{
|
||||
* authorization_header?: bool|array{
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* prefix?: scalar|null|Param, // Default: "Bearer"
|
||||
* name?: scalar|null|Param, // Default: "Authorization"
|
||||
* },
|
||||
* cookie?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* name?: scalar|null|Param, // Default: "BEARER"
|
||||
* },
|
||||
* query_parameter?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* name?: scalar|null|Param, // Default: "bearer"
|
||||
* },
|
||||
* split_cookie?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* cookies?: list<scalar|null|Param>,
|
||||
* },
|
||||
* },
|
||||
* remove_token_from_body_when_cookies_used?: scalar|null|Param, // Default: true
|
||||
* set_cookies?: array<string, array{ // Default: []
|
||||
* lifetime?: scalar|null|Param, // The cookie lifetime. If null, the "token_ttl" option value will be used // Default: null
|
||||
* samesite?: "none"|"lax"|"strict"|Param, // Default: "lax"
|
||||
* path?: scalar|null|Param, // Default: "/"
|
||||
* domain?: scalar|null|Param, // Default: null
|
||||
* secure?: scalar|null|Param, // Default: true
|
||||
* httpOnly?: scalar|null|Param, // Default: true
|
||||
* partitioned?: scalar|null|Param, // Default: false
|
||||
* split?: list<scalar|null|Param>,
|
||||
* }>,
|
||||
* api_platform?: bool|array{ // API Platform compatibility: add check_path in OpenAPI documentation.
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* check_path?: scalar|null|Param, // The login check path to add in OpenAPI. // Default: null
|
||||
* username_path?: scalar|null|Param, // The path to the username in the JSON body. // Default: null
|
||||
* password_path?: scalar|null|Param, // The path to the password in the JSON body. // Default: null
|
||||
* },
|
||||
* access_token_issuance?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* signature?: array{
|
||||
* algorithm: scalar|null|Param, // The algorithm use to sign the access tokens.
|
||||
* key: scalar|null|Param, // The signature key. It shall be JWK encoded.
|
||||
* },
|
||||
* encryption?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* key_encryption_algorithm: scalar|null|Param, // The key encryption algorithm is used to encrypt the token.
|
||||
* content_encryption_algorithm: scalar|null|Param, // The key encryption algorithm is used to encrypt the token.
|
||||
* key: scalar|null|Param, // The encryption key. It shall be JWK encoded.
|
||||
* },
|
||||
* },
|
||||
* access_token_verification?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* signature?: array{
|
||||
* header_checkers?: list<scalar|null|Param>,
|
||||
* claim_checkers?: list<scalar|null|Param>,
|
||||
* mandatory_claims?: list<scalar|null|Param>,
|
||||
* allowed_algorithms?: list<scalar|null|Param>,
|
||||
* keyset: scalar|null|Param, // The signature keyset. It shall be JWKSet encoded.
|
||||
* },
|
||||
* encryption?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* continue_on_decryption_failure?: bool|Param, // If enable, non-encrypted tokens or tokens that failed during decryption or verification processes are accepted. // Default: false
|
||||
* header_checkers?: list<scalar|null|Param>,
|
||||
* allowed_key_encryption_algorithms?: list<scalar|null|Param>,
|
||||
* allowed_content_encryption_algorithms?: list<scalar|null|Param>,
|
||||
* keyset: scalar|null|Param, // The encryption keyset. It shall be JWKSet encoded.
|
||||
* },
|
||||
* },
|
||||
* blocklist_token?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* cache?: scalar|null|Param, // Storage to track blocked tokens // Default: "cache.app"
|
||||
* },
|
||||
* }
|
||||
* @psalm-type ApiPlatformConfig = array{
|
||||
* title?: scalar|null|Param, // The title of the API. // Default: ""
|
||||
* description?: scalar|null|Param, // The description of the API. // Default: ""
|
||||
@@ -1526,6 +1618,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine?: DoctrineConfig,
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* "when@dev"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1537,6 +1630,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine?: DoctrineConfig,
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* },
|
||||
* "when@prod"?: array{
|
||||
@@ -1549,6 +1643,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine?: DoctrineConfig,
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* },
|
||||
* "when@test"?: array{
|
||||
@@ -1561,6 +1656,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine?: DoctrineConfig,
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* },
|
||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
|
||||
api_login:
|
||||
path: /login_check
|
||||
methods: [POST]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FetchOptions } from 'ofetch'
|
||||
import { $fetch, FetchError } from 'ofetch'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
export type AnyObject = Record<string, unknown>
|
||||
|
||||
@@ -26,6 +27,7 @@ export const useApi = (): ApiClient => {
|
||||
const config = useRuntimeConfig()
|
||||
const baseURL = config.public.apiBase ?? '/api'
|
||||
const toast = useToast()
|
||||
const auth = useAuthStore()
|
||||
const nuxtApp = useNuxtApp()
|
||||
const i18n = nuxtApp.$i18n as
|
||||
| {
|
||||
@@ -70,12 +72,17 @@ export const useApi = (): ApiClient => {
|
||||
const client = $fetch.create({
|
||||
baseURL,
|
||||
retry: 0,
|
||||
onResponse({ options }) {
|
||||
credentials: 'include',
|
||||
onResponse({ options, response }) {
|
||||
const apiOptions = options as ApiFetchOptions<'json'>
|
||||
if (apiOptions?.toast === false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (response?.status && response.status >= 400) {
|
||||
return
|
||||
}
|
||||
|
||||
const successKey = apiOptions?.toastSuccessKey
|
||||
const successMessage =
|
||||
apiOptions?.toastSuccessMessage ||
|
||||
|
||||
@@ -13,11 +13,20 @@
|
||||
"create": "Impossible de créer la réception.",
|
||||
"update": "Impossible de mettre à jour la réception.",
|
||||
"weigh": "Impossible de récupérer la pesée."
|
||||
},
|
||||
"auth": {
|
||||
"login": "Identifiants invalides.",
|
||||
"users": "Impossible de récupérer les utilisateurs.",
|
||||
"logout": "Impossible de se déconnecter."
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
"reception": {
|
||||
"update": "Réception mise à jour avec succès."
|
||||
},
|
||||
"auth": {
|
||||
"login": "Connexion réussie.",
|
||||
"logout": "Déconnexion réussie."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
frontend/layouts/auth.vue
Normal file
7
frontend/layouts/auth.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-primary-500 from-primary-50 via-white to-neutral-100 text-neutral-900">
|
||||
<main class="mx-auto flex min-h-screen w-full max-w-[720px] items-center px-6 py-12">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,7 +9,7 @@
|
||||
LOGO
|
||||
</span>
|
||||
</NuxtLink>
|
||||
<nav class="mx-8 flex gap-8 text-2xl font-bold uppercase text-white">
|
||||
<nav class="mx-8 flex flex-1 gap-8 text-2xl font-bold uppercase text-white">
|
||||
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }">
|
||||
<a
|
||||
:href="href"
|
||||
@@ -29,6 +29,13 @@
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto text-xl font-bold uppercase text-white transition hover:opacity-80"
|
||||
@click="handleLogout"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="mx-auto w-full max-w-[1050px] px-6 pt-[90px] pb-0">
|
||||
@@ -38,6 +45,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const isReceptionActive = computed(() => route.path.startsWith('/reception'))
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await auth.logout()
|
||||
} finally {
|
||||
await navigateTo('/login')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
17
frontend/middleware/auth.global.ts
Normal file
17
frontend/middleware/auth.global.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (to.path === '/login') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!auth.isAuthenticated) {
|
||||
await auth.ensureSession()
|
||||
}
|
||||
|
||||
if (!auth.isAuthenticated) {
|
||||
return navigateTo('/login')
|
||||
}
|
||||
})
|
||||
95
frontend/pages/login.vue
Normal file
95
frontend/pages/login.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="mx-auto w-full max-w-lg">
|
||||
<span
|
||||
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
||||
>
|
||||
LOGO
|
||||
</span>
|
||||
<form
|
||||
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-neutral-700" for="user-select">
|
||||
Utilisateur
|
||||
</label>
|
||||
<select
|
||||
id="user-select"
|
||||
v-model="selectedUsername"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||
:disabled="isLoadingUsers"
|
||||
>
|
||||
<option value="" disabled>Choisir un utilisateur</option>
|
||||
<option v-for="user in users" :key="user.username" :value="user.username">
|
||||
{{ user.username }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-neutral-700" for="password">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Connexion
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
import { getUsers } from '~/services/auth'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
definePageMeta({
|
||||
layout: 'auth'
|
||||
})
|
||||
|
||||
const users = ref<UserData[]>([])
|
||||
const isLoadingUsers = ref(true)
|
||||
const selectedUsername = ref('')
|
||||
const password = ref('')
|
||||
|
||||
const isSubmitting = computed(() => {
|
||||
return auth.isLoading || !selectedUsername.value || !password.value
|
||||
})
|
||||
|
||||
const loadUsers = async () => {
|
||||
isLoadingUsers.value = true
|
||||
try {
|
||||
users.value = await getUsers()
|
||||
} finally {
|
||||
isLoadingUsers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isSubmitting.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await auth.login(selectedUsername.value, password.value)
|
||||
await router.push('/')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadUsers()
|
||||
})
|
||||
</script>
|
||||
38
frontend/services/auth.ts
Normal file
38
frontend/services/auth.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
|
||||
export async function getUsers() {
|
||||
const api = useApi()
|
||||
const data = await api.get<UserData[] | { 'hydra:member': UserData[] }>('users', {}, {
|
||||
toastErrorKey: 'errors.auth.users'
|
||||
})
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
|
||||
return data['hydra:member'] ?? []
|
||||
}
|
||||
|
||||
export async function getCurrentUser() {
|
||||
const api = useApi()
|
||||
return api.get<UserData>('me', {}, {
|
||||
toast: false
|
||||
})
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string) {
|
||||
const api = useApi()
|
||||
return api.post<{ token: string }>('login_check', { username, password }, {
|
||||
toastErrorKey: 'errors.auth.login',
|
||||
toastSuccessKey: 'success.auth.login'
|
||||
})
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
const api = useApi()
|
||||
return api.post<void>('logout', {}, {
|
||||
toastErrorKey: 'errors.auth.logout',
|
||||
toastSuccessKey: 'success.auth.logout',
|
||||
redirect: 'manual'
|
||||
})
|
||||
}
|
||||
3
frontend/services/dto/user-data.ts
Normal file
3
frontend/services/dto/user-data.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface UserData {
|
||||
username: string
|
||||
}
|
||||
58
frontend/stores/auth.ts
Normal file
58
frontend/stores/auth.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
import { getCurrentUser, login, logout } from '~/services/auth'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
user: null as UserData | null,
|
||||
isLoading: false,
|
||||
checked: false
|
||||
}),
|
||||
getters: {
|
||||
isAuthenticated: (state) => Boolean(state.user)
|
||||
},
|
||||
actions: {
|
||||
async ensureSession() {
|
||||
if (this.checked) {
|
||||
return this.user
|
||||
}
|
||||
|
||||
this.checked = true
|
||||
|
||||
try {
|
||||
const me = await getCurrentUser()
|
||||
this.user = me
|
||||
return me
|
||||
} catch {
|
||||
this.user = null
|
||||
return null
|
||||
}
|
||||
},
|
||||
async login(username: string, password: string) {
|
||||
this.isLoading = true
|
||||
|
||||
try {
|
||||
await login(username, password)
|
||||
const me = await getCurrentUser()
|
||||
this.user = me
|
||||
this.checked = true
|
||||
return me
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
async logout() {
|
||||
this.isLoading = true
|
||||
|
||||
try {
|
||||
await logout()
|
||||
} catch {
|
||||
// Ignore logout errors so we can still clear local auth state.
|
||||
} finally {
|
||||
this.user = null
|
||||
this.checked = true
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
9
makefile
9
makefile
@@ -40,13 +40,14 @@ restart: env-init
|
||||
$(DOCKER_COMPOSE) down
|
||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||
|
||||
install: copy-git-hook composer-install cache-clear node-use build-nuxtJS
|
||||
install: copy-git-hook composer-install cache-clear node-use build-nuxtJS migration-migrate
|
||||
|
||||
# Supprime tout est réinstalle tout (Attention ça supprime la bdd aussi)
|
||||
reset: delete_built_dir remove_orphans build-without-cache start wait install
|
||||
|
||||
composer-install:
|
||||
$(EXEC_PHP) composer install
|
||||
$(SYMFONY_CONSOLE) lexik:jwt:generate-keypair --skip-if-exists
|
||||
|
||||
build-nuxtJS:
|
||||
# $(EXEC_PHP) cp -n frontend/.env.dist frontend/.env.local
|
||||
@@ -72,10 +73,16 @@ build-without-cache:
|
||||
--build-arg="CURRENT_GID=$(shell id -g)" \
|
||||
--no-cache
|
||||
|
||||
migration-migrate:
|
||||
$(SYMFONY_CONSOLE) bin/console doctrine:migrations:migrate --no-interaction
|
||||
|
||||
# Attention, supprime votre bdd local
|
||||
db-reset:
|
||||
$(DOCKER_COMPOSE) down -v
|
||||
$(DOCKER_COMPOSE) up -d
|
||||
$(MAKE) wait
|
||||
$(SYMFONY_CONSOLE) doctrine:database:create --if-not-exists
|
||||
$(MAKE) migration-migrate
|
||||
|
||||
# Restart la bdd
|
||||
db-restart:
|
||||
|
||||
27
migrations/Version20260112000700.php
Normal file
27
migrations/Version20260112000700.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260112000700 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create user table for authentication';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE "user" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_USER_USERNAME ON "user" (username)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE "user"');
|
||||
}
|
||||
}
|
||||
30
migrations/Version20260112000800.php
Normal file
30
migrations/Version20260112000800.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260112000800 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Ensure user table exists and align weight type default';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE IF NOT EXISTS "user" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
||||
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS UNIQ_8D93D649F85E0677 ON "user" (username)');
|
||||
$this->addSql('ALTER TABLE weight ALTER type DROP DEFAULT');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE weight ALTER type SET DEFAULT \'gross\'');
|
||||
$this->addSql('DROP INDEX IF EXISTS UNIQ_8D93D649F85E0677');
|
||||
$this->addSql('DROP TABLE IF EXISTS "user"');
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
provider: ReceptionReceiptProvider::class,
|
||||
),
|
||||
],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
)]
|
||||
class Reception
|
||||
{
|
||||
|
||||
104
src/Entity/User.php
Normal file
104
src/Entity/User.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\State\MeProvider;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'user', schema: 'public')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/me',
|
||||
normalizationContext: ['groups' => ['user:read']],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
provider: MeProvider::class
|
||||
),
|
||||
new GetCollection(
|
||||
normalizationContext: ['groups' => ['user:read']],
|
||||
security: "is_granted('PUBLIC_ACCESS')"
|
||||
),
|
||||
],
|
||||
normalizationContext: ['groups' => ['user:read']],
|
||||
paginationEnabled: false
|
||||
)]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180, unique: true)]
|
||||
#[Groups(['user:read'])]
|
||||
private string $username = '';
|
||||
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column]
|
||||
private string $password = '';
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function setUsername(string $username): self
|
||||
{
|
||||
$this->username = $username;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
$roles[] = 'ROLE_USER';
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
public function setRoles(array $roles): self
|
||||
{
|
||||
$this->roles = $roles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): self
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// No-op: we don't store temporary sensitive data on the entity.
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
denormalizationContext: ['groups' => ['weight:write']],
|
||||
),
|
||||
],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
)]
|
||||
#[UniqueEntity(fields: ['reception', 'type'], message: 'A weighing already exists for this type.')]
|
||||
class Weight
|
||||
|
||||
27
src/State/MeProvider.php
Normal file
27
src/State/MeProvider.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Entity\User;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
|
||||
|
||||
final readonly class MeProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(private Security $security) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedException('User not authenticated.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
12
symfony.lock
12
symfony.lock
@@ -61,6 +61,18 @@
|
||||
".php-cs-fixer.dist.php"
|
||||
]
|
||||
},
|
||||
"lexik/jwt-authentication-bundle": {
|
||||
"version": "3.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.5",
|
||||
"ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/lexik_jwt_authentication.yaml"
|
||||
]
|
||||
},
|
||||
"nelmio/cors-bundle": {
|
||||
"version": "2.6",
|
||||
"recipe": {
|
||||
|
||||
Reference in New Issue
Block a user