Spring Boot,Spring security,React
- Présentation du projet : développer une application e-commerce où les clients peuvent consulter et commander des produits, tandis que les administrateurs peuvent gérer les produits, les utilisateurs et les commandes.
- Objectifs du projet :
- Créer une API RESTful avec Spring Boot pour gérer les données de l'application.
- Mettre en place une authentification et une gestion des rôles avec Spring Security.
- Développer une interface utilisateur avec ReactJS pour interagir avec l'API.
- Description du modèle de données :
Utilisateur
: Stocke les informations des utilisateurs (clients et administrateurs).Produit
: Stocke les informations des produits disponibles.Commande
: Stocke les commandes passées par les clients.LigneCommande
: Stocke les détails des produits dans chaque commande.
ecommerce-backend/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/ecommerce/
│ │ │ ├── EcommerceApplication.java
│ │ │ ├── config/ # Configuration de sécurité et gestion des erreurs
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ ├── JwtUtil.java
│ │ │ │ ├── JwtFilter.java
│ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ ├── entity/ # Modèles de données (JPA Entities)
│ │ │ │ ├── Utilisateur.java
│ │ │ │ ├── Produit.java
│ │ │ │ ├── Commande.java
│ │ │ │ ├── LigneCommande.java
│ │ │ ├── repository/ # Couche d'accès aux données (DAO)
│ │ │ │ ├── UtilisateurRepository.java
│ │ │ │ ├── ProduitRepository.java
│ │ │ │ ├── CommandeRepository.java
│ │ │ │ ├── LigneCommandeRepository.java
│ │ │ ├── service/ # Logique métier
│ │ │ │ ├── UtilisateurService.java
│ │ │ │ ├── ProduitService.java
│ │ │ │ ├── CommandeService.java
│ │ │ │ ├── CustomUserDetailsService.java
│ │ │ ├── controller/ # API REST
│ │ │ │ ├── AuthController.java
│ │ │ │ ├── UtilisateurController.java
│ │ │ │ ├── ProduitController.java
│ │ │ │ ├── CommandeController.java
│ │ ├── resources/
│ │ │ ├── application.properties
│ │ │ └── static/
│ ├── test/
│ │ ├── java/
│ │ │ └── com/example/ecommerce/
│ │ │ └── EcommerceApplicationTests.java
├── pom.xml
└── README.md
ecommerce-frontend/
├── public/ # Fichiers publics accessibles par le navigateur
│ ├── index.html
│ ├── favicon.ico
│ └── manifest.json
├── src/
│ ├── components/ # Composants réutilisables
│ │ ├── Navbar.js
│ │ ├── Sidebar.js
│ │ ├── PrivateRoute.js
│ │ ├── AdminRoute.js
│ ├── pages/ # Pages principales de l'application
│ │ ├── auth/ # Pages liées à l'authentification
│ │ │ ├── Login.js
│ │ │ ├── Register.js
│ │ ├── admin/ # Pages d'administration
│ │ │ ├── AdminProducts.js
│ │ │ ├── AdminUsers.js
│ │ ├── user/ # Pages accessibles aux utilisateurs connectés
│ │ │ ├── Cart.js
│ │ │ ├── Orders.js
│ │ ├── products/ # Pages liées aux produits
│ │ │ ├── ProductList.js
│ ├── App.js # Composant principal
│ └── index.js # Point d'entrée de l'application
├── package.json
└── README.md
classDiagram
class EcommerceApplication {
+main(args: String[])
}
class Utilisateur {
-int idUtilisateur
-String nom
-String email
-String password
-Role role
}
class Produit {
-int idProduit
-String nom
-int quantiteStock
-BigDecimal prix
}
class Commande {
-int idCommande
-LocalDateTime dateCommande
-Utilisateur utilisateur
-List~LigneCommande~ lignesCommande
}
class LigneCommande {
-int idLigneCommande
-Commande commande
-Produit produit
-int quantite
-BigDecimal prixUnitaire
}
Utilisateur o--> Commande : "possède"
Commande o--> LigneCommande : "contient"
Produit o--> LigneCommande : "lié à"
class AuthController {
+login(request: LoginRequest)
+register(utilisateur: Utilisateur)
}
class ProduitController {
+getAllProduits()
+addProduit(produit: Produit)
+updateProduit(id: int, produit: Produit)
+deleteProduit(id: int)
+searchProduits(nom: String)
}
class CommandeController {
+createCommande()
+getUserCommandes()
+addProduitToCommande(idCommande: int, idProduit: int, quantite: int)
+deleteLigneCommande(idLigneCommande: int)
}
class UtilisateurController {
+getAllUtilisateurs()
+deleteUtilisateur(id: int)
}
Étape 1 : Configuration Initiale
Création du projet via Spring Initializr :
- Allez sur Spring Initializr.
- Sélectionnez les options suivantes :
- Project: Maven Project
- Language: Java
- Spring Boot: Version la plus récente
- Project Metadata:
- Group: com.example
- Artifact: ecommerce
- Name: ecommerce
- Description: E-commerce Backend
- Packaging: Jar
- Java: Version la plus récente
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver, Spring Security, Spring Boot DevTools, Lombok
- Cliquez sur "Generate" pour télécharger le projet compressé.
- Décompressez le fichier et ouvrez-le dans votre IDE .
Backend : pom.xml
- Nom du fichier :
pom.xml
- Rôle : Configure Maven pour le projet backend.
- Métadonnées du projet :
- Groupe :
com.example
- Artefact :
ecommerce
- Version :
0.0.1-SNAPSHOT
- Nom :
ecommerce
- Description : Projet de boutique en ligne
- Groupe :
- Parent : Hérite de
spring-boot-starter-parent
(version 3.2.3) pour la gestion des dépendances. - Dépendances principales :
spring-boot-starter-web
→ API RESTspring-boot-starter-data-jpa
→ JPA/Hibernatespring-boot-starter-security
→ Sécurité avec JWTmysql-connector-java
→ Connexion à MySQLjjwt
→ Gestion des tokens JWTlombok
→ Réduction du code répétitif
- Plugin Maven :
spring-boot-maven-plugin
→ Permet de lancer l’application via Maven.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>ecommerce</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ecommerce</name>
<description>E-commerce application</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>3.2.2</version> <!-- Remplacez par la version de votre Spring Boot -->
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source> <!-- Adapter selon votre version de Java -->
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
Backend : EcommerceApplication.java
package com.example.ecommerce;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EcommerceApplication {
public static void main(String[] args) {
SpringApplication.run(EcommerceApplication.class, args);
}
}
- Nom du fichier :
EcommerceApplication.java
- Rôle : Point d’entrée de l’application Spring Boot.
- Annotation principale :
@SpringBootApplication
, qui combine :@Configuration
→ Définit la configuration de l’application.@EnableAutoConfiguration
→ Active l’auto-configuration Spring.@ComponentScan
→ Scanne les composants du packagecom.example.ecommerce
.
- Méthode principale :
main()
→ UtiliseSpringApplication.run
pour démarrer l’application.- Démarre un serveur web intégré (Tomcat).
Backend : application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/ecommerce_db?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=votre_mot_de_passe
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
- Nom du fichier :
application.properties
- Rôle : Configure les paramètres de l’application.
- Paramètres principaux :
spring.datasource.url
→ Définit l’URL de connexion à MySQL et crée la baseecommerce_db
si elle n’existe pas.spring.datasource.username
→ Spécifie l’identifiant MySQL (à personnaliser).spring.datasource.password
→ Spécifie le mot de passe MySQL (à personnaliser).spring.jpa.hibernate.ddl-auto=update
→ Met à jour automatiquement le schéma de la base en fonction des entités Java.spring.jpa.show-sql=true
→ Affiche les requêtes SQL dans la console pour le débogage.
Frontend : Créer un projet
-
Téléchargez et installez Node.js depuis https://nodejs.org.
-
Redémarrez votre terminal.
-
Vérifiez avec
node -v
etnpm -v
pour confirmer l’installation.
npx create-react-app ecommerce-frontend
Frontend : package.json
{
....
"dependencies": {
...
"axios": "^1.6.8",
"react-router-dom": "^6.22.3",
....
},
....
}
Installez les dépendances :
npm install
- Nom du fichier :
package.json
- Rôle : Configure le projet React.
- Informations générales :
name
→ecommerce-frontend
version
→0.1.0
- Dépendances principales :
axios
→ Gestion des requêtes HTTP.react
etreact-dom
→ Base du projet React.react-router-dom
→ Gestion de la navigation entre les pages.- AdminLTE.IO : Pas inclus dans les dépendances, car utilisé via CDN ou fichiers locaux.
- Scripts disponibles :
start
→ Démarre l’application en mode développement.build
→ Compile l’application pour la production.test
→ Exécute les tests.eject
→ Extrait la configuration dereact-scripts
.
- DevDependencies :
react-scripts
→ Gère la compilation et l’exécution.
Frontend : public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-commerce</title>
<!-- AdminLTE CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
<!-- FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<!-- Bootstrap CSS (requis par AdminLTE) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
</head>
<body class="hold-transition sidebar-mini">
<div id="root"></div>
<!-- jQuery (requis par AdminLTE) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap JS (requis par AdminLTE) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE JS -->
<script src="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/js/adminlte.min.js"></script>
</body>
</html>
- Nom du fichier :
public/index.html
- Rôle : Template HTML de base pour React.
- Éléments principaux :
<div id="root">
→ Conteneur où React rend l’application.
- Liens externes inclus :
- AdminLTE.IO : Fichiers CSS et JS via CDN.
- jQuery : Requis pour AdminLTE.
- Bootstrap : Nécessaire pour le bon fonctionnement d’AdminLTE.
- FontAwesome : Fournit des icônes.
- Organisation des scripts :
- Les fichiers JS sont placés à la fin du
<body>
pour garantir que le DOM est chargé avant leur exécution.
- Les fichiers JS sont placés à la fin du
Frontend : index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(
<App /<
);
- Nom du fichier :
index.js
- Rôle : Point d’entrée de l’application React.
- Importations principales :
- React et ReactDOM : Utilisés pour créer et rendre les composants.
- Composant App : Importé depuis
App.js
.
- Rendu de l’application :
- Le composant
App
est rendu dans l’élément#root
avecReactDOM.render
.
- Le composant
- Utilisation de
<React.StrictMode>
:- Active des vérifications supplémentaires pour détecter les problèmes potentiels dans le code.
Étape 2 : Authentification et Inscription
Backend : Entité Utilisateur.java
package com.example.ecommerce.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.*;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Utilisateur {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int idUtilisateur;
@NotNull
private String nom;
@NotNull
@Column(unique = true)
private String email;
@NotNull
private String password;
@NotNull
@Enumerated(EnumType.STRING)
private Role role;
public enum Role {
client, admin
}
public int getIdUtilisateur() {
return idUtilisateur;
}
public void setIdUtilisateur(int idUtilisateur) {
this.idUtilisateur = idUtilisateur;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
}
- Nom du fichier :
Utilisateur.java
- Rôle : Définit l’entité
Utilisateur
, mappée à la tableUtilisateur
dans la base de données. - Annotations principales :
@Entity
: Indique que cette classe est une entité JPA.@Data
(Lombok) : Génère automatiquement les getters, setters, et méthodestoString
,equals
, ethashCode
.@NoArgsConstructor
et@AllArgsConstructor
(Lombok) : Créent un constructeur sans arguments et un constructeur avec tous les arguments.
- Clé primaire :
idUtilisateur
: Défini comme clé primaire avec@Id
.- Auto-incrémentation activée via
@GeneratedValue
.
- Champs principaux :
nom
,email
,password
,role
: Champs obligatoires annotés avec@NotNull
.email
: Défini comme unique avec@Column(unique = true)
.
- Gestion du rôle :
role
: Défini comme une énumération (Role
).- Stocké sous forme de chaîne grâce à
@Enumerated(EnumType.STRING)
. - Peut avoir les valeurs :
client
etadmin
.
Backend : Repository UtilisateurRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.Utilisateur;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UtilisateurRepository extends JpaRepository<Utilisateur, Integer> {
Optional<Utilisateur> findByEmail(String email);
}
- Nom du fichier :
UtilisateurRepository.java
- Rôle : Interface de repository JPA pour la gestion des utilisateurs.
- Héritage :
- Étend
JpaRepository<Utilisateur, Integer>
. - Fournit automatiquement des méthodes CRUD (Create, Read, Update, Delete).
- Étend
- Méthodes personnalisées :
findByEmail(String email)
: Recherche un utilisateur par son email.- Retourne un
Optional<Utilisateur>
pour gérer les cas où aucun utilisateur n’est trouvé.
Backend : Service UtilisateurService.java
package com.example.ecommerce.service;
import com.example.ecommerce.entity.Utilisateur;
import com.example.ecommerce.repository.UtilisateurRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UtilisateurService {
@Autowired
private UtilisateurRepository utilisateurRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public Utilisateur save(Utilisateur utilisateur) {
utilisateur.setPassword(passwordEncoder.encode(utilisateur.getPassword()));
return utilisateurRepository.save(utilisateur);
}
public Utilisateur findByEmail(String email) {
return utilisateurRepository.findByEmail(email).orElse(null);
}
public List<Utilisateur;> findAll() {
return utilisateurRepository.findAll();
}
public void delete(int id) {
utilisateurRepository.deleteById(id);
}
}
- Nom du fichier :
UtilisateurService.java
- Rôle : Service Spring pour la gestion des utilisateurs.
- Annotations :
@Service
: Indique que cette classe est un service géré par Spring.
- Injections de dépendances :
UtilisateurRepository
: Accès aux opérations CRUD sur les utilisateurs.PasswordEncoder
: Hachage des mots de passe pour la sécurité.
- Méthodes principales :
save(Utilisateur utilisateur)
: Hache le mot de passe et sauvegarde l’utilisateur.findByEmail(String email)
: Recherche un utilisateur par email, retournenull
si non trouvé.findAll()
: Retourne la liste de tous les utilisateurs.delete(Integer id)
: Supprime un utilisateur par son ID.
Backend : Service CustomUserDetailsService.java
package com.example.ecommerce.service;
import com.example.ecommerce.entity.Utilisateur;
import com.example.ecommerce.repository.UtilisateurRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UtilisateurRepository utilisateurRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Utilisateur utilisateur = utilisateurRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé"));
return User.withUsername(utilisateur.getEmail())
.password(utilisateur.getPassword())
.roles(utilisateur.getRole().name())
.build();
}
}
- Nom du fichier :
CustomUserDetailsService.java
- Rôle : Implémentation de
UserDetailsService
pour Spring Security. - Annotations :
@Service
: Indique que cette classe est un service géré par Spring.
- Injection de dépendance :
UtilisateurRepository
: Permet d’accéder aux utilisateurs stockés en base de données.
- Méthode principale :
-
loadUserByUsername(String email)
:- Recherche un utilisateur par son email avec
findByEmail
. - Lance une
UsernameNotFoundException
si l’utilisateur n’est pas trouvé. - Construit un objet
User
de Spring Security avec : - L’email comme nom d’utilisateur.
- Le mot de passe haché.
- Le rôle de l’utilisateur.
- Recherche un utilisateur par son email avec
-
Backend : JwtUtil.java
package com.example.ecommerce.config;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
private String secret = "votre_secret_key_avec_au_moins_32_caracteres_pour_HS256";
public String generateToken(String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", role); // Ajouter le rôle au token
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10h
.signWith(SignatureAlgorithm.HS256, secret.getBytes())
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(secret.getBytes())
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public String extractRole(String token) {
return Jwts.parser()
.setSigningKey(secret.getBytes())
.parseClaimsJws(token)
.getBody()
.get("roles", String.class); // Extraire le rôle du token
}
public boolean validateToken(String token, String username) {
return (username.equals(extractUsername(token)) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return Jwts.parser()
.setSigningKey(secret.getBytes())
.parseClaimsJws(token)
.getBody()
.getExpiration()
.before(new Date());
}
}
- Nom du fichier :
JwtUtil.java
- Rôle : Classe utilitaire pour la gestion des tokens JWT.
- Annotations :
@Component
: Indique que cette classe est un composant Spring injectable.
- Clé secrète : Stockée sous forme de variable
secret
pour signer les tokens. - Principales méthodes :
-
generateToken(String email)
:- Crée un token JWT.
- Utilise l’email comme sujet.
- Ajoute une date d’émission et une expiration après 10 heures.
- Utilise l’algorithme de signature
HS256
.
-
extractUsername(String token)
:- Extrait l’email contenu dans le token JWT.
-
validateToken(String token, UserDetails userDetails)
:- Vérifie si le token est valide.
- Compare l’email extrait avec celui de
userDetails
. - Vérifie si le token est expiré avec
isTokenExpired
.
-
Backend : JwtFilter.java
package com.example.ecommerce.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
jwt = authHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (Exception e) {
System.out.println("Erreur lors de l'extraction du token : " + e.getMessage());
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtUtil.validateToken(jwt, username)) {
String role = jwtUtil.extractRole(jwt); // Extraire le rôle du token
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
Collections.singletonList(new SimpleGrantedAuthority(role))
);
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
- Nom du fichier :
JwtFilter.java
- Rôle : Filtre de vérification des tokens JWT dans les requêtes HTTP.
- Héritage : Étend
OncePerRequestFilter
pour s'exécuter une seule fois par requête. - Annotations :
@Component
: Rend le filtre injectable par Spring.
- Injections :
JwtUtil
: Gestion des tokens JWT.CustomUserDetailsService
: Récupération des détails de l'utilisateur.
- Principale méthode :
doFilterInternal
- Récupère l’en-tête
Authorization
de la requête. - Vérifie la présence et le préfixe
"Bearer "
. - Extrait et valide le token JWT.
- Charge les informations de l’utilisateur via
CustomUserDetailsService
. - Met à jour le contexte de sécurité avec
SecurityContextHolder
si le token est valide. - Passe au filtre suivant avec
chain.doFilter
.
- Récupère l’en-tête
Backend : SecurityConfig.java
package com.example.ecommerce.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtFilter jwtFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("admin")
.anyRequest().authenticated()
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
- Nom du fichier :
SecurityConfig.java
- Rôle : Configuration de la sécurité Spring Security avec JWT.
- Annotations :
@Configuration
: Indique une classe de configuration Spring.@EnableWebSecurity
: Active la sécurité web de Spring.
- Injections :
JwtFilter
: Filtre JWT pour valider les tokens.
- Principale méthode :
securityFilterChain
- Désactive CSRF (application stateless avec JWT).
- Autorise les requêtes vers
/api/auth/**
sans authentification. - Restreint l’accès à
/api/admin/**
aux administrateurs uniquement. - Exige une authentification pour toutes les autres routes.
- Utilise une gestion stateless des sessions.
- Ajoute
JwtFilter
dans la chaîne de filtres.
- Autres composants :
passwordEncoder
: UtiliseBCryptPasswordEncoder
pour sécuriser les mots de passe.authenticationManager
: Fournit un gestionnaire d’authentification.
Backend : AuthController.java
package com.example.ecommerce.controller;
import com.example.ecommerce.config.JwtUtil;
import com.example.ecommerce.entity.Utilisateur;
import com.example.ecommerce.service.CustomUserDetailsService;
import com.example.ecommerce.service.UtilisateurService;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private UtilisateurService utilisateurService;
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
);
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getEmail());
String role = userDetails.getAuthorities().stream()
.map(grantedAuthority -> grantedAuthority.getAuthority())
.findFirst()
.orElse("ROLE_client");
String token = jwtUtil.generateToken(userDetails.getUsername(), role);
Map<String, String> response = new HashMap<>();
response.put("token", token);
response.put("role", role);
return ResponseEntity.ok(response);
}
@PostMapping("/register")
public ResponseEntity<Utilisateur> register(@RequestBody Utilisateur utilisateur) {
return ResponseEntity.ok(utilisateurService.save(utilisateur));
}
}
@Data
class LoginRequest {
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Tester Register
Tester Login
- Nom du fichier :
AuthController.java
- Rôle : Gestion de l’authentification des utilisateurs.
- Annotations :
@RestController
: Indique un contrôleur REST Spring Boot.@RequestMapping("/api/auth")
: Définit la route de base.
- Injections :
AuthenticationManager
: Gère l’authentification.JwtUtil
: Génère et valide les tokens JWT.CustomUserDetailsService
: Charge les utilisateurs.UtilisateurService
: Gère la persistance des utilisateurs.
- Endpoints :
POST /api/auth/login
:- Authentifie l’utilisateur avec
authenticationManager
. - Génère un token avec
jwtUtil
. - Renvoie le token en réponse.
- Authentifie l’utilisateur avec
POST /api/auth/register
:- Sauvegarde un nouvel utilisateur avec
utilisateurService.save
.
- Sauvegarde un nouvel utilisateur avec
- DTO interne :
LoginRequest
- Contient les champs
email
etpassword
. - Utilisé pour transmettre les données de connexion.
- Contient les champs
Frontend : App.js
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Navbar from './components/Navbar';
import Sidebar from './components/Sidebar';
import Login from './pages/Login';
import Register from './pages/Register';
import ProductList from './pages/ProductList';
import AdminProducts from './pages/AdminProducts';
import Cart from './pages/Cart';
import Orders from './pages/Orders';
import AdminUsers from './pages/AdminUsers';
import PrivateRoute from './components/PrivateRoute';
import AdminRoute from './components/AdminRoute';
function App() {
return (
<Router>
<div className="wrapper">
<Navbar />
<Sidebar />
<div className="content-wrapper">
<div className="content">
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route element={<PrivateRoute />}>
<Route path="/produits" element={<ProductList />} />
<Route path="/panier" element={<Cart />} />
<Route path="/commandes" element={<Orders />} />
</Route>
<Route element={<AdminRoute />}>
<Route path="/admin/produits" element={<AdminProducts />} />
<Route path="/admin/utilisateurs" element={<AdminUsers />} />
</Route>
</Routes>
</div>
</div>
</div>
</Router>
);
}
export default App;
- Nom du fichier :
App.js
- Rôle : Composant principal de l’application React.
- Bibliothèques utilisées :
react-router-dom
: Gestion des routes.AdminLTE
: Structure et styles de l’interface.
- Imports :
BrowserRouter
,Routes
,Route
dereact-router-dom
.PrivateRoute
etAdminRoute
(à créer).- Composants UI :
Navbar
,Sidebar
,Login
,Register
, etc.
- Structure du rendu :
- Utilisation de
AdminLTE
pour la mise en page. <aside>
pour inclureNavbar
etSidebar
.<main>
pour afficher les routes.
- Utilisation de
- Routes définies :
/login
et/register
: Accès public.- Autres routes protégées par
PrivateRoute
ouAdminRoute
.
Frontend : components/Navbar.js
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
function Navbar() {
const navigate = useNavigate();
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('cart');
navigate('/login');
};
return (
<nav className="main-header navbar navbar-expand navbar-white navbar-light">
<ul className="navbar-nav">
<li className="nav-item">
<Link className="nav-link" to="/" data-widget="pushmenu">
<i className="fas fa-bars"></i>
</Link>
</li>
</ul>
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<button className="nav-link btn btn-link" onClick={handleLogout}>
<i className="fas fa-sign-out-alt"></i> Déconnexion
</button>
</li>
</ul>
</nav>
);
}
export default Navbar;
- Nom du fichier :
Navbar.js
- Rôle : Définit la barre de navigation AdminLTE.
- Bibliothèques utilisées :
react-router-dom
: Utilisation deuseNavigate
pour la redirection.
- Fonctionnalités principales :
- Affichage du logo et de la barre de navigation.
- Bouton de déconnexion.
- Fonction
handleLogout
:- Supprime le token et le panier de
localStorage
. - Redirige vers
/login
après la déconnexion.
- Supprime le token et le panier de
- Structure du rendu :
- Utilisation de la classe
main-header
d’AdminLTE. - Inclut un logo et un bouton de déconnexion.
- Utilisation de la classe
Frontend : components/Sidebar.js
import React from 'react';
import { Link } from 'react-router-dom';
function Sidebar() {
return (
<aside className="main-sidebar sidebar-dark-primary elevation-4">
<Link to="/" className="brand-link">
<span className="brand-text font-weight-light">E-commerce</span>
</Link>
<div className="sidebar">
<nav className="mt-2">
<ul className="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu">
<li className="nav-item">
<Link to="/produits" className="nav-link">
<i className="nav-icon fas fa-shopping-bag"></i>
<p>Produits</p>
</Link>
</li>
<li className="nav-item">
<Link to="/panier" className="nav-link">
<i className="nav-icon fas fa-cart-plus"></i>
<p>Panier</p>
</Link>
</li>
<li className="nav-item">
<Link to="/commandes" className="nav-link">
<i className="nav-icon fas fa-list"></i>
<p>Mes Commandes</p>
</Link>
</li>
<li className="nav-item">
<Link to="/admin/produits" className="nav-link">
<i className="nav-icon fas fa-cogs"></i>
<p>Admin Produits</p>
</Link>
</li>
<li className="nav-item">
<Link to="/admin/utilisateurs" className="nav-link">
<i className="nav-icon fas fa-users"></i>
<p>Admin Utilisateurs</p>
</Link>
</li>
</ul>
</nav>
</div>
</aside>
);
}
export default Sidebar;
- Nom du fichier :
Sidebar.js
- Rôle : Définit la barre latérale AdminLTE.
- Bibliothèques utilisées :
react-router-dom
: Gestion de la navigation avecLink
.FontAwesome
: Ajout d'icônes pour un design professionnel.
- Fonctionnalités principales :
- Affichage des liens de navigation vers les différentes pages.
- Utilisation d’icônes pour améliorer l’interface utilisateur.
- Les liens sont affichés pour tous les utilisateurs (la gestion des permissions est déléguée aux routes).
- Structure du rendu :
- Utilisation de la classe
main-sidebar
d’AdminLTE. - Liste de liens avec des icônes FontAwesome.
- Utilisation de la classe
Frontend : pages/Login.js
import React, { useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post('http://localhost:8080/api/auth/login', { email, password });
localStorage.removeItem('cart');
// Vérifier si la réponse est un objet (nouveau format) ou une chaîne (ancien format)
const token = typeof response.data === 'string' ? response.data : response.data.token;
const role = typeof response.data === 'object' && response.data.role ? response.data.role : null;
console.log(response.data);
localStorage.setItem('token', token);
if (role) {
localStorage.setItem('role', role); // Stocker le rôle si présent
}
navigate('/produits');
} catch (error) {
setError(error.response?.data || 'Erreur de connexion');
}
};
return (
<div className="container mt-5">
<div className="row justify-content-center">
<div className="col-md-6">
<div className="card">
<div className="card-header">Connexion</div>
<div className="card-body">
{error && <div className="alert alert-danger">{typeof error === 'string' ? error : 'Erreur inconnue'}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Email</label>
<input type="email" className="form-control" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div className="form-group">
<label>Mot de passe</label>
<input type="password" className="form-control" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary">Se connecter</button>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
export default Login;
- Nom du fichier :
Login.js
- Rôle : Affiche le formulaire de connexion avec AdminLTE.
- Bibliothèques utilisées :
useState
: Gestion des états (email
,password
,error
).useNavigate
: Redirection après connexion réussie.axios
: Envoi de la requête POST vers/api/auth/login
.
- Fonctionnalités principales :
- Affichage du formulaire de connexion avec AdminLTE.
- Gestion des erreurs et mise à jour de
error
en cas d’échec. - Stockage du token après connexion réussie.
- Vidage du panier utilisateur.
- Redirection vers
/produits
après connexion.
- Structure du rendu :
- Utilisation d’une
card
AdminLTE pour un design épuré. - Champs de saisie pour l’email et le mot de passe.
- Bouton de connexion avec gestion de soumission.
- Utilisation d’une
Frontend : pages/Register.js
import React, { useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
function Register() {
const [nom, setNom] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [role, setRole] = useState('client');
const [error, setError] = useState(null);
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
await axios.post('http://localhost:8080/api/auth/register', { nom, email, password, role });
navigate('/login');
} catch (error) {
setError(error.response?.data || 'Erreur lors de l’inscription');
}
};
return (
<div className="container mt-5">
<div className="row justify-content-center">
<div className="col-md-6">
<div className="card">
<div className="card-header">Inscription</div>
<div className="card-body">
{error && <div className="alert alert-danger">{typeof error === 'string' ? error : 'Erreur inconnue'}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Nom</label>
<input type="text" className="form-control" value={nom} onChange={(e) => setNom(e.target.value)} />
</div>
<div className="form-group">
<label>Email</label>
<input type="email" className="form-control" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div className="form-group">
<label>Mot de passe</label>
<input type="password" className="form-control" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<div className="form-group">
<label>Rôle</label>
<select className="form-control" value={role} onChange={(e) => setRole(e.target.value)}>
<option value="client">Client</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit" className="btn btn-primary">S’inscrire</button>
</form>
</div>
</div>
</div>
</div>
</div>
);
}
export default Register;
- Nom du fichier :
Register.js
- Rôle : Affiche un formulaire d’inscription avec AdminLTE.
- Bibliothèques utilisées :
useState
: Gestion des états (nom
,email
,password
,role
,error
).useNavigate
: Redirection après une inscription réussie.axios
: Envoi de la requête POST vers/api/auth/register
.
- Fonctionnalités principales :
- Affichage du formulaire d’inscription avec AdminLTE.
- Validation et gestion des erreurs via
error
. - Enregistrement des utilisateurs avec un rôle sélectionnable.
- Redirection vers
/login
après inscription réussie.
- Structure du rendu :
- Utilisation d’une
card
AdminLTE pour un design clair et intuitif. - Champs de saisie pour le nom, l’email et le mot de passe.
- Liste déroulante pour sélectionner le rôle.
- Bouton d’inscription avec gestion de soumission.
- Utilisation d’une
Frontend : components/PrivateRoute.js
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
const PrivateRoute = () => {
const isAuthenticated = !!localStorage.getItem('token');
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />;
};
export default PrivateRoute;
- Nom du fichier :
PrivateRoute.js
- Rôle : Protéger les routes nécessitant une authentification.
- Bibliothèques utilisées :
useNavigate
: Redirection si l’utilisateur n’est pas authentifié.Outlet
dereact-router-dom
: Rend le contenu des routes protégées.
- Fonctionnalités principales :
isAuthenticated
vérifie la présence d’un token danslocalStorage
.- Si authentifié, affiche le contenu avec
Outlet
. - Sinon, redirige vers
/login
viauseNavigate
.
- Structure du rendu :
- Condition vérifiant si un token existe.
- Redirection vers
/login
en cas d’absence. - Affichage du contenu protégé si authentifié.
Frontend : components/AdminRoute.js
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
const AdminRoute = () => {
const token = localStorage.getItem('token');
if (!token) return <Navigate to="/login" />;
const payload = JSON.parse(atob(token.split('.')[1]));
const isAdmin = payload.roles && payload.roles.includes('admin');
return isAdmin ? <Outlet /> : <Navigate to="/login" />;
};
export default AdminRoute;
- Nom du fichier :
AdminRoute.js
- Rôle : Protéger les routes réservées aux administrateurs.
- Bibliothèques utilisées :
useNavigate
: Redirection si l’utilisateur n’a pas les droits admin.Outlet
dereact-router-dom
: Rend le contenu des routes protégées.jwt-decode
: Décoder le token JWT pour vérifier le rôle.
- Fonctionnalités principales :
- Récupère et décode le token JWT depuis
localStorage
. - Vérifie si le rôle dans le token est
admin
. - Si non admin ou token absent, redirige vers
/login
. - Sinon, rend le contenu protégé avec
Outlet
.
- Récupère et décode le token JWT depuis
- Structure du rendu :
- Lecture du token JWT.
- Décodage et extraction du rôle.
- Condition vérifiant l’accès admin.
- Redirection si l’accès est refusé.
Tester Register
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Navbar from './components/Navbar';
import Sidebar from './components/Sidebar';
import Login from './pages/Login';
import Register from './pages/Register';
// import ProductList from './pages/ProductList';
// import AdminProducts from './pages/AdminProducts';
// import Cart from './pages/Cart';
// import Orders from './pages/Orders';
// import AdminUsers from './pages/AdminUsers';
// import PrivateRoute from './components/PrivateRoute';
// import AdminRoute from './components/AdminRoute';
function App() {
return (
<Router>
<div className="wrapper">
<Navbar />
<Sidebar />
<div className="content-wrapper">
<div className="content">
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
{/*
<Route element={<PrivateRoute />}>
<Route path="/produits" element={<ProductList />} />
<Route path="/panier" element={<Cart />} />
<Route path="/commandes" element={<Orders />} />
</Route>
<Route element={<AdminRoute />}>
<Route path="/admin/produits" element={<AdminProducts />} />
<Route path="/admin/utilisateurs" element={<AdminUsers />} />
</Route>
*/}
</Routes>
</div>
</div>
</div>
</Router>
);
}
export default App;
Tester login
Token in localStorage
Étape 3 : Gestion des Produits (Consultation et Recherche)
Backend : Entité Produit.java
package com.example.ecommerce.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import java.math.BigDecimal;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Produit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int idProduit;
@NotNull
private String nom;
@NotNull
private int quantiteStock;
@NotNull
private BigDecimal prix;
}
- Nom du fichier :
Produit.java
- Rôle : Définir l’entité Produit et son mapping avec la base de données.
- Annotations utilisées :
@Entity
: Indique que cette classe est une entité JPA.@Data
(Lombok) : Génère automatiquement les getters, setters,toString()
, etc.@NoArgsConstructor
et@AllArgsConstructor
: Génèrent les constructeurs.@Id
et@GeneratedValue
: Définissent la clé primaire auto-incrémentée.@NotNull
: Rend certains champs obligatoires.
- Attributs principaux :
idProduit
(Integer) : Clé primaire auto-générée.nom
(String) : Nom du produit, obligatoire.quantiteStock
(Integer) : Quantité disponible en stock.prix
(BigDecimal) : Prix du produit (précision monétaire).
- Structure du rendu :
- Annotations JPA et Lombok.
- Déclaration des attributs avec validations.
- Constructeurs et méthodes générées automatiquement.
Backend : Repository ProduitRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.Produit;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ProduitRepository extends JpaRepository<Produit, Integer> {
List<Produit> findByNomContainingIgnoreCase(String nom);
}
- Nom du fichier :
ProduitRepository.java
- Rôle : Interface de repository JPA pour gérer les opérations CRUD sur l'entité Produit.
- Annotations utilisées :
@Repository
(optionnelle) : Indique que cette interface est un composant Spring gérant la persistance.
- Héritage :
JpaRepository<Produit, Integer>
- Fournit automatiquement les méthodes CRUD.
- Utilise
Integer
comme type d’ID de Produit.
- Méthodes spécifiques :
findByNomContainingIgnoreCase(String nom)
:- Recherche les produits dont le nom contient une sous-chaîne donnée.
- Ignore la casse pour une meilleure flexibilité.
- Utile pour les fonctionnalités de recherche dynamique.
- Structure du rendu :
- Déclaration d’une interface qui hérite de
JpaRepository
. - Ajout de la méthode de recherche spécifique.
- Spring Data JPA génère automatiquement l’implémentation.
- Déclaration d’une interface qui hérite de
Backend : Service ProduitService.java
package com.example.ecommerce.service;
import com.example.ecommerce.entity.Produit;
import com.example.ecommerce.repository.ProduitRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProduitService {
@Autowired
private ProduitRepository produitRepository;
public List<Produit> findAll() {
return produitRepository.findAll();
}
public Produit save(Produit produit) {
return produitRepository.save(produit);
}
public Produit findById(int id) {
return produitRepository.findById(id).orElse(null);
}
public void delete(int id) {
produitRepository.deleteById(id);
}
public List<Produit> searchByNom(String nom) {
return produitRepository.findByNomContainingIgnoreCase(nom);
}
}
- Nom du fichier :
ProduitService.java
- Rôle : Service Spring gérant la logique métier des produits.
- Annotations utilisées :
@Service
: Indique que cette classe est un composant Spring de service.@Autowired
: Injecte automatiquementProduitRepository
.
- Dépendances :
ProduitRepository
: Interface pour la persistance des produits.
- Méthodes principales :
findAll()
: Retourne la liste de tous les produits.save(Produit produit)
: Enregistre ou met à jour un produit.findById(Integer id)
:- Recherche un produit par ID.
- Retourne
null
si le produit n’existe pas.
delete(Integer id)
: Supprime un produit par ID.searchByNom(String nom)
:- Utilise
findByNomContainingIgnoreCase
du repository. - Permet une recherche insensible à la casse.
- Utilise
- Structure du rendu :
- Définition d’une classe avec
@Service
. - Injection du repository via
@Autowired
. - Méthodes organisées selon leur rôle (CRUD et recherche).
- Définition d’une classe avec
Backend : ProduitController.java
package com.example.ecommerce.controller;
import com.example.ecommerce.entity.Produit;
import com.example.ecommerce.service.ProduitService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class ProduitController {
@Autowired
private ProduitService produitService;
@GetMapping("/produits")
public List<Produit> getAllProduits() {
return produitService.findAll();
}
@PostMapping("/admin/produits")
@PreAuthorize("hasRole('admin')")
public Produit addProduit(@RequestBody Produit produit) {
return produitService.save(produit);
}
@PutMapping("/admin/produits/{id}")
@PreAuthorize("hasRole('admin')")
public Produit updateProduit(@PathVariable int id, @RequestBody Produit produit) {
Produit existing = produitService.findById(id);
if (existing != null) {
existing.setNom(produit.getNom());
existing.setQuantiteStock(produit.getQuantiteStock());
existing.setPrix(produit.getPrix());
return produitService.save(existing);
}
return null;
}
@DeleteMapping("/admin/produits/{id}")
@PreAuthorize("hasRole('admin')")
public void deleteProduit(@PathVariable int id) {
produitService.delete(id);
}
@GetMapping("/produits/search")
public List<Produit> searchProduits(@RequestParam String nom) {
return produitService.searchByNom(nom);
}
}
- Nom du fichier :
ProduitController.java
- Rôle : Contrôleur REST gérant les produits.
- Annotations utilisées :
@RestController
: Définit cette classe comme un contrôleur Spring.@RequestMapping("/api")
: Définit le préfixe de toutes les routes.@Autowired
: Injecte leProduitService
.@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
: Pour gérer les requêtes HTTP.@PreAuthorize
: Restreint certaines actions aux administrateurs.
- Dépendances :
ProduitService
: Service pour la gestion des produits.
- Endpoints :
GET /api/produits
- getAllProduits() :- Retourne la liste complète des produits.
POST /api/admin/produits
- addProduit() :- Restreint aux administrateurs (
@PreAuthorize("hasRole('ADMIN')")
). - Ajoute un nouveau produit.
- Restreint aux administrateurs (
PUT /api/admin/produits/{id}
- updateProduit() :- Restreint aux administrateurs.
- Met à jour un produit existant par ID.
DELETE /api/admin/produits/{id}
- deleteProduit() :- Restreint aux administrateurs.
- Supprime un produit par ID.
GET /api/produits/search
- searchProduits() :- Recherche des produits par nom.
- Utilise
ProduitService.searchByNom()
.
- Structure du rendu :
- Classe annotée avec
@RestController
et@RequestMapping
. - Injection de
ProduitService
avec@Autowired
. - Endpoints bien structurés avec des autorisations pour l'administration.
- Classe annotée avec
Frontend : pages/ProductList.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function ProductList() {
const [produits, setProduits] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [quantites, setQuantites] = useState({});
const fetchProduits = async (term = '') => {
try {
const url = term ? `http://localhost:8080/api/produits/search?nom=${term}` : 'http://localhost:8080/api/produits';
const response = await axios.get(url, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setProduits(response.data);
} catch (error) {
alert('Erreur lors du chargement des produits');
}
};
useEffect(() => {
fetchProduits();
}, []);
const handleSearch = () => {
fetchProduits(searchTerm);
};
const addToCart = (produit) => {
const quantite = parseInt(quantites[produit.idProduit] || 1);
if (quantite <= 0 || quantite > produit.quantiteStock) {
alert(`Quantité invalide pour ${produit.nom}. Stock disponible : ${produit.quantiteStock}`);
return;
}
let cart = JSON.parse(localStorage.getItem('cart')) || [];
cart.push({ produit, quantite });
localStorage.setItem('cart', JSON.stringify(cart));
alert(`${produit.nom} (x${quantite}) ajouté au panier !`);
};
return (
<div className="container-fluid">
<div className="row">
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">Liste des Produits</h3>
<div className="card-tools">
<div className="input-group input-group-sm" style={{ width: '150px' }}>
<input
type="text"
className="form-control"
placeholder="Rechercher..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div className="input-group-append">
<button className="btn btn-default" onClick={handleSearch}>
<i className="fas fa-search"></i>
</button>
</div>
</div>
</div>
</div>
<div className="card-body table-responsive p-0">
<table className="table table-hover text-nowrap">
<thead>
<tr>
<th>Nom</th>
<th>Prix</th>
<th>Stock</th>
<th>Quantité</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{produits.map((produit) => (
<tr key={produit.idProduit}>
<td>{produit.nom}</td>
<td>{produit.prix} €</td>
<td>{produit.quantiteStock}</td>
<td>
<input
type="number"
min="1"
max={produit.quantiteStock}
className="form-control"
style={{ width: '70px' }}
value={quantites[produit.idProduit] || 1}
onChange={(e) => setQuantites({ ...quantites, [produit.idProduit]: e.target.value })}
/>
</td>
<td>
<button className="btn btn-primary btn-sm" onClick={() => addToCart(produit)}>
<i className="fas fa-cart-plus"></i> Ajouter
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}
export default ProductList;
- Nom du fichier :
ProductList.js
- Rôle : Afficher la liste des produits avec recherche et ajout au panier.
- Hooks React utilisés :
useState
: Gère l'état des produits, du terme de recherche et des quantités.useEffect
: Charge les produits au montage du composant.
- Fonctions principales :
fetchProduits()
:- Fait une requête à
/api/produits
pour récupérer tous les produits. - Si
searchTerm
est défini, utilise/api/produits/search
pour filtrer. - Stocke les résultats dans
produits
.
- Fait une requête à
addToCart(produitId, quantite)
:- Vérifie que la quantité saisie est valide.
- Ajoute le produit au panier dans
localStorage
.
- Structure du rendu :
- Une
card
AdminLTE contenant : - Un champ de recherche pour filtrer les produits.
- Une
table
affichant les produits avec leurs informations. - Un bouton "Ajouter au panier" avec un champ de quantité.
- Une
Tester /produits
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Navbar from './components/Navbar';
import Sidebar from './components/Sidebar';
import Login from './pages/Login';
import Register from './pages/Register';
import ProductList from './pages/ProductList';
// import AdminProducts from './pages/AdminProducts';
// import Cart from './pages/Cart';
// import Orders from './pages/Orders';
// import AdminUsers from './pages/AdminUsers';
// import PrivateRoute from './components/PrivateRoute';
// import AdminRoute from './components/AdminRoute';
function App() {
return (
<Router>
<div className="wrapper">
<Navbar />
<Sidebar />
<div className="content-wrapper">
<div className="content">
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
{/*
<Route element={<PrivateRoute />}>
<Route path="/produits" element={<ProductList />} />
</Route>
</Routes>
</div>
</div>
</div>
</Router>
);
}
export default App;
Frontend : pages/AdminProducts.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function AdminProducts() {
const [produits, setProduits] = useState([]);
const [nom, setNom] = useState('');
const [quantiteStock, setQuantiteStock] = useState(0);
const [prix, setPrix] = useState(0);
const [editId, setEditId] = useState(null);
useEffect(() => {
fetchProduits();
}, []);
const fetchProduits = async () => {
try {
const response = await axios.get('http://localhost:8080/api/produits', {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setProduits(response.data);
} catch (error) {
alert('Erreur lors du chargement des produits');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
const produit = { nom, quantiteStock, prix };
try {
if (editId) {
await axios.put(`http://localhost:8080/api/admin/produits/${editId}`, produit, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setEditId(null);
} else {
await axios.post('http://localhost:8080/api/admin/produits', produit, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
}
fetchProduits();
resetForm();
} catch (error) {
alert('Erreur lors de la gestion du produit');
}
};
const handleEdit = (produit) => {
setNom(produit.nom);
setQuantiteStock(produit.quantiteStock);
setPrix(produit.prix);
setEditId(produit.idProduit);
};
const handleDelete = async (id) => {
try {
await axios.delete(`http://localhost:8080/api/admin/produits/${id}`, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
fetchProduits();
} catch (error) {
alert('Erreur lors de la suppression');
}
};
const resetForm = () => {
setNom('');
setQuantiteStock(0);
setPrix(0);
};
return (
<div className="container-fluid">
<div className="row">
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">Gestion des Produits</h3>
</div>
<div className="card-body">
<form onSubmit={handleSubmit} className="mb-4">
<div className="form-group">
<label>Nom</label>
<input type="text" className="form-control" value={nom} onChange={(e) => setNom(e.target.value)} />
</div>
<div className="form-group">
<label>Quantité en stock</label>
<input type="number" className="form-control" value={quantiteStock} onChange={(e) => setQuantiteStock(e.target.value)} />
</div>
<div className="form-group">
<label>Prix</label>
<input type="number" step="0.01" className="form-control" value={prix} onChange={(e) => setPrix(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary">{editId ? 'Modifier' : 'Ajouter'}</button>
</form>
<table className="table table-bordered table-hover">
<thead>
<tr>
<th>Nom</th>
<th>Prix</th>
<th>Stock</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{produits.map((produit) => (
<tr key={produit.idProduit}>
<td>{produit.nom}</td>
<td>{produit.prix} €</td>
<td>{produit.quantiteStock}</td>
<td>
<button className="btn btn-warning btn-sm mr-2" onClick={() => handleEdit(produit)}>
<i className="fas fa-edit"></i>
</button>
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(produit.idProduit)}>
<i className="fas fa-trash"></i>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}
export default AdminProducts;
- Nom du fichier :
AdminProducts.js
- Rôle : Permet aux administrateurs de gérer les produits.
- Hooks React utilisés :
useState
: Gère l'état des produits, des champs du formulaire (nom, quantiteStock, prix) et de l’ID d’édition (editId
).
- Fonctions principales :
fetchProduits()
:- Charge la liste des produits depuis
/api/produits
. - Stocke les produits dans
produits
.
- Charge la liste des produits depuis
handleSubmit(event)
:- Empêche la soumission par défaut du formulaire.
- Si
editId
est défini, met à jour le produit avecPUT /api/admin/produits/{id}
. - Sinon, ajoute un nouveau produit avec
POST /api/admin/produits
. - Recharge la liste après l'opération.
handleEdit(produit)
:- Remplit le formulaire avec les données du produit sélectionné.
- Met à jour
editId
pour activer le mode modification.
handleDelete(id)
:- Supprime un produit via
DELETE /api/admin/produits/{id}
. - Recharge la liste après suppression.
- Supprime un produit via
- Structure du rendu :
- Une
card
AdminLTE contenant : - Un formulaire permettant l'ajout et la modification d'un produit.
- Une table affichant tous les produits avec des boutons Modifier/Supprimer.
- Une
Étape 4 : Gestion des Commandes et Panier
Backend : Entité Commande.java
package com.example.ecommerce.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonManagedReference;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Commande {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int idCommande;
private LocalDateTime dateCommande = LocalDateTime.now();
@ManyToOne
@JoinColumn(name = "idUtilisateur")
private Utilisateur utilisateur;
@JsonManagedReference
@OneToMany(mappedBy = "commande", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<LigneCommande> lignesCommande;
public int getIdCommande() {
return idCommande;
}
public void setIdCommande(int idCommande) {
this.idCommande = idCommande;
}
public LocalDateTime getDateCommande() {
return dateCommande;
}
public void setDateCommande(LocalDateTime dateCommande) {
this.dateCommande = dateCommande;
}
public Utilisateur getUtilisateur() {
return utilisateur;
}
public void setUtilisateur(Utilisateur utilisateur) {
this.utilisateur = utilisateur;
}
public List<LigneCommande> getLignesCommande() {
return lignesCommande;
}
public void setLignesCommande(List<LigneCommande> lignesCommande) {
this.lignesCommande = lignesCommande;
}
}
- Nom du fichier :
Commande.java
- Rôle : Représente une commande passée par un utilisateur.
- Annotations principales :
@Entity
: Indique que cette classe est une entité JPA.@Table(name = "commandes")
(optionnel) : Définit le nom de la table.
- Attributs :
idCommande
:@Id
: Définit la clé primaire.@GeneratedValue(strategy = GenerationType.IDENTITY)
: Génération automatique de l’ID.
dateCommande
:@Temporal(TemporalType.TIMESTAMP)
: Stocke la date et l’heure.- Initialisée à
new Date()
pour enregistrer la date de commande.
utilisateur
:@ManyToOne
: Relation plusieurs commandes → un utilisateur.@JoinColumn(name = "utilisateur_id")
: Clé étrangère vers l’entitéUtilisateur
.
lignesCommande
:@OneToMany(mappedBy = "commande", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
:- Relation un-à-plusieurs avec
LigneCommande
. cascade = CascadeType.ALL
: Propagation des opérations (ex : suppression).fetch = FetchType.EAGER
: Charge immédiatement les lignes de commande.
- Autres :
- Utilisation de
Lombok
pour générer les getters, setters et constructeurs. - Possible ajout d’une méthode
getTotal()
pour calculer le montant total.
- Utilisation de
Backend : Entité LigneCommande.java
package com.example.ecommerce.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import java.math.BigDecimal;
import com.fasterxml.jackson.annotation.JsonBackReference;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LigneCommande {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int idLigneCommande;
@JsonBackReference
@ManyToOne
@JoinColumn(name = "idCommande")
private Commande commande;
@ManyToOne
@JoinColumn(name = "idProduit")
private Produit produit;
@NotNull
private int quantite;
@NotNull
private BigDecimal prixUnitaire;
public int getIdLigneCommande() {
return idLigneCommande;
}
public void setIdLigneCommande(int idLigneCommande) {
this.idLigneCommande = idLigneCommande;
}
public Commande getCommande() {
return commande;
}
public void setCommande(Commande commande) {
this.commande = commande;
}
public Produit getProduit() {
return produit;
}
public void setProduit(Produit produit) {
this.produit = produit;
}
public int getQuantite() {
return quantite;
}
public void setQuantite(int quantite) {
this.quantite = quantite;
}
public BigDecimal getPrixUnitaire() {
return prixUnitaire;
}
public void setPrixUnitaire(BigDecimal prixUnitaire) {
this.prixUnitaire = prixUnitaire;
}
}
- Nom du fichier :
LigneCommande.java
- Rôle : Représente un élément d'une commande contenant un produit.
- Annotations principales :
@Entity
: Indique que cette classe est une entité JPA.@Table(name = "lignes_commande")
(optionnel) : Définit le nom de la table.
- Attributs :
idLigneCommande
:@Id
: Définit la clé primaire.@GeneratedValue(strategy = GenerationType.IDENTITY)
: Génération automatique de l’ID.
commande
:@ManyToOne
: Relation plusieurs lignes de commande → une commande.@JoinColumn(name = "commande_id")
: Clé étrangère versCommande
.
produit
:@ManyToOne
: Relation plusieurs lignes de commande → un produit.@JoinColumn(name = "produit_id")
: Clé étrangère versProduit
.
quantite
:- Quantité commandée du produit.
@NotNull
pour garantir une valeur obligatoire.
prixUnitaire
:BigDecimal
: Utilisé pour éviter les erreurs de précision monétaire.@NotNull
pour garantir une valeur obligatoire.
- Autres :
- Utilisation de
Lombok
pour générer les getters, setters et constructeurs. - Possible ajout d’une méthode
getTotalLigne()
pour calculer le total de la ligne (quantite × prixUnitaire
).
- Utilisation de
Backend : Repository CommandeRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.Commande;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CommandeRepository extends JpaRepository<Commande, Integer> {
List<Commande> findByUtilisateurIdUtilisateur(int idUtilisateur);
}
- Nom du fichier :
CommandeRepository.java
- Rôle : Interface permettant d’accéder aux commandes en base de données.
- Annotations principales :
@Repository
(optionnel mais recommandé) : Indique que cette interface est un bean Spring pour la gestion des données.
- Héritage :
- Étend
JpaRepository<Commande, Long>
, offrant des méthodes CRUD par défaut.
- Étend
- Méthodes spécifiques :
List<Commande> findByUtilisateurIdUtilisateur(Long idUtilisateur);
:- Recherche toutes les commandes passées par un utilisateur donné.
- Utilise la convention de nommage Spring Data JPA pour générer automatiquement la requête.
- Autres :
- Peut être enrichi avec des requêtes
@Query
pour des recherches avancées. - Possibilité d’ajouter une pagination avec
Pageable
en paramètre.
- Peut être enrichi avec des requêtes
Backend : Repository LigneCommandeRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.LigneCommande;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LigneCommandeRepository extends JpaRepository<LigneCommande, Integer> {
}
- Nom du fichier :
LigneCommandeRepository.java
- Rôle : Interface d’accès aux lignes de commande en base de données.
- Annotations principales :
@Repository
(optionnel mais recommandé) : Indique que cette interface est un bean Spring pour la gestion des données.
- Héritage :
- Étend
JpaRepository<LigneCommande, Long>
, offrant des méthodes CRUD par défaut.
- Étend
- Méthodes spécifiques :
- Utilisation possible de
findByCommandeIdCommande(Long idCommande)
pour récupérer les lignes d’une commande donnée.
- Utilisation possible de
- Autres :
- Peut être enrichi avec des requêtes
@Query
pour des besoins spécifiques. - Possibilité d’utiliser la pagination avec
Pageable
.
- Peut être enrichi avec des requêtes
Backend : Service CommandeService.java
package com.example.ecommerce.service;
import com.example.ecommerce.entity.Commande;
import com.example.ecommerce.entity.LigneCommande;
import com.example.ecommerce.entity.Produit;
import com.example.ecommerce.repository.CommandeRepository;
import com.example.ecommerce.repository.LigneCommandeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CommandeService {
@Autowired
private CommandeRepository commandeRepository;
@Autowired
private LigneCommandeRepository ligneCommandeRepository;
@Autowired
private ProduitService produitService;
public Commande save(Commande commande) {
return commandeRepository.save(commande);
}
public List<Commande> findByUtilisateurId(int idUtilisateur) {
return commandeRepository.findByUtilisateurIdUtilisateur(idUtilisateur);
}
public LigneCommande addLigneCommande(int idCommande, int idProduit, int quantite) {
Commande commande = commandeRepository.findById(idCommande).orElse(null);
Produit produit = produitService.findById(idProduit);
if (commande != null && produit != null && produit.getQuantiteStock() >= quantite) {
LigneCommande ligne = new LigneCommande();
ligne.setCommande(commande);
ligne.setProduit(produit);
ligne.setQuantite(quantite);
ligne.setPrixUnitaire(produit.getPrix());
produit.setQuantiteStock(produit.getQuantiteStock() - quantite);
produitService.save(produit);
return ligneCommandeRepository.save(ligne);
}
return null;
}
public void deleteLigneCommande(int idLigneCommande) {
LigneCommande ligne = ligneCommandeRepository.findById(idLigneCommande).orElse(null);
if (ligne != null) {
Produit produit = ligne.getProduit();
produit.setQuantiteStock(produit.getQuantiteStock() + ligne.getQuantite());
produitService.save(produit);
ligneCommandeRepository.delete(ligne);
}
}
}
- Nom du fichier :
CommandeService.java
- Rôle : Service métier pour la gestion des commandes.
- Annotations principales :
@Service
: Indique que cette classe est un service Spring injectable.
- Dépendances injectées :
CommandeRepository
: Gestion des commandes en base de données.LigneCommandeRepository
: Gestion des lignes de commande.ProduitService
: Mise à jour du stock des produits.
- Méthodes principales :
save(Commande commande)
: Persiste une commande en base de données.findByUtilisateurId(Long idUtilisateur)
: Retourne toutes les commandes d’un utilisateur.addLigneCommande(Long idCommande, Long idProduit, int quantite)
:- Ajoute une ligne de commande à une commande existante.
- Met à jour le stock du produit.
- Retourne la ligne de commande créée.
deleteLigneCommande(Long idLigneCommande)
:- Supprime une ligne de commande.
- Restaure le stock du produit correspondant.
- Autres :
- Gestion des exceptions en cas de stock insuffisant.
- Possibilité d’amélioration avec la gestion transactionnelle (
@Transactional
).
Backend : CommandeController.java
package com.example.ecommerce.controller;
import com.example.ecommerce.entity.Commande;
import com.example.ecommerce.entity.LigneCommande;
import com.example.ecommerce.entity.Utilisateur;
import com.example.ecommerce.service.CommandeService;
import com.example.ecommerce.service.UtilisateurService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class CommandeController {
@Autowired
private CommandeService commandeService;
@Autowired
private UtilisateurService utilisateurService;
@PostMapping("/commandes")
public Commande createCommande() {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
Utilisateur utilisateur = utilisateurService.findByEmail(email);
Commande commande = new Commande();
commande.setUtilisateur(utilisateur);
return commandeService.save(commande);
}
@GetMapping("/commandes")
public List<Commande> getUserCommandes() {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
Utilisateur utilisateur = utilisateurService.findByEmail(email);
return commandeService.findByUtilisateurId(utilisateur.getIdUtilisateur());
}
@PostMapping("/commandes/{idCommande}/produits/{idProduit}")
public LigneCommande addProduitToCommande(@PathVariable int idCommande, @PathVariable int idProduit, @RequestParam int quantite) {
return commandeService.addLigneCommande(idCommande, idProduit, quantite);
}
@DeleteMapping("/commandes/lignes/{idLigneCommande}")
public void deleteLigneCommande(@PathVariable int idLigneCommande) {
commandeService.deleteLigneCommande(idLigneCommande);
}
}
- Nom du fichier :
CommandeController.java
- Rôle : Gérer les endpoints liés aux commandes.
- Annotations principales :
@RestController
: Définit un contrôleur REST.@RequestMapping("/api/commandes")
: Définit le préfixe des routes.@Autowired
: Injection du serviceCommandeService
.
- Dépendance injectée :
CommandeService
: Gestion des commandes et lignes de commande.
- Endpoints :
createCommande()
(POST /api/commandes
) :- Crée une nouvelle commande pour l’utilisateur connecté.
- Retourne la commande créée.
getUserCommandes()
(GET /api/commandes
) :- Retourne toutes les commandes de l’utilisateur connecté.
addProduitToCommande(Long idCommande, Long idProduit, int quantite)
(POST /api/commandes/{idCommande}/produits/{idProduit}
) :- Ajoute un produit à une commande existante.
- Met à jour le stock du produit.
deleteLigneCommande(Long idLigneCommande)
(DELETE /api/commandes/lignes/{idLigneCommande}
) :- Supprime une ligne de commande.
- Restaure le stock du produit correspondant.
- Autres :
- Gère les erreurs en cas de produit indisponible ou commande inexistante.
- Possibilité d’amélioration avec
@PreAuthorize
pour sécuriser les endpoints.
Frontend : pages/Cart.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
function Cart() {
const [cart, setCart] = useState([]);
const [error, setError] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const storedCart = JSON.parse(localStorage.getItem('cart')) || [];
setCart(storedCart);
}, []);
const updateQuantity = (idProduit, newQuantity) => {
const updatedCart = cart.map(item => {
if (item.produit.idProduit === idProduit) {
const quantite = parseInt(newQuantity);
if (quantite <= 0 || quantite > item.produit.quantiteStock) {
alert(`Quantité invalide. Stock disponible : ${item.produit.quantiteStock}`);
return item;
}
return { ...item, quantite };
}
return item;
});
setCart(updatedCart);
localStorage.setItem('cart', JSON.stringify(updatedCart));
};
const handleOrder = async () => {
try {
const response = await axios.post('http://localhost:8080/api/commandes', {}, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
const idCommande = response.data.idCommande;
for (const item of cart) {
await axios.post(`http://localhost:8080/api/commandes/${idCommande}/produits/${item.produit.idProduit}`,
null,
{
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` },
params: { quantite: item.quantite }
}
);
}
localStorage.setItem('cart', JSON.stringify([]));
setCart([]);
alert('Commande passée avec succès !');
navigate('/commandes');
} catch (error) {
setError(error.response?.data || 'Erreur lors de la commande');
}
};
const total = cart.reduce((sum, item) => sum + item.produit.prix * item.quantite, 0);
return (
<div className="container-fluid">
<div className="row">
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">Panier</h3>
</div>
<div className="card-body">
{error && <div className="alert alert-danger">{typeof error === 'string' ? error : 'Erreur inconnue'}</div>}
{cart.length === 0 ? (
<p>Votre panier est vide.</p>
) : (
<>
<table className="table table-bordered table-hover">
<thead>
<tr>
<th>Produit</th>
<th>Prix</th>
<th>Quantité</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{cart.map((item) => (
<tr key={item.produit.idProduit}>
<td>{item.produit.nom}</td>
<td>{item.produit.prix} €</td>
<td>
<input
type="number"
min="1"
max={item.produit.quantiteStock}
className="form-control"
style={{ width: '70px' }}
value={item.quantite}
onChange={(e) => updateQuantity(item.produit.idProduit, e.target.value)}
/>
</td>
<td>{(item.produit.prix * item.quantite).toFixed(2)} €</td>
</tr>
))}
</tbody>
</table>
<h4>Total : {total.toFixed(2)} €</h4>
<button className="btn btn-success" onClick={handleOrder}>Passer la commande</button>
</>
)}
</div>
</div>
</div>
</div>
</div>
);
}
export default Cart;
http://localhost:3000/panier- Nom du fichier :
Cart.js
- Rôle : Gérer le panier d’achat et l’envoi de commandes.
- Hooks utilisés :
useState
: Gère l’état du panier (cart
) et les erreurs (error
).useEffect
: Charge les produits du panier depuislocalStorage
au montage.useNavigate
: Redirection après la validation de la commande.
- Fonctions principales :
updateQuantity(id, newQuantity)
:- Modifie la quantité d’un produit dans le panier.
- Met à jour
localStorage
.
handleRemove(id)
:- Supprime un produit du panier.
- Met à jour l’état et
localStorage
.
handleOrder()
:- Crée une commande en envoyant une requête
POST /api/commandes
. - Ajoute chaque produit au panier via
POST /api/commandes/{idCommande}/produits/{idProduit}
. - Vide le panier et redirige vers la liste des commandes.
- Crée une commande en envoyant une requête
- Rendu :
- Utilise une carte AdminLTE pour l’affichage.
- Affiche une table avec les produits du panier (nom, prix, quantité, total).
- Affiche un bouton pour valider la commande et un total général.
- Autres :
- Gère les erreurs en cas de requête échouée.
- Vérifie que les quantités sont valides avant l’envoi.
Frontend : pages/Orders.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function Orders() {
const [commandes, setCommandes] = useState([]);
useEffect(() => {
const fetchCommandes = async () => {
try {
const response = await axios.get('http://localhost:8080/api/commandes', {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setCommandes(response.data);
} catch (error) {
alert('Erreur lors du chargement des commandes');
}
};
fetchCommandes();
}, []);
const calculateTotal = (lignes) => {
return lignes.reduce((sum, ligne) => sum + (ligne.prixUnitaire * ligne.quantite), 0).toFixed(2);
};
return (
<div className="container-fluid">
<div className="row">
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">Mes Commandes</h3>
</div>
<div className="card-body">
{commandes.length === 0 ? (
<p>Aucune commande</p>
) : (
commandes.map((commande) => (
<div key={commande.idCommande} className="card mb-3">
<div className="card-header">
Commande #{commande.idCommande} - Date: {new Date(commande.dateCommande).toLocaleString()}
</div>
<div className="card-body">
<table className="table table-bordered">
<thead>
<tr>
<th>Produit</th>
<th>Quantité</th>
<th>Prix Unitaire</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{commande.lignesCommande.map((ligne) => (
<tr key={ligne.idLigneCommande}>
<td>{ligne.produit.nom}</td>
<td>{ligne.quantite}</td>
<td>{ligne.prixUnitaire} €</td>
<td>{(ligne.prixUnitaire * ligne.quantite).toFixed(2)} €</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan="3" className="text-right"><strong>Total</strong></td>
<td><strong>{calculateTotal(commande.lignesCommande)} €</strong></td>
</tr>
</tfoot>
</table>
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
);
}
export default Orders;
http://localhost:3000/commandes- Nom du fichier :
Orders.js
- Rôle : Afficher la liste des commandes détaillées de l’utilisateur.
- Hooks utilisés :
useState
: Gère l’état des commandes (commandes
).useEffect
: Charge les commandes depuis/api/commandes
au montage.
- Fonctions principales :
fetchCommandes()
:- Envoie une requête
GET /api/commandes
pour récupérer les commandes. - Met à jour l’état avec les données reçues.
- Envoie une requête
calculateTotal(commande)
:- Calcule le total d’une commande en additionnant
quantite * prixUnitaire
pour chaque produit.
- Calcule le total d’une commande en additionnant
- Rendu :
- Utilise des cartes AdminLTE pour afficher chaque commande.
- Affiche la date, les produits commandés (nom, quantité, prix unitaire, total partiel).
- Affiche un total général par commande.
- Autres :
- Affiche un message si aucune commande n’est trouvée.
- Gère les erreurs de récupération des données.
Étape 5 : Gestion des Utilisateurs (Admin)
Backend : UtilisateurController.java
package com.example.ecommerce.controller;
import com.example.ecommerce.entity.Utilisateur;
import com.example.ecommerce.service.UtilisateurService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/admin")
public class UtilisateurController {
@Autowired
private UtilisateurService utilisateurService;
@GetMapping("/utilisateurs")
@PreAuthorize("hasRole('admin')")
public List<Utilisateur> getAllUtilisateurs() {
return utilisateurService.findAll();
}
@DeleteMapping("/utilisateurs/{id}")
@PreAuthorize("hasRole('admin')")
public void deleteUtilisateur(@PathVariable int id) {
utilisateurService.delete(id);
}
}
- Nom du fichier :
UtilisateurController.java
- Rôle : Gérer les utilisateurs pour les administrateurs.
- Annotations utilisées :
@RestController
: Indique que c’est un contrôleur Spring REST.@RequestMapping("/api/admin/utilisateurs")
: Définit le préfixe des routes.@PreAuthorize("hasRole('admin')")
: Restreint l’accès aux administrateurs.
- Endpoints :
getAllUtilisateurs()
–GET /api/admin/utilisateurs
- Récupère la liste complète des utilisateurs.
- Renvoie une liste JSON contenant les utilisateurs.
deleteUtilisateur(Long id)
–DELETE /api/admin/utilisateurs/{id}
- Supprime un utilisateur par son
id
. - Renvoie une confirmation après suppression.
- Supprime un utilisateur par son
- Sécurité :
- Les actions sont protégées par
@PreAuthorize("hasRole('admin')")
, garantissant que seuls les administrateurs peuvent les exécuter.
- Les actions sont protégées par
Frontend : pages/AdminUsers.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function AdminUsers() {
const [utilisateurs, setUtilisateurs] = useState([]);
useEffect(() => {
fetchUtilisateurs();
}, []);
const fetchUtilisateurs = async () => {
try {
const response = await axios.get('http://localhost:8080/api/admin/utilisateurs', {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setUtilisateurs(response.data);
} catch (error) {
alert('Erreur lors du chargement des utilisateurs');
}
};
const handleDelete = async (id) => {
try {
await axios.delete(`http://localhost:8080/api/admin/utilisateurs/${id}`, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
fetchUtilisateurs();
} catch (error) {
alert('Erreur lors de la suppression');
}
};
return (
<div className="container-fluid">
<div className="row">
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">Gestion des Utilisateurs</h3>
</div>
<div className="card-body">
<table className="table table-bordered table-hover">
<thead>
<tr>
<th>Nom</th>
<th>Email</th>
<th>Rôle</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{utilisateurs.map((utilisateur) => (
<tr key={utilisateur.idUtilisateur}>
<td>{utilisateur.nom}</td>
<td>{utilisateur.email}</td>
<td>{utilisateur.role}</td>
<td>
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(utilisateur.idUtilisateur)}>
<i className="fas fa-trash"></i>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}export default AdminUsers;
- Nom du fichier :
AdminUsers.js
- Rôle : Permet aux administrateurs de gérer les utilisateurs.
- Hooks utilisés :
useState
: Gère l'état des utilisateurs.useEffect
: Charge les utilisateurs depuis l’API au montage du composant.
- Fonctionnalités :
fetchUsers()
: Récupère la liste des utilisateurs viaGET /api/admin/utilisateurs
.handleDelete(userId)
: Supprime un utilisateur viaDELETE /api/admin/utilisateurs/{id}
et met à jour la liste.
- Rendu :
- Utilise une carte AdminLTE pour afficher les utilisateurs.
- Affiche une table listant les utilisateurs avec un bouton de suppression.
- Sécurité :
- Accessible uniquement aux administrateurs.
Étape 6 : Gestion des Erreurs
Backend : GlobalExceptionHandler.java
package com.example.ecommerce.config;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralExceptions(Exception ex) {
return new ResponseEntity<>("Une erreur est survenue : " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
- Nom du fichier :
GlobalExceptionHandler.java
- Rôle : Gestion centralisée des exceptions.
- Annonations utilisées :
@RestControllerAdvice
: Permet d'intercepter globalement les exceptions dans toute l'application.@ExceptionHandler
: Définit des méthodes pour gérer différents types d'exceptions.
- Exceptions gérées :
handleValidationExceptions(MethodArgumentNotValidException ex)
:- Intercepte les erreurs de validation (
@NotNull
,@Size
, etc.). - Retourne un statut 400 (Bad Request).
- Récupère et retourne les messages d’erreur de chaque champ.
- Intercepte les erreurs de validation (
handleGeneralExceptions(Exception ex)
:- Gère les autres exceptions non spécifiquement traitées.
- Retourne un statut 500 (Internal Server Error).
- Renvoie un message générique pour éviter d'exposer des détails sensibles.
- Bénéfices :
- Centralise la gestion des erreurs pour une meilleure maintenance.
- Améliore la lisibilité et la cohérence des réponses d'erreur.
- Évite de dupliquer du code de gestion d'erreur dans les contrôleurs.