Intermediate

Full-stack Java Development with Spring Boot 3 and React

A job-portal app — Spring Boot 3 backend, JWT/OAuth2 auth, a React frontend and Docker CI/CD on AWS.

Demo project: Job Portal Application (Spring Boot backend + React frontend)


Table of Contents

  1. Architecture Overview
  2. Module 2 — Backend Configuration (Spring Boot 3)
  3. Module 3 — Authentication & Authorization with JWT and OAuth2
  4. Module 4 — React Frontend with State Management
  5. Module 5 — Deployment with Docker and CI/CD on AWS EC2
  6. Mermaid Diagrams
  7. Reference Tables

1. Architecture Overview

A full-stack system consists of two distinct applications:

  • Frontend (React): runs in the browser, handles user interactions and state
  • Backend (Spring Boot): runs on the server, handles API calls and database interactions

The backend exposes REST APIs that serve as the contact point with the frontend. Security is implemented via JWT Token and OAuth2. Database management uses Spring Data JPA with MySQL. Deployment is done via Docker on AWS EC2 with a GitHub Actions CI/CD pipeline.

The demo project is a Job Portal: users can register, log in (via credentials or Google OAuth), browse job listings, and apply.


2. Module 2 — Backend Configuration (Spring Boot 3)

2.1 Project Initialization with Spring Initializr

Access via start.spring.io. Configuration used in the course:

ParameterValue
ProjectMaven
LanguageJava 21
Spring Boot3.4.4
Groupcom.example
Artifactjobportal
PackagingJar

Dependencies added during initialization:

DependencyRole
Spring WebBuilding REST APIs
Spring Data JPADatabase operations via Hibernate
MySQL DriverMySQL connection
Spring DevToolsLive reload and better developer experience
Spring SecurityAPI authentication and authorization

2.2 Package Structure

src/main/java/com/example/jobportal/
├── JobportalApplication.java      ← Entry point @SpringBootApplication
├── config/
│   ├── SecurityConfig.java        ← Spring Security + CORS configuration
│   └── JwtAuthFilter.java         ← Custom JWT filter
├── controller/
│   ├── JobController.java         ← Endpoints /api/jobs
│   ├── ApplicationController.java ← Endpoints /api/applications
│   ├── LoginController.java       ← Endpoint /api/auth/login
│   └── OAuthController.java       ← Endpoints /oauth (Google)
├── service/
│   ├── JobService.java
│   └── ApplicationService.java
├── repository/
│   ├── JobRepository.java
│   ├── UserRepository.java
│   └── ApplicationRepository.java
├── model/
│   ├── Job.java                   ← @Entity table jobs
│   ├── User.java                  ← @Entity table users
│   └── Application.java
└── util/
    └── JwtUtil.java               ← JWT generation/validation

Important: All packages must be sub-packages of the base package (com.example.jobportal). Spring Boot uses component scanning starting from this base package. If classes are located outside of it, Spring will not detect them and auto-wiring will be broken.

2.3 JPA Entities — Data Models

Job Entity

package com.example.jobportal.model;

import java.time.LocalDate;
import jakarta.persistence.*;

@Entity
@Table(name = "jobs")
public class Job {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String description;

    @Column(nullable = false)
    private String company;

    @Column(nullable = false)
    private LocalDate postedDate = LocalDate.now(); // Auto-assigned to current date

    /* No Argument Constructor — required by JPA */
    public Job() {}

    /* Constructor for easy object creation */
    public Job(String title, String description, String company) {
        this.title = title;
        this.description = description;
        this.company = company;
    }

    // Getters and Setters...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public String getCompany() { return company; }
    public void setCompany(String company) { this.company = company; }
    public LocalDate getPostedDate() { return postedDate; }
    public void setPostedDate(LocalDate postedDate) { this.postedDate = postedDate; }
}

Key points:

  • @Entity: tells Hibernate this class is a JPA entity and will serve as a blueprint to create a table
  • @Table(name = "jobs"): best practice to explicitly name tables (preference for plural in databases)
  • @Id @GeneratedValue(strategy = GenerationType.IDENTITY): the database automatically generates the ID via AUTO_INCREMENT (MySQL)
  • @Column(nullable = false): NOT NULL constraint at the schema level
  • LocalDate.now(): date auto-assigned at object creation

User Entity

package com.example.jobportal.model;

import jakarta.persistence.*;

@Entity
@Table(name = "users") // "user" is a reserved word in MySQL
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true) // Uniqueness for login/identity
    private String email;

    @Column(nullable = true) // OAuth users don't have a password
    private String password;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private AuthProvider authProvider;

    public enum AuthProvider {
        LOCAL, GOOGLE
    }

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    public enum Role {
        APPLICANT, ADMIN
    }

    /* Constructors, Getters, Setters... */
    public User() {}

    // For regular users (with password)
    public User(String name, String email, String password,
                AuthProvider provider, Role role) { ... }

    // For Google users (without password)
    public User(String name, String email, AuthProvider provider, Role role) {
        this.password = null; // Google Users don't have a password
        ...
    }
}

Key points:

  • users as table name because user is a reserved word in many relational databases
  • email with unique = true: critical uniqueness constraint for login and identity
  • password nullable: supports OAuth users (Google) who don’t have a local password
  • @Enumerated(EnumType.STRING): stores enums as strings ("LOCAL", "GOOGLE", "APPLICANT", "ADMIN")

2.4 Spring Data JPA Repositories

package com.example.jobportal.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.example.jobportal.model.Job;

public interface JobRepository extends JpaRepository<Job, Long> {
    // JpaRepository already provides: save(), findAll(), findById(), deleteById()...
    // Derived methods can be added if necessary
}
public interface UserRepository extends JpaRepository<User, Long> {
    // Derived method: Spring automatically generates the SQL query
    Optional<User> findByEmail(String email);
}
public interface ApplicationRepository extends JpaRepository<Application, Long> {
}

Spring Data JPA automatically generates implementations of these interfaces. No more need to write SQL or manual DAO implementations. Methods like findByEmail() are resolved by naming convention.

2.5 Services — Business Layer

package com.example.jobportal.service;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.jobportal.model.Job;
import com.example.jobportal.repository.JobRepository;

@Service
public class JobService {

    @Autowired
    private JobRepository jobRepository;

    // Create a new Job
    public Job createJob(Job job) {
        return jobRepository.save(job); // save() method from JpaRepository
    }

    // Retrieve all jobs
    public List<Job> getAllJobs() {
        return jobRepository.findAll(); // findAll() method from JpaRepository
    }
}

Principle: The Controller calls the Service layer, which does all the heavy lifting. The Controller focuses solely on reading data and sending the response.

2.6 REST Controllers

package com.example.jobportal.controller;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.example.jobportal.model.Job;
import com.example.jobportal.service.JobService;

@RestController
@RequestMapping("/api")
public class JobController {

    @Autowired
    private JobService jobService;

    // POST /api/jobs — Create a job (ADMIN only)
    @PostMapping("/jobs")
    public ResponseEntity<Job> createJob(@RequestBody Job job) {
        return ResponseEntity.ok(jobService.createJob(job));
    }

    // GET /api/jobs/all — Retrieve all jobs (public)
    @GetMapping("/jobs/all")
    public ResponseEntity<List<Job>> getAllJobs() {
        return ResponseEntity.ok(jobService.getAllJobs());
    }
}

Key points:

  • @RestController = @Controller + @ResponseBody: automatically returns JSON
  • @RequestMapping("/api"): base prefix for all endpoints
  • @RequestBody: Spring Boot uses Jackson (included by default) to automatically convert the JSON body to a Java object
  • ResponseEntity<T>: allows controlling the returned HTTP code

2.7 MySQL Database Configuration

src/main/resources/application.properties:

spring.datasource.url=jdbc:mysql://localhost:3306/jobportal
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# Google OAuth (store in environment variables in production)
spring.security.oauth2.client.registration.google.client-id=YOUR_CLIENT_ID
spring.security.oauth2.client.registration.google.client-secret=YOUR_CLIENT_SECRET

Production: Never commit application.properties with credentials to Git. Use environment variables.


3. Module 3 — Authentication & Authorization with JWT and OAuth2

3.1 Introduction: HTTP Basic vs JWT

AspectHTTP Basic AuthJWT
Credentials sentOn every requestOnly once (login)
SecurityRisky (credentials on the network)Token in the header
ExpirationNoControllable
MetadataNoYes (roles, claims)
StatelessNoYes

JWT Flow in 5 steps:

  1. Create a JWT token with HMAC/RSA algorithm
  2. User logs in → API verifies credentials → returns the JWT
  3. Each API request: custom filter extracts and validates the token from the header
  4. This filter is registered in Spring Security before internal mechanisms
  5. The client uses this token as proof of identity for all secured requests

3.2 JwtUtil — Token Generation and Validation

package com.example.jobportal.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtUtil {

    // In production: store in an environment variable
    private static final String SECRET_KEY = "APP_SECRET_KEY_987654321098765432109876543210";
    private static final long EXPIRATION_TIME = 86400000; // 1 day (in milliseconds)

    private Key getSigningKey() {
        return Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
    }

    // Generate the JWT Token for the logged-in user
    public String generateToken(String email) {
        return Jwts.builder()
                .setSubject(email)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // Extract email from the JWT Token
    public String extractEmail(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    // Validate the JWT Token
    public boolean validateToken(String token, String email) {
        return extractEmail(token).equals(email);
    }
}

JWT dependencies to add in pom.xml:

<!-- Main JWT classes (JWTBuilder, JWTS) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<!-- Runtime implementation -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<!-- Jackson support for JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

3.3 Login API

package com.example.jobportal.controller;

import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.example.jobportal.util.JwtUtil;

import java.security.Principal;

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

    @Autowired
    private JwtUtil jwtUtil;

    // POST /api/auth/login — Generates a JWT Token after Basic authentication
    @PostMapping("/login")
    public Map<String, String> login(Principal principal) {
        // Principal is provided by Spring Security after credential verification
        String token = jwtUtil.generateToken(principal.getName());
        Map<String, String> response = new HashMap<>();
        response.put("token", token);
        return response;
    }

    // GET /api/auth/details — Returns the current user's username and roles
    @GetMapping("/details")
    public Map<String, Object> getUserDetails(Principal principal) {
        // Extracts username and roles from the SecurityContext
        // ...
    }
}

Testing flow with Postman:

  1. POST /api/auth/login with Authorization header in Basic Auth (username:password)
  2. Retrieve the JWT token from the response
  3. For subsequent requests, add Authorization: Bearer <token> in headers

3.4 JwtAuthFilter — Validation Filter

package com.example.jobportal.config;

import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.example.jobportal.util.JwtUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.*;

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        // 1. Check for the Authorization header
        final String authHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;

        // 2. Extract the token from "Bearer <token>" header
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
            try {
                username = jwtUtil.extractEmail(token);
            } catch (Exception e) {
                System.out.println("Invalid token: " + e.getMessage());
            }
        }

        // 3. Validate the token if username was extracted and user isn't authenticated yet
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(token, userDetails.getUsername())) {
                // 4. Configure authentication in Spring's SecurityContext
                UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Critical point: The filter verifies that the JWT token was indeed issued for this particular user: the token is generated with the username at login, and when the token is passed back in a request, the username is extracted and the match is verified.

3.5 SecurityConfig — Spring Security Configuration

package com.example.jobportal.config;

import java.util.Arrays;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.*;

@Configuration
public class SecurityConfig {

    // In-memory users (replace with a real database in production)
    @Bean
    UserDetailsService userDetailsService() {
        UserDetails admin = User.withUsername("admin")
                .password("{noop}admin123")
                .roles("ADMIN")
                .build();
        UserDetails applicant = User.withUsername("user")
                .password("{noop}user123")
                .roles("APPLICANT")
                .build();
        return new InMemoryUserDetailsManager(admin, applicant);
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter)
            throws Exception {
        return http
                .cors(Customizer.withDefaults())
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                    .requestMatchers(HttpMethod.POST, "/api/jobs").hasRole("ADMIN")
                    .requestMatchers(HttpMethod.GET, "/api/jobs/all").permitAll()
                    .requestMatchers(HttpMethod.POST, "/api/application/apply/**").hasRole("APPLICANT")
                    .requestMatchers(HttpMethod.POST, "/api/auth/login").authenticated()
                    .requestMatchers("/oauth/**", "/login/**", "/oauth2/**", "/api/oauth/**").permitAll()
                    .requestMatchers(HttpMethod.GET, "/api/auth/details").permitAll()
                    .anyRequest().authenticated()
                )
                // Place the JWT filter BEFORE Spring's internal filter
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
                .httpBasic(Customizer.withDefaults())
                // OAuth2 configuration with default APPLICANT role
                .oauth2Login(oauth -> oauth
                    .userInfoEndpoint(userInfo -> userInfo
                        .userService(userRequest -> {
                            var delegate = new DefaultOAuth2UserService();
                            var oauth2User = delegate.loadUser(userRequest);
                            // Assign ROLE_APPLICANT to all OAuth users
                            return new DefaultOAuth2User(
                                List.of(new SimpleGrantedAuthority("ROLE_APPLICANT")),
                                oauth2User.getAttributes(),
                                "email"
                            );
                        })
                    )
                )
                .build();
    }

    // Global CORS configuration (preferred over @CrossOrigin per controller)
    @Bean
    UrlBasedCorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("http://localhost:5173")); // React frontend URL
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setExposedHeaders(Arrays.asList("Authorization")); // Expose the Authorization header
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

Configured security rules:

EndpointMethodRequired Role
/api/jobsPOSTADMIN
/api/jobs/allGETPublic
/api/application/apply/**POSTAPPLICANT
/api/auth/loginPOSTAuthenticated (Basic Auth)
/oauth/**, /oauth2/**AllPublic
/api/auth/detailsGETPublic
Everything elseAuthenticated

3.6 OAuth2 with Google

Implementation steps:

  1. Register the application on Google Developer Console

    • Create a “Web application” type project
    • Add redirect URIs: http://localhost:5173 (React) and http://localhost:8080 (backend)
    • Obtain client-id and client-secret
  2. Additional Maven dependencies:

<!-- Spring internal OAuth2 support -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
  1. OAuthController — Two key endpoints:
@RestController
@RequestMapping("/api/oauth")
public class OAuthController {

    // Exchanges the Google authorization code for an access token
    @PostMapping("/exchange-token")
    public Map<String, String> exchangeToken(@RequestParam String code) {
        // Server-to-server call to Google to exchange the code for a token
        // Parameters: client_id, client_secret, code, redirect_uri, grant_type
        ...
    }

    // Validates the Google token and returns user details
    @GetMapping("/user-details")
    public Map<String, Object> getUserDetails(@RequestParam String token) {
        // Call to the Google API to verify the token and retrieve user info
        ...
    }
}

OAuth2 Flow (5 steps):

  1. App registers with Google → gets ClientID and client_secret
  2. User clicks “Sign in with Google” → redirect to Google login page
  3. After login, Google returns an authorization code (not directly a token)
  4. App sends this code to Google to obtain an access token
  5. This token is used to access secured APIs; Spring handles verification

3.7 CORS Configuration

Global approach (recommended) in SecurityConfig:

@Bean
UrlBasedCorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
    config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(Arrays.asList("*"));
    config.setExposedHeaders(Arrays.asList("Authorization")); // Critical for sending the token
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

Local approach (per controller) — less recommended:

@CrossOrigin(origins = "http://localhost:5173")
@RestController
public class JobController { ... }

Why prefer global config: full control over headers, methods, and exposed headers; centralized change if the frontend port changes; avoids duplication across every controller.


4. Module 4 — React Frontend with State Management

4.1 Project Creation with Vite

# Create the React project with Vite
npm create vite@latest my-job-portal -- --template react

# Navigate to the directory
cd my-job-portal

# Install essential dependencies
npm install react-router-dom bootstrap axios

# Install Redux Toolkit and React Redux
npm install @reduxjs/toolkit react-redux

# Start the development server
npm run dev

React project structure:

src/
├── App.jsx                    ← Root component + Router setup
├── main.jsx                   ← Entry point, Redux Provider
├── config/
│   └── backend.js             ← Backend URL (BACKEND_API_URL)
├── components/
│   ├── Login.jsx
│   ├── Navbar.jsx
│   ├── ApplicantDashboard.jsx
│   ├── AdminDashboard.jsx
│   └── OAuthLogin.jsx         ← OAuth2 redirect handling
└── store/
    ├── store.js               ← Redux store configuration
    ├── userActions.js         ← Redux actions
    ├── userReducer.js         ← Redux reducer
    └── userSelectors.js       ← Redux selectors

src/config/backend.js:

// In local development
export const BACKEND_API_URL = "http://localhost:8080";
// In production (EC2): replace with the public IP of the instance
// export const BACKEND_API_URL = "http://<EC2_PUBLIC_IP>:8080";

src/main.jsx — Redux store registration:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { Provider } from 'react-redux'
import store from './store/store.js'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.min.js'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>  {/* Makes the store available throughout the app */}
      <App />
    </Provider>
  </React.StrictMode>
)

4.2 Login Component

// src/components/Login.jsx
import axios from "axios";
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { setUserDetails } from "../store/userActions";
import { useNavigate } from "react-router-dom";
import { BACKEND_API_URL } from '../config/backend';

const Login = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const processLogin = async () => {
    try {
      // 1. Login API call with Basic Auth (credentials encoded in Base64)
      const response = await axios.post(BACKEND_API_URL + '/api/auth/login', {}, {
        headers: {
          "Authorization": "Basic " + window.btoa(username + ":" + password)
        }
      });
      
      let token = response.data.token;
      // 2. Save the token in localStorage
      localStorage.setItem('token', token);

      // 3. Call the user details API with the Bearer token
      const resp = await axios.get(BACKEND_API_URL + '/api/auth/details', {
        headers: {
          "Authorization": "Bearer " + token
        }
      });

      // 4. Build the user object
      let user = {
        role: resp.data.roles[0],
        username: resp.data.username
      };

      // 5. Dispatch to the Redux store
      setUserDetails(dispatch)(user);

      // 6. Navigate to the appropriate dashboard based on role
      switch (user.role) {
        case "ROLE_APPLICANT":
          navigate("/applicant-dashboard");
          break;
        case "ROLE_ADMIN":
          navigate("/admin-dashboard");
          break;
        default:
          console.log("Invalid role");
      }
    } catch (error) {
      alert('Invalid credentials');
    }
  };

  return (
    <div className="container" style={{ marginTop: '8%' }}>
      <div className="row">
        <div className="col-lg-6 offset-sm-3">
          <div className="card shadow p-4">
            <div className="card-header bg-white text-center">
              <h3>Login to Job Portal</h3>
            </div>
            <div className="card-body">
              <form onSubmit={(e) => { e.preventDefault(); processLogin(); }}>
                <div className="mb-3">
                  <label>Username</label>
                  <input type="text" className="form-control"
                    value={username}
                    onChange={(e) => setUsername(e.target.value)} />
                </div>
                <div className="mb-3">
                  <label>Password</label>
                  <input type="password" className="form-control"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)} />
                </div>
                <button type="submit" className="btn btn-primary w-100">Login</button>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Login;

4.3 Redux — Global State Management

Redux data flow:

React Component → Action → Reducer → Redux Store → useSelector → React Component

src/store/store.js:

import { createStore } from 'redux';
import userReducer from './userReducer';
// Or with Redux Toolkit:
// import { configureStore } from '@reduxjs/toolkit';

const store = createStore(userReducer);
export default store;

src/store/userActions.js:

// Action creator: dispatches user details to the Reducer
export const setUserDetails = (dispatch) => (user) => {
    dispatch({
        type: 'SET_USER_DETAILS',
        payload: user
    });
};

// Action to reset (logout)
export const clearUserDetails = (dispatch) => () => {
    dispatch({
        type: 'SET_USER_DETAILS',
        payload: { username: '', role: '' }
    });
};

src/store/userReducer.js:

// Initial state
const initialState = {
    username: '',
    role: ''
};

const userReducer = (state = initialState, action) => {
    switch (action.type) {
        case 'SET_USER_DETAILS':
            return {
                ...state,
                username: action.payload.username,
                role: action.payload.role
            };
        default:
            return state;
    }
};

export default userReducer;

src/store/userSelectors.js:

// Selector: provides clean access to store data
export const selectUser = (state) => state;

Usage in a component with useSelector:

import { useSelector } from 'react-redux';
import { selectUser } from '../store/userSelectors';

const Navbar = () => {
    const user = useSelector(selectUser); // Access to the Redux store
    
    return (
        <nav className="navbar navbar-dark bg-dark">
            <span className="navbar-text text-white">
                Welcome, {user.username}!
            </span>
        </nav>
    );
};

4.4 ApplicantDashboard — API Integration

// src/components/ApplicantDashboard.jsx
import { useEffect, useState } from "react";
import Navbar from "./Navbar";
import axios from "axios";
import { BACKEND_API_URL } from '../config/backend';

const ApplicantDashboard = () => {
    const [jobs, setJobs] = useState([]); // State for the job list

    // Load jobs on component mount
    useEffect(() => {
        getAllJobs();
    }, []);

    // GET API call for all jobs
    const getAllJobs = async () => {
        try {
            const response = await axios.get(BACKEND_API_URL + "/api/jobs/all");
            setJobs(response.data);
        } catch (error) {
            console.log("Error fetching jobs:", error);
        }
    };

    // Apply to a job — Sends the Bearer token in the header
    const apply = async (jobId) => {
        try {
            await axios.post(
                BACKEND_API_URL + "/api/applications/apply/" + jobId,
                {},
                {
                    headers: {
                        "Authorization": "Bearer " + localStorage.getItem("token")
                    }
                }
            );
            alert("Application Success!!!");
        } catch (error) {
            console.log(error);
        }
    };

    return (
        <>
            <Navbar />
            <h1>Applicant Dashboard</h1>
            <div className="container">
                <div className="row">
                    {jobs.map((job, index) => (
                        <div className="col-sm-4" key={index}>
                            <div className="card mb-4">
                                <div className="card-body">
                                    <h4>Title: {job.title}</h4>
                                    <p>Details: {job.description}</p>
                                    <p>Company: {job.company}</p>
                                    <p>Posted Date: {job.postedDate}</p>
                                    <button
                                        className="btn btn-primary"
                                        onClick={() => apply(job.id)}>
                                        Apply
                                    </button>
                                </div>
                            </div>
                        </div>
                    ))}
                </div>
            </div>
        </>
    );
};

export default ApplicantDashboard;

Navbar component with Logout:

// src/components/Navbar.jsx
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { selectUser } from '../store/userSelectors';
import { clearUserDetails } from '../store/userActions';

const Navbar = () => {
    const user = useSelector(selectUser);
    const dispatch = useDispatch();
    const navigate = useNavigate();

    const handleLogout = () => {
        // 1. Remove the token from localStorage
        localStorage.removeItem('token');
        // 2. Reset the Redux store (no trace of the previous user)
        clearUserDetails(dispatch)();
        // 3. Redirect to login
        navigate('/');
    };

    return (
        <nav className="navbar navbar-dark bg-dark px-3">
            <span className="navbar-brand">Job Portal</span>
            <span className="navbar-text text-white">Welcome, {user.username}!</span>
            <button className="btn btn-outline-light" onClick={handleLogout}>
                Logout
            </button>
        </nav>
    );
};

export default Navbar;

4.5 OAuth2 on the React Side

// src/components/OAuthLogin.jsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import axios from 'axios';
import { setUserDetails } from '../store/userActions';
import { BACKEND_API_URL } from '../config/backend';

const OAuthLogin = () => {
    const navigate = useNavigate();
    const dispatch = useDispatch();

    useEffect(() => {
        const handleOAuth = async () => {
            // 1. Read the authorization code that Google provided in the URL
            const urlParams = new URLSearchParams(window.location.search);
            const code = urlParams.get('code');

            if (code) {
                try {
                    // 2. Exchange the code for a token via our backend
                    const tokenResponse = await axios.post(
                        BACKEND_API_URL + '/api/oauth/exchange-token',
                        { code }
                    );
                    const token = tokenResponse.data.token;

                    // 3. Retrieve user details from Google
                    const userResponse = await axios.get(
                        BACKEND_API_URL + '/api/oauth/user-details',
                        { params: { token } }
                    );

                    // 4. Assign APPLICANT role by default to OAuth users
                    const user = {
                        username: userResponse.data.email,
                        role: 'ROLE_APPLICANT'
                    };

                    // 5. Save in Redux store
                    setUserDetails(dispatch)(user);

                    // 6. Redirect to Applicant Dashboard
                    navigate('/applicant-dashboard');
                } catch (error) {
                    console.log('OAuth error:', error);
                }
            }
        };

        handleOAuth();
    }, []);

    return <div>Processing OAuth login...</div>;
};

export default OAuthLogin;

src/App.jsx — Router Configuration:

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './components/Login';
import ApplicantDashboard from './components/ApplicantDashboard';
import AdminDashboard from './components/AdminDashboard';
import OAuthLogin from './components/OAuthLogin';

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/" element={<Login />} />
                <Route path="/applicant-dashboard" element={<ApplicantDashboard />} />
                <Route path="/admin-dashboard" element={<AdminDashboard />} />
                <Route path="/oauth-login" element={<OAuthLogin />} />
            </Routes>
        </BrowserRouter>
    );
}

export default App;

5. Module 5 — Deployment with Docker and CI/CD on AWS EC2

5.1 Dockerize the Backend

Prerequisite: Migrate the database to Amazon RDS

  1. Create a MySQL RDS instance on AWS (Free Tier option)
  2. Copy the Endpoint URL and update application.properties
  3. Configure inbound rules to allow port 3306

Generate the JAR:

cd job_portal_backend
./mvnw clean package -DskipTests
# Generates: target/jobportal-0.0.1-SNAPSHOT.jar

Backend Dockerfile:

FROM eclipse-temurin:21-jdk-alpine
VOLUME /tmp
COPY target/jobportal-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

Build and push the Docker image:

# Build the image
docker build -t <dockerhub_username>/jobportal-backend .

# Test locally
docker run -p 8080:8080 \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://<RDS_ENDPOINT>:3306/jobportal \
  -e SPRING_DATASOURCE_USERNAME=<db_user> \
  -e SPRING_DATASOURCE_PASSWORD=<db_pass> \
  <dockerhub_username>/jobportal-backend

# Push to Docker Hub
docker push <dockerhub_username>/jobportal-backend

5.2 Dockerize the React Frontend

src/config/backend.js — Point to EC2:

export const BACKEND_API_URL = "http://<EC2_PUBLIC_IP>:8080";

Check vite.config.js:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  base: '/', // Important for serving from the root directory
});

Frontend Dockerfile:

FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Build and deployment:

# Build and local test
docker build -t <dockerhub_username>/jobportal-frontend .
docker run -p 5173:80 <dockerhub_username>/jobportal-frontend

# Push to Docker Hub
docker push <dockerhub_username>/jobportal-frontend

Deploy to EC2:

# SSH to the EC2 instance
ssh -i "keypair.pem" ec2-user@<EC2_PUBLIC_IP>

# Install Docker
sudo yum update -y
sudo yum install docker -y
sudo service docker start

# Pull and run containers
docker pull <dockerhub_username>/jobportal-backend
docker run -d -p 8080:8080 \
  -e SPRING_DATASOURCE_URL=... \
  <dockerhub_username>/jobportal-backend

docker pull <dockerhub_username>/jobportal-frontend
docker run -d -p 5173:80 <dockerhub_username>/jobportal-frontend

5.3 CI/CD with GitHub Actions

.github/workflows/docker-build.yaml:

name: Build and Push Docker Image

on:
  push:
    branches: [ main ] # Triggers on every push to main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # 1. Checkout source code
      - name: Checkout code
        uses: actions/checkout@v3

      # 2. Set up JDK 21
      - name: Set up JDK 21
        uses: actions/setup-java@v3
        with:
          java-version: '21'
          distribution: 'temurin'

      # 3. Build the Maven JAR
      - name: Build with Maven
        run: mvn clean package -DskipTests

      # 4. Login to Docker Hub (uses GitHub secrets)
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # 5. Build the Docker image
      - name: Build Docker image
        run: docker build -t ${{ secrets.DOCKER_USERNAME }}/jobportal-backend .

      # 6. Push to Docker Hub
      - name: Push Docker image
        run: docker push ${{ secrets.DOCKER_USERNAME }}/jobportal-backend

GitHub secrets configuration:

In the GitHub repository → Settings → Security → Secrets and variables → Actions:

  • DOCKER_USERNAME: your Docker Hub username
  • DOCKER_PASSWORD: Personal Access Token generated from Docker Hub (Account Settings)

CI/CD advantage: On every git push to main, GitHub Actions automatically rebuilds the Docker image and pushes it to Docker Hub. No more manual builds needed.


6. Mermaid Diagrams

6.1 Full-stack General Architecture

graph TB
    subgraph Frontend["Frontend — React (Vite)"]
        Browser["Browser"]
        Login["Login.jsx"]
        Dashboard["ApplicantDashboard.jsx"]
        Redux["Redux Store\n(Global State)"]
        OAuthComp["OAuthLogin.jsx"]
    end

    subgraph Backend["Backend — Spring Boot 3"]
        SC["SecurityConfig\n(JWT + OAuth2)"]
        Filter["JwtAuthFilter"]
        JobCtrl["JobController\n/api/jobs"]
        LoginCtrl["LoginController\n/api/auth/login"]
        OAuthCtrl["OAuthController\n/api/oauth"]
        JobSvc["JobService"]
        JobRepo["JobRepository\n(JpaRepository)"]
    end

    subgraph Database["Database"]
        MySQL[(MySQL / Amazon RDS)]
    end

    subgraph External["External Services"]
        Google["Google OAuth2"]
        DockerHub["Docker Hub"]
        EC2["AWS EC2"]
    end

    Browser -->|"HTTP Requests"| SC
    SC --> Filter
    Filter --> LoginCtrl
    Filter --> JobCtrl
    Filter --> OAuthCtrl
    JobCtrl --> JobSvc
    JobSvc --> JobRepo
    JobRepo --> MySQL
    OAuthCtrl <-->|"Token Exchange"| Google
    Login --> Redux
    Dashboard --> Redux

6.2 REST API Flow

sequenceDiagram
    participant C as Client (React)
    participant CF as JwtAuthFilter
    participant SC as SecurityConfig
    participant Ctrl as JobController
    participant Svc as JobService
    participant Repo as JobRepository
    participant DB as MySQL

    C->>CF: GET /api/jobs/all
    CF->>CF: Check Authorization header
    CF->>SC: No token → check permitAll() rule
    SC->>Ctrl: Access granted (public)
    Ctrl->>Svc: getAllJobs()
    Svc->>Repo: findAll()
    Repo->>DB: SELECT * FROM jobs
    DB-->>Repo: List<Job>
    Repo-->>Svc: List<Job>
    Svc-->>Ctrl: List<Job>
    Ctrl-->>C: 200 OK [{"id":1,"title":"Dev",...}]

6.3 JWT Authentication Flow

sequenceDiagram
    participant C as Client (React)
    participant API as LoginController
    participant JU as JwtUtil
    participant Filter as JwtAuthFilter
    participant SC as SecurityContext

    Note over C,API: Step 1 — Login with Basic Auth
    C->>API: POST /api/auth/login\nAuthorization: Basic base64(user:pass)
    API->>JU: generateToken(email)
    JU-->>API: JWT Token (HS256)
    API-->>C: {"token": "eyJhbGc..."}

    Note over C: localStorage.setItem('token', jwt)

    Note over C,SC: Step 2 — Secured request with JWT
    C->>Filter: POST /api/jobs\nAuthorization: Bearer eyJhbGc...
    Filter->>JU: extractEmail(token)
    JU-->>Filter: "admin@company.com"
    Filter->>JU: validateToken(token, email)
    JU-->>Filter: true
    Filter->>SC: setAuthentication(user, roles)
    SC-->>C: 200 OK — Access granted

6.4 Google OAuth2 Flow

sequenceDiagram
    participant U as User
    participant R as React App
    participant B as Spring Boot Backend
    participant G as Google OAuth2

    U->>R: Clicks "Sign in with Google"
    R->>G: Redirect to Google login page
    U->>G: Enters Google credentials
    G-->>R: Redirect with authorization_code
    R->>B: POST /api/oauth/exchange-token\n{code: "auth_code"}
    B->>G: Exchange code for access_token\n(client_id, client_secret, code)
    G-->>B: access_token
    B-->>R: {"token": "access_token"}
    R->>B: GET /api/oauth/user-details?token=...
    B->>G: Verify token and retrieve user info
    G-->>B: {email, name, ...}
    B-->>R: User details
    R->>R: Assign APPLICANT role\nSave in Redux
    R->>U: Redirect to /applicant-dashboard

6.5 JPA/Hibernate Model

erDiagram
    JOB {
        Long id PK
        String title "NOT NULL"
        String description "NOT NULL"
        String company "NOT NULL"
        LocalDate postedDate "DEFAULT NOW()"
    }

    USER {
        Long id PK
        String name "NOT NULL"
        String email "NOT NULL UNIQUE"
        String password "NULLABLE"
        AuthProvider authProvider "LOCAL | GOOGLE"
        Role role "APPLICANT | ADMIN"
    }

    APPLICATION {
        Long id PK
        Long jobId FK
        String applicantEmail FK
        LocalDate appliedDate
    }

    USER ||--o{ APPLICATION : "submits"
    JOB ||--o{ APPLICATION : "receives"

7. Reference Tables

7.1 Essential Spring Boot Annotations

AnnotationPackageDescription
@SpringBootApplicationSpring BootEntry point — combines @Configuration, @EnableAutoConfiguration, @ComponentScan
@RestControllerSpring MVCCombines @Controller + @ResponseBody — returns JSON automatically
@RequestMappingSpring MVCBase prefix for controller endpoints
@GetMappingSpring MVCShortcut for @RequestMapping(method=GET)
@PostMappingSpring MVCShortcut for @RequestMapping(method=POST)
@RequestBodySpring MVCConverts the JSON body to a Java object (via Jackson)
@PathVariableSpring MVCExtracts a variable from the URL path (/jobs/{id})
@RequestParamSpring MVCExtracts a query string parameter (?token=xyz)
@ServiceSpringMarks as a service layer component
@RepositorySpringMarks as a data access layer component
@AutowiredSpringAutomatic dependency injection
@ComponentSpringGeneric Spring component (e.g.: JwtUtil, JwtAuthFilter)
@ConfigurationSpringConfiguration class (beans)
@BeanSpringDeclares a Spring bean in a @Configuration class
@EntityJPAMarks the class as a JPA entity → database table
@Table(name="")JPAExplicit table name
@IdJPAMarks the primary key
@GeneratedValueJPAID generation strategy (IDENTITY = auto_increment MySQL)
@ColumnJPACustomizes the column (nullable, unique, length…)
@Enumerated(STRING)JPAStores enums as strings
@CrossOriginSpring MVCEnables CORS at the controller level

7.2 Job Portal REST Endpoints

EndpointMethodAccessDescription
/api/auth/loginPOSTAuthenticated (Basic)Generates a JWT token
/api/auth/detailsGETPublicReturns current user’s username and roles
/api/jobsPOSTADMINCreate a job listing
/api/jobs/allGETPublicList all listings
/api/applications/apply/{jobId}POSTAPPLICANTApply for a listing
/oauth2/authorization/googleGETPublicInitiate the Google OAuth2 flow
/api/oauth/exchange-tokenPOSTPublicExchange the OAuth code for a token
/api/oauth/user-detailsGETPublicRetrieve Google user details

7.3 Key Maven Dependencies (pom.xml)

<dependencies>
    <!-- Web — Building REST APIs -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- JPA — Database operations via Hibernate -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- Security — Authentication and authorization -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- OAuth2 — Google OAuth support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>

    <!-- MySQL Driver -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- DevTools — Live reload -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>

    <!-- JWT — Token generation and validation -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

7.4 Essential React npm Packages

PackageCommandRole
react-router-domnpm install react-router-domSPA navigation and routing
bootstrapnpm install bootstrapQuick CSS styling
axiosnpm install axiosHTTP client (cleaner than fetch)
@reduxjs/toolkitnpm install @reduxjs/toolkitSimplified, less verbose Redux
react-reduxnpm install react-reduxBridge between Redux and React

7.5 React Hooks Used in the Course

HookImportUsage
useStatereactComponent local state (username, password, jobs…)
useEffectreactSide effects (loading data on mount)
useDispatchreact-reduxDispatch actions to the Redux store
useSelectorreact-reduxRead data from the Redux store
useNavigatereact-router-domProgrammatic navigation

Course summary: This course covers the complete modern full-stack development cycle — from configuring the Spring Boot 3 backend with JPA and MySQL, to securing via JWT and Google OAuth2, building a React SPA with Redux for state management, all the way to containerized deployment with Docker on AWS EC2 and automation via GitHub Actions CI/CD.


Search Terms

full-stack · java · development · spring · boot · react · backend · architecture · web · configuration · oauth2 · api · flow · jpa · jwt · rest · authentication · ci/cd · data · dockerize · entity · essential · frontend · google

Interested in this course?

Contact us to book it or get a custom training plan for your team.