By:Hakiki

Spring MVC

  • Introduction
    • Aperçu Général
    • Fonctionnalités
    • Technologies Utilisées
    • Étape 1 : Configuration Initiale
      • Backend
        • pom.xml
        • EcommerceApplication.java
        • application.properties
      • Frontend
        • package.json
        • public/index.html
        • index.js
    • Étape 2 : Authentification et Inscription
      • Backend
        • entity/Utilisateur.java
        • repository/UtilisateurRepository.java
        • service/UtilisateurService.java
        • service/CustomUserDetailsService.java
        • config/JwtUtil.java
        • config/JwtFilter.java
        • config/SecurityConfig.java
        • controller/AuthController.java
      • Frontend
        • App.js
        • components/Navbar.js
        • components/Sidebar.js
        • pages/Login.js
        • pages/Register.js
        • components/PrivateRoute.js
        • components/AdminRoute.js
    • Étape 3 : Gestion des Produits
      • Backend
        • entity/Produit.java
        • repository/ProduitRepository.java
        • service/ProduitService.java
        • controller/ProduitController.java
      • Frontend
        • pages/ProductList.js
        • pages/AdminProducts.js
    • Étape 4 : Gestion des Commandes et Panier
      • Backend
        • entity/Commande.java
        • entity/LigneCommande.java
        • repository/CommandeRepository.java
        • repository/LigneCommandeRepository.java
        • service/CommandeService.java
        • controller/CommandeController.java
      • Frontend
        • pages/Cart.js
        • pages/Orders.js
    • Étape 5 : Gestion des Utilisateurs (Admin)
      • Backend
        • controller/UtilisateurController.java
      • Frontend
        • pages/AdminUsers.js
    • Étape 6 : Gestion des Erreurs
      • Backend
        • config/GlobalExceptionHandler.java
  • Structure des Projets
    • Structure du Projet Backend : ecommerce-backend
    • Structure du Projet Frontend : ecommerce-frontend
  • Résumé Final




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

  1. 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
  • 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 REST
    • spring-boot-starter-data-jpa → JPA/Hibernate
    • spring-boot-starter-security → Sécurité avec JWT
    • mysql-connector-java → Connexion à MySQL
    • jjwt → Gestion des tokens JWT
    • lombok → 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 package com.example.ecommerce.
  • Méthode principale :
    • main() → Utilise SpringApplication.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 base ecommerce_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

  1. Téléchargez et installez Node.js depuis https://nodejs.org.

  2. Redémarrez votre terminal.

  3. Vérifiez avec node -v et npm -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 :
    • nameecommerce-frontend
    • version0.1.0
  • Dépendances principales :
    • axios → Gestion des requêtes HTTP.
    • react et react-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 de react-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.

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 avec ReactDOM.render.
  • 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 table Utilisateur 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éthodes toString, equals, et hashCode.
    • @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 et admin.

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).
  • 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, retourne null 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.

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.

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 : Utilise BCryptPasswordEncoder 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.
    • POST /api/auth/register :
      • Sauvegarde un nouvel utilisateur avec utilisateurService.save.
  • DTO interne : LoginRequest
    • Contient les champs email et password.
    • Utilisé pour transmettre les données de connexion.

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 de react-router-dom.
    • PrivateRoute et AdminRoute (à créer).
    • Composants UI : Navbar, Sidebar, Login, Register, etc.
  • Structure du rendu :
    • Utilisation de AdminLTE pour la mise en page.
    • <aside> pour inclure Navbar et Sidebar.
    • <main> pour afficher les routes.
  • Routes définies :
    • /login et /register : Accès public.
    • Autres routes protégées par PrivateRoute ou AdminRoute.

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 de useNavigate 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.
  • Structure du rendu :
    • Utilisation de la classe main-header d’AdminLTE.
    • Inclut un logo et un bouton de déconnexion.

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 avec Link.
    • 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.

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.

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.

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 de react-router-dom : Rend le contenu des routes protégées.
  • Fonctionnalités principales :
    • isAuthenticated vérifie la présence d’un token dans localStorage.
    • Si authentifié, affiche le contenu avec Outlet.
    • Sinon, redirige vers /login via useNavigate.
  • 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 de react-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.
  • 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.

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 automatiquement ProduitRepository.
  • 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.
  • 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).

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 le ProduitService.
    • @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.
    • 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.

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.
    • 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é.

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;	
/produits
  • 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.
    • handleSubmit(event) :
      • Empêche la soumission par défaut du formulaire.
      • Si editId est défini, met à jour le produit avec PUT /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.
  • 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.

É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.

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 vers Commande.
    • produit :
      • @ManyToOne : Relation plusieurs lignes de commande → un produit.
      • @JoinColumn(name = "produit_id") : Clé étrangère vers Produit.
    • 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).

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.
  • 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.

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.
  • Méthodes spécifiques :
    • Utilisation possible de findByCommandeIdCommande(Long idCommande) pour récupérer les lignes d’une commande donnée.
  • Autres :
    • Peut être enrichi avec des requêtes @Query pour des besoins spécifiques.
    • Possibilité d’utiliser la pagination avec Pageable.

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 service CommandeService.
  • 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 depuis localStorage 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.
  • 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.
    • calculateTotal(commande) :
      • Calcule le total d’une commande en additionnant quantite * prixUnitaire pour chaque produit.
  • 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.
  • Sécurité :
    • Les actions sont protégées par @PreAuthorize("hasRole('admin')"), garantissant que seuls les administrateurs peuvent les exécuter.

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 via GET /api/admin/utilisateurs.
    • handleDelete(userId) : Supprime un utilisateur via DELETE /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.
    • 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.