Skip to content

Latest commit

 

History

History
235 lines (184 loc) · 8.49 KB

File metadata and controls

235 lines (184 loc) · 8.49 KB

RentaCar — Full-Stack Car Rental Platform

A full-stack car rental application built with Spring Boot 4 (Java 25) + React 19, featuring JWT cookie-based authentication, role-based access control, soft-delete with Hibernate @SQLRestriction, MapStruct DTO mapping, and Docker Compose deployment. Built as a learning project exploring enterprise Java patterns.

✨ Features

  • 🔐 JWT HttpOnly cookie authentication — XSS-safe token storage
  • 👥 Role-based access control (USER, ADMIN) via Spring Security @PreAuthorize
  • 🚗 Car & brand catalog with admin CRUD
  • 📅 Reservation system with date-range conflict detection and pessimistic-style overlap check
  • 🗑 Soft delete on all entities via @SQLRestriction("is_deleted = false")
  • 🔄 MapStruct for entity ↔ DTO mapping (zero boilerplate)
  • 📊 Transactional service layer (@Transactional(readOnly = true) at class level, write overrides)
  • 🧱 Global exception handler with typed business exceptions
  • 🐳 Docker Compose — MySQL + Spring Boot + Nginx React SPA, one command
  • 🏠 Modern responsive UI — React 19 + Vite + Tailwind + shadcn-style components
  • 🖼 Hero carousel + search bar on homepage; availability query for date-range filter
  • 📱 RTK Query for data fetching with tag-based cache invalidation

🛠 Tech Stack

Backend

Layer Technology
Language Java 25
Framework Spring Boot 4.0.5
Web Spring Web MVC
Security Spring Security 7 + JWT (jjwt 0.12.6)
ORM Spring Data JPA + Hibernate 7
Mapping MapStruct 1.5.5
Validation Jakarta Bean Validation (Hibernate Validator)
Database MySQL 8.0
Boilerplate Lombok

Frontend

Layer Technology
Framework React 19 + Vite
Styling Tailwind CSS v4 + shadcn-style primitives
State / data Redux Toolkit + RTK Query
Routing React Router v7
Icons lucide-react

DevOps

  • Docker multi-stage builds (JDK 25 for build → JRE 25 for runtime)
  • Docker Compose orchestration (MySQL + backend + Nginx frontend)
  • Nginx serving built SPA + reverse-proxying /api/* to backend

📁 Project Structure

rentacarproject/
├── src/main/java/com/rentacar/rentacarproject/
│   ├── controller/         # REST controllers (Auth, Brand, Car, Reservation)
│   ├── service/            # Business logic + @Transactional
│   ├── repository/         # Spring Data JPA interfaces
│   ├── entity/             # JPA entities (User, Brand, Car, Reservation)
│   ├── dto/                # Request/response DTOs (auth, brand, car, reservation)
│   ├── mapper/             # MapStruct mapper interfaces
│   ├── security/           # JwtService, JwtAuthFilter, SecurityService
│   ├── exception/          # AppException + GlobalExceptionHandler
│   └── enums/              # Role, Status, Color, FuelType, Transmission, ReservationStatus
│
├── src/main/resources/
│   └── application.properties  # Spring config (env-var driven)
│
├── frontend/
│   ├── src/
│   │   ├── app/            # RTK store + baseApi
│   │   ├── features/       # auth, brand, car, reservation (each feature owns its API)
│   │   ├── pages/          # HomePage, CarListPage, CarDetailPage, ...
│   │   ├── components/     # Layout, Header, HeroCarousel, SearchBar, ui/*
│   │   └── hooks/          # useAuth
│   ├── nginx.conf          # SPA fallback + API proxy
│   └── Dockerfile
│
├── Dockerfile              # Multi-stage Spring Boot image
├── docker-compose.yml      # MySQL + backend + frontend
├── .env.example            # Env var template
└── pom.xml                 # Maven config

🚀 Getting Started

Prerequisites

  • Docker + Docker Compose (easiest path)
  • Or: Java 25 + Maven + Node 22 + MySQL 8 (local dev)

Option A — Docker Compose (recommended)

# 1. Clone
git clone https://github.com/RRimeKS/rentacarproject.git
cd rentacarproject

# 2. Configure env
cp .env.example .env
# Edit .env — set MYSQL_ROOT_PASSWORD, JWT_SECRET, etc.

# 3. Build & run
docker compose up --build

Visit:

Option B — Local dev

Backend:

./mvnw spring-boot:run        # port 8080

Frontend:

cd frontend
npm install
npm run dev                    # port 5173

Frontend dev server proxies /api/* to http://localhost:8080 automatically (see vite.config.js).

Create an admin user

Register a normal user via POST /auth/register, then elevate in MySQL:

UPDATE users SET role = 'ADMIN' WHERE email = 'you@example.com';

🔌 Core Endpoints

Auth

POST   /auth/register       Public
POST   /auth/login          Public (sets HttpOnly JWT cookie)
GET    /auth/me             Authenticated (returns current user)
POST   /auth/logout         Public (clears cookie)

Brands

GET    /brand/all                 Public
GET    /brand/detail/{id}         Public
POST   /brand/create              ADMIN
PUT    /brand/update/{id}         ADMIN
DELETE /brand/delete/{id}         ADMIN (soft delete, blocks if cars attached)

Cars

GET    /car/all                   Public
GET    /car/detail/{id}           Public
GET    /car/available             Public — query by pickupDate/returnDate
POST   /car/create                ADMIN
PUT    /car/{id}                  ADMIN
DELETE /car/{id}                  ADMIN (soft delete, blocks RESERVED/RENTED)

Reservations

POST   /reservation/create            Authenticated
GET    /reservation/my                Authenticated (own reservations)
GET    /reservation/all               ADMIN
GET    /reservation/{id}              Owner or ADMIN
POST   /reservation/{id}/cancel       Owner or ADMIN

🧠 Notable Architecture Patterns

  • Class-level @Transactional(readOnly = true) on every service; write methods override with @Transactional — enables Hibernate flush skipping on reads
  • @EntityGraph on repositories to avoid N+1 on join-heavy queries (e.g., Car → Brand)
  • Hibernate @SQLRestriction — all read queries auto-filter soft-deleted rows
  • JpaRepository method naming convention — no JPQL needed for simple filters (findAllByIsDeletedFalse, existsByBrandIdAndIsDeletedFalse)
  • Custom @Query with native SQL — for availability search with NOT EXISTS subquery
  • MapStruct with nullValuePropertyMappingStrategy = IGNORE — null fields in update requests don't overwrite existing entity fields
  • Frontend feature-based structure — each feature owns its API slice, injected into a single baseApi
  • RTK Query tag invalidation — cache stays coherent across related resources (e.g., creating a reservation invalidates Car LIST)

🔒 Security Highlights

  • JWT in HttpOnly + Secure + SameSite=Lax cookies (XSS-safe)
  • Cookie path / so protected endpoints receive it (not just /auth/*)
  • @PreAuthorize method-level guards (hasRole('ADMIN'), isAuthenticated())
  • Custom AccessDeniedHandler + AuthenticationEntryPoint → JSON error responses
  • Global GlobalExceptionHandler for validation, auth, and business errors
  • .env git-ignored — no secrets committed
  • Env-var driven config (${SPRING_DATASOURCE_URL:default})

🐳 Docker Notes

  • Multi-stage backend Dockerfile — JDK 25 builds .jar, JRE 25 runs it (~250MB final image)
  • Frontend multi-stage — Node 22 Alpine builds, Nginx Alpine serves (~30MB final image)
  • Backend connects to MySQL via service name db:3306 (Docker network)
  • Frontend Nginx proxies /api/*http://backend:8080
  • Healthcheck on MySQL + depends_on: service_healthy prevents boot-time race

📖 Learnings / Notes

This project was built as a learning exercise to explore enterprise Java patterns:

  • Why @Data is risky on JPA entities (→ @ToString.Exclude on collections)
  • @SQLRestriction vs manual filter methods — trade-offs
  • @SQLRestriction + restore-on-create conflicts (solved with native query)
  • MapStruct annotation processor setup alongside Lombok
  • Spring Security filter chain order + /error path exception
  • JPA bidirectional relations and the mappedBy side
  • Docker Compose env var precedence and volume persistence

📄 License

MIT — free to use, fork, learn from.