Demo project: Job Portal Application (Spring Boot backend + React frontend)
Table of Contents
- Architecture Overview
- Module 2 — Backend Configuration (Spring Boot 3)
- Module 3 — Authentication & Authorization with JWT and OAuth2
- Module 4 — React Frontend with State Management
- Module 5 — Deployment with Docker and CI/CD on AWS EC2
- Mermaid Diagrams
- 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:
| Parameter | Value |
|---|---|
| Project | Maven |
| Language | Java 21 |
| Spring Boot | 3.4.4 |
| Group | com.example |
| Artifact | jobportal |
| Packaging | Jar |
Dependencies added during initialization:
| Dependency | Role |
|---|---|
| Spring Web | Building REST APIs |
| Spring Data JPA | Database operations via Hibernate |
| MySQL Driver | MySQL connection |
| Spring DevTools | Live reload and better developer experience |
| Spring Security | API 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 viaAUTO_INCREMENT(MySQL)@Column(nullable = false):NOT NULLconstraint at the schema levelLocalDate.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:
usersas table name becauseuseris a reserved word in many relational databasesemailwithunique = true: critical uniqueness constraint for login and identitypasswordnullable: 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 objectResponseEntity<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.propertieswith credentials to Git. Use environment variables.
3. Module 3 — Authentication & Authorization with JWT and OAuth2
3.1 Introduction: HTTP Basic vs JWT
| Aspect | HTTP Basic Auth | JWT |
|---|---|---|
| Credentials sent | On every request | Only once (login) |
| Security | Risky (credentials on the network) | Token in the header |
| Expiration | No | Controllable |
| Metadata | No | Yes (roles, claims) |
| Stateless | No | Yes |
JWT Flow in 5 steps:
- Create a JWT token with HMAC/RSA algorithm
- User logs in → API verifies credentials → returns the JWT
- Each API request: custom filter extracts and validates the token from the header
- This filter is registered in Spring Security before internal mechanisms
- 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:
POST /api/auth/loginwith Authorization header in Basic Auth (username:password)- Retrieve the JWT token from the response
- 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:
| Endpoint | Method | Required Role |
|---|---|---|
/api/jobs | POST | ADMIN |
/api/jobs/all | GET | Public |
/api/application/apply/** | POST | APPLICANT |
/api/auth/login | POST | Authenticated (Basic Auth) |
/oauth/**, /oauth2/** | All | Public |
/api/auth/details | GET | Public |
| Everything else | Authenticated |
3.6 OAuth2 with Google
Implementation steps:
-
Register the application on Google Developer Console
- Create a “Web application” type project
- Add redirect URIs:
http://localhost:5173(React) andhttp://localhost:8080(backend) - Obtain
client-idandclient-secret
-
Additional Maven dependencies:
<!-- Spring internal OAuth2 support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
- 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):
- App registers with Google → gets
ClientIDandclient_secret - User clicks “Sign in with Google” → redirect to Google login page
- After login, Google returns an authorization code (not directly a token)
- App sends this code to Google to obtain an access token
- 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
- Create a MySQL RDS instance on AWS (Free Tier option)
- Copy the Endpoint URL and update
application.properties - 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 usernameDOCKER_PASSWORD: Personal Access Token generated from Docker Hub (Account Settings)
CI/CD advantage: On every
git pushtomain, 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
| Annotation | Package | Description |
|---|---|---|
@SpringBootApplication | Spring Boot | Entry point — combines @Configuration, @EnableAutoConfiguration, @ComponentScan |
@RestController | Spring MVC | Combines @Controller + @ResponseBody — returns JSON automatically |
@RequestMapping | Spring MVC | Base prefix for controller endpoints |
@GetMapping | Spring MVC | Shortcut for @RequestMapping(method=GET) |
@PostMapping | Spring MVC | Shortcut for @RequestMapping(method=POST) |
@RequestBody | Spring MVC | Converts the JSON body to a Java object (via Jackson) |
@PathVariable | Spring MVC | Extracts a variable from the URL path (/jobs/{id}) |
@RequestParam | Spring MVC | Extracts a query string parameter (?token=xyz) |
@Service | Spring | Marks as a service layer component |
@Repository | Spring | Marks as a data access layer component |
@Autowired | Spring | Automatic dependency injection |
@Component | Spring | Generic Spring component (e.g.: JwtUtil, JwtAuthFilter) |
@Configuration | Spring | Configuration class (beans) |
@Bean | Spring | Declares a Spring bean in a @Configuration class |
@Entity | JPA | Marks the class as a JPA entity → database table |
@Table(name="") | JPA | Explicit table name |
@Id | JPA | Marks the primary key |
@GeneratedValue | JPA | ID generation strategy (IDENTITY = auto_increment MySQL) |
@Column | JPA | Customizes the column (nullable, unique, length…) |
@Enumerated(STRING) | JPA | Stores enums as strings |
@CrossOrigin | Spring MVC | Enables CORS at the controller level |
7.2 Job Portal REST Endpoints
| Endpoint | Method | Access | Description |
|---|---|---|---|
/api/auth/login | POST | Authenticated (Basic) | Generates a JWT token |
/api/auth/details | GET | Public | Returns current user’s username and roles |
/api/jobs | POST | ADMIN | Create a job listing |
/api/jobs/all | GET | Public | List all listings |
/api/applications/apply/{jobId} | POST | APPLICANT | Apply for a listing |
/oauth2/authorization/google | GET | Public | Initiate the Google OAuth2 flow |
/api/oauth/exchange-token | POST | Public | Exchange the OAuth code for a token |
/api/oauth/user-details | GET | Public | Retrieve 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
| Package | Command | Role |
|---|---|---|
react-router-dom | npm install react-router-dom | SPA navigation and routing |
bootstrap | npm install bootstrap | Quick CSS styling |
axios | npm install axios | HTTP client (cleaner than fetch) |
@reduxjs/toolkit | npm install @reduxjs/toolkit | Simplified, less verbose Redux |
react-redux | npm install react-redux | Bridge between Redux and React |
7.5 React Hooks Used in the Course
| Hook | Import | Usage |
|---|---|---|
useState | react | Component local state (username, password, jobs…) |
useEffect | react | Side effects (loading data on mount) |
useDispatch | react-redux | Dispatch actions to the Redux store |
useSelector | react-redux | Read data from the Redux store |
useNavigate | react-router-dom | Programmatic 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