Conception et développement d'une plateforme de gestion de chambres d'hôtes avec Spring Boot et Vue.js

Stack Technique et Architecture

Le développement de cette plateforme de gestion de chambres d'hôtes repose sur une architecture moderne et découplée, optimisée pour la performance, l'évolutivité et la maintenabilité.

1. Front-end : Vue.js

Vue.js est utilisé pour construire l'interface utilisateur réactive. Grâce à son système de composants et à l'API de composition, le code front-end est hautement modulaire. L'intégration de Vue Router gère la navigation côté client, tandis que Pinia centralise la gestion de l'état global de l'application, assurant une synchronisation fluide des données telles que les disponibilités des logements, les filtres de recherche et le panier de réservation.

2. Back-end : Spring Boot

Spring Boot sert de colonne vertébrale pour les services métier. En adoptant le principe de "convention over configuration", il permet un déploiement rapide grâce à son serveur Tomcat intégré. Le framwork facilite l'exposition d'API RESTful, la gestion des transactions via Spring Data JPA, et l'implémentation de mécanismes de sécurité robustes. L'utilisation de Spring Actuator permet également de superviser la santé de l'application et d'exposer des métriques en production.

3. Base de données : MySQL

MySQL est employé comme système de gestion de base de données relationnelle (SGBDR). Le moteur de stockage InnoDB est privilégié pour sa conformité ACID et son support des clés étrangères, essentiels pour maintenir l'intégrité des données entre les tables des utilisateurs, des propriétés et des réservations. L'optimisation des requêtes est assurée par la création d'index sur les colonnes fréquemment interrogées, comme les dates de disponibilité et les identifiants de logements.

4. Architecture B/S (Browser/Server)

Le système adopte une architecture Browser/Server (B/S) avec une séparation stricte entre le front-end et le back-end. Les échanges de données s'effectuent exclusivement via des requêtes HTTP/HTTPS au format JSON. Cette approche permet aux équipes de développement de travailler en parallèle et garantit une expérience utilisateur fluide, typique des applications monopages (SPA), tout en simplifiant les mises à jour et le déploiement continu via des conteneurs Docker.

Stratégie de Test et Validation

Les tests fonctionnels sont conçus pour valider les parcours utilisateurs critiques à travers une approche de boîte noire. L'objectif est de s'assurer que chaque module répond aux spécifications fonctionnelles sans nécessiter de connaissances sur l'implémentation interne du code.

1. Tests fonctionnels côté client (Locataire)

ID Fonctionnalité Scénario de test Résultat attendu Statut
1 Inscription Saisir un email valide et un mot de passe sécurisé Compte créé avec succès Succès
2 Recherche de logement Filtrer les chambres par ville et dates de disponibilité Liste des logements correspondants affichée Succès
3 Réservation Sélectionner un logement et confirmer les dates Réservation enregistrée et paiement initié Succès
4 Avis client Rédiger et soumettre un commentaire après un séjour Avis publié sur la page du logement Succès

2. Tests fonctionnels côté administration (Hôte)

ID Fonctionnalité Scénario de test Résultat attendu Statut
1 Authentification admin Connexion avec les identifiants administrateur Accès au tableau de bord Succès
2 Gestion des biens Ajouter un nouveau logement avec photos et tarifs Logement visible dans le catalogue Succès
3 Traitement des commandes Valider une demande de réservation en attente Statut mis à jour et notification envoyée Succès
4 Modération Supprimer un avis inapproprié d'un utilisateur Avis retiré de la base de données Succès

Implémentation du Code

Les extraits suivants illustrent la logique d'authentification par jeton (Token) et la configuration du schéma de la base de données relationnelle.

Contrôleur d'authentification et Intercepteur de sécurité (Spring Boot)


@RestController
@RequestMapping("/api/v1/auth")
public class AuthenticationController {

    private final UserService userService;
    private final JwtTokenProvider tokenProvider;
    private final PasswordEncoder passwordEncoder;

    public AuthenticationController(UserService userService, JwtTokenProvider tokenProvider, PasswordEncoder passwordEncoder) {
        this.userService = userService;
        this.tokenProvider = tokenProvider;
        this.passwordEncoder = passwordEncoder;
    }

    @PostMapping("/signin")
    public ResponseEntity<ApiResponse> authenticateUser(@RequestBody LoginRequest request) {
        Optional<User> userOpt = userService.findByEmail(request.getEmail());
        
        if (userOpt.isEmpty() || !passwordEncoder.matches(request.getPassword(), userOpt.get().getPassword())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new ApiResponse(false, "Identifiants invalides"));
        }

        User authenticatedUser = userOpt.get();
        String jwt = tokenProvider.generateJwt(authenticatedUser.getId(), authenticatedUser.getEmail(), authenticatedUser.getRole());
        
        return ResponseEntity.ok(new ApiResponse(true, "Connexion réussie", jwt));
    }
}

@Component
public class JwtSecurityInterceptor implements HandlerInterceptor {

    private static final String AUTH_HEADER = "Authorization";
    private static final String BEARER_PREFIX = "Bearer ";

    private final JwtTokenProvider tokenProvider;

    public JwtSecurityInterceptor(JwtTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        applyCorsHeaders(response, request);

        if (RequestMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return false;
        }

        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            if (handlerMethod.hasMethodAnnotation(PermitAll.class)) {
                return true;
            }
        }

        String header = request.getHeader(AUTH_HEADER);
        if (header != null && header.startsWith(BEARER_PREFIX)) {
            String token = header.substring(BEARER_PREFIX.length());
            if (tokenProvider.isTokenValid(token)) {
                request.setAttribute("currentUserId", tokenProvider.extractUserId(token));
                request.setAttribute("currentUserRole", tokenProvider.extractRole(token));
                return true;
            }
        }

        writeUnauthorizedResponse(response, "Accès refusé : authentification requise");
        return false;
    }

    private void applyCorsHeaders(HttpServletResponse response, HttpServletRequest request) {
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With");
        response.setHeader("Access-Control-Allow-Credentials", "true");
    }

    private void writeUnauthorizedResponse(HttpServletResponse response, String message) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.write("{\"success\":false, \"message\":\"" + message + "\"}");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Schéma de la base de données (MySQL)


CREATE DATABASE IF NOT EXISTS `homestay_management_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `homestay_management_db`;

-- Table des propriétés (chambres d'hôtes)
DROP TABLE IF EXISTS `properties`;
CREATE TABLE `properties` (
  `property_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `host_id` bigint(20) NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `title` varchar(255) NOT NULL,
  `description` text,
  `address` varchar(255) NOT NULL,
  `price_per_night` decimal(10,2) NOT NULL,
  `max_guests` int(11) NOT NULL DEFAULT '2',
  `status` enum('AVAILABLE', 'BOOKED', 'MAINTENANCE') DEFAULT 'AVAILABLE',
  `cover_image_url` varchar(500),
  PRIMARY KEY (`property_id`),
  KEY `idx_host_id` (`host_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Catalogue des logements';

-- Table des réservations
DROP TABLE IF EXISTS `bookings`;
CREATE TABLE `bookings` (
  `booking_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `property_id` bigint(20) NOT NULL,
  `guest_id` bigint(20) NOT NULL,
  `check_in_date` date NOT NULL,
  `check_out_date` date NOT NULL,
  `total_amount` decimal(10,2) NOT NULL,
  `booking_status` enum('PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED') DEFAULT 'PENDING',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`booking_id`),
  KEY `fk_booking_property` (`property_id`),
  KEY `fk_booking_guest` (`guest_id`),
  CONSTRAINT `fk_booking_property` FOREIGN KEY (`property_id`) REFERENCES `properties` (`property_id`) ON DELETE CASCADE,
  CONSTRAINT `fk_booking_guest` FOREIGN KEY (`guest_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Historique et gestion des réservations';

Étiquettes: Spring Boot Vue.js MySQL JWT REST API

Publié le 16 juillet à 09h51