In this Workshop3, we will try to build Admin Dashboard and also user login based on roles, if user logged in as an admin, different panel, same way .. logged in as normal user has restriction to Admin Dashboard.
Before we begin this Workshop, we will modify existing code that we developed at Workshop2, you can get full code here at DevOps Workshop2 Github
As in previous Workshop, we stopped after Registration Page. Now here let us start with Login Page
Login Page
backend/src/main/java/com/maaystore/backend/dto/LoginRequest.java
package com.maaystore.backend.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public class LoginRequest {
// User's email address
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
// User's password
@NotBlank(message = "Password is required")
private String password;
// Get email
public String getEmail() {
return email;
}
// Set email
public void setEmail(String email) {
this.email = email;
}
// Get password
public String getPassword() {
return password;
}
// Set password
public void setPassword(String password) {
this.password = password;
}
} Update AuthController.java
package com.maayastore.backend.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.maayastore.backend.dto.ErrorResponse;
import com.maayastore.backend.dto.LoginRequest;
import com.maayastore.backend.dto.LoginResponse;
import com.maayastore.backend.entity.User;
import com.maayastore.backend.repository.UserRepository;
import com.maayastore.backend.service.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public AuthController(
UserService userService,
UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userService = userService;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
// =====================================================
// REGISTER
// =====================================================
@PostMapping("/register")
public ResponseEntity<?> registerUser(
@Valid @RequestBody User user) {
// Check whether email is already registered
if (userService.emailExists(user.getEmail())) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Email already registered"
));
}
// Anyone registering through the public
// registration API is automatically a USER.
user.setRole("USER");
return ResponseEntity.ok(
userService.registerUser(user)
);
}
// =====================================================
// LOGIN
// =====================================================
@PostMapping("/login")
public ResponseEntity<?> login(
@Valid @RequestBody LoginRequest loginRequest) {
// Find the user using the email provided
// in the login request.
User user = userRepository
.findByEmail(loginRequest.getEmail())
.orElse(null);
// Email doesn't exist
if (user == null) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Invalid email or password"
));
}
// Compare the entered password with the
// encrypted password stored in PostgreSQL.
boolean passwordMatches =
passwordEncoder.matches(
loginRequest.getPassword(),
user.getPassword()
);
// Password is incorrect
if (!passwordMatches) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Invalid email or password"
));
}
// =================================================
// LOGIN SUCCESSFUL
//
// For now the token is null.
// We will generate the JWT in the next step.
// =================================================
return ResponseEntity.ok(
new LoginResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getRole(),
null
)
);
}
} Add JWT dependency
JSON Web token
Why is JWTAuthentication needed, think of it as checking into hotel, first time you check in in the reception, compare it with our login request. Imagine you go out for a tourist destination and come again, the hotel asks you to re check in everytime you enter the hotel. that would be lot frustrating.
instead once you check in, you will be given key card.
this keycard you can think of as a JWT token.
<!-- JWT API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<!-- JWT implementation -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<!-- JWT Jackson support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
backend/src/main/java/com/maayastore/backend/service/JwtService.java
package com.maayastore.backend.service;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
@Service
public class JwtService {
// =====================================================
// SECRET KEY
// =====================================================
//
// This key is used to SIGN our JWT.
//
// IMPORTANT:
// The same secret key must be available when we later
// VERIFY the token.
//
// In a real production application, this should NOT
// be hard-coded. It should come from an environment
// variable or secure configuration.
// =====================================================
private final String secretKey =
"maaya-store-secret-key-2026-this-is-long-enough";
// Convert our String secret into a cryptographic key.
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(
secretKey.getBytes(StandardCharsets.UTF_8)
);
}
// =====================================================
// GENERATE TOKEN
// =====================================================
//
// We generate a JWT after the user successfully logs in.
//
// The email becomes the "subject" of the token.
// =====================================================
public String generateToken(String email) {
return Jwts.builder()
// Store user's email inside JWT subject
.subject(email)
// When the token was created
.issuedAt(new Date())
// Token expires after 24 hours
.expiration(
new Date(
System.currentTimeMillis()
+ 1000L * 60 * 60 * 24
)
)
// Sign the token using our secret key
.signWith(getSigningKey())
// Create the final JWT string
.compact();
}
// =====================================================
// EXTRACT EMAIL
// =====================================================
//
// Later, when the frontend sends:
//
// Authorization: Bearer <JWT>
//
// we can extract the email from the token.
// =====================================================
public String extractEmail(String token) {
Claims claims = Jwts.parser()
// Tell JWT which key should be used
// to verify the token.
.verifyWith(getSigningKey())
.build()
// Read the JWT
.parseSignedClaims(token)
// Get the information stored inside it
.getPayload();
// Return the email stored as the subject
return claims.getSubject();
}
// =====================================================
// VALIDATE TOKEN
// =====================================================
//
// This checks:
//
// 1. Is the JWT correctly signed?
// 2. Is it still valid?
// 3. Does it belong to the expected user?
// =====================================================
public boolean isTokenValid(
String token,
String email) {
try {
String tokenEmail = extractEmail(token);
return tokenEmail.equals(email)
&& !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
// =====================================================
// CHECK TOKEN EXPIRATION
// =====================================================
private boolean isTokenExpired(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getExpiration()
.before(new Date());
}
}
Now update AuthController.java to include JWT service
package com.maayastore.backend.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.maayastore.backend.dto.ErrorResponse;
import com.maayastore.backend.dto.LoginRequest;
import com.maayastore.backend.dto.LoginResponse;
import com.maayastore.backend.entity.User;
import com.maayastore.backend.repository.UserRepository;
import com.maayastore.backend.service.JwtService; // updated import
import com.maayastore.backend.service.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService; // updated variable for JWT
public AuthController(
UserService userService,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtService jwtService) { //updated constructor
this.userService = userService;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
}
// =====================================================
// REGISTER
// =====================================================
@PostMapping("/register")
public ResponseEntity<?> registerUser(
@Valid @RequestBody User user) {
// Check whether email is already registered
if (userService.emailExists(user.getEmail())) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Email already registered"
));
}
// Anyone registering through the public
// registration API is automatically a USER.
user.setRole("USER");
return ResponseEntity.ok(
userService.registerUser(user)
);
}
// =====================================================
// LOGIN
// =====================================================
@PostMapping("/login")
public ResponseEntity<?> login(
@Valid @RequestBody LoginRequest loginRequest) {
// Find the user using the email provided
// in the login request.
User user = userRepository
.findByEmail(loginRequest.getEmail())
.orElse(null);
// Email doesn't exist
if (user == null) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Invalid email or password"
));
}
// Compare the entered password with the
// encrypted password stored in PostgreSQL.
boolean passwordMatches =
passwordEncoder.matches(
loginRequest.getPassword(),
user.getPassword()
);
// Password is incorrect
if (!passwordMatches) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Invalid email or password"
));
}
// =================================================
// LOGIN SUCCESSFUL
//
// For now the token is null.
// We will generate the JWT in the next step.
// =================================================
String token = jwtService.generateToken(user.getEmail()); //update for JWT to get token
return ResponseEntity.ok(
new LoginResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getRole(),
token //updated token as response
)
);
}
} and now test using postman
POST http://localhost:8080/api/auth/login
{
"email": "your-existing-user-email",
"password": "your-existing-password"
} Now let us Design Login Page UI
frontend/src/pages/Login.jsximport { useState } from "react";
import { useNavigate } from "react-router-dom";
function Login() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleLogin = async (event) => {
event.preventDefault();
setError("");
setLoading(true);
try {
// Send email and password to Spring Boot
const response = await fetch(
"http://localhost:8080/api/auth/login",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email: email,
password: password
})
}
);
const data = await response.json();
console.log("Login response:", data);
// Login failed
if (!response.ok) {
setError(
data.message || "Invalid email or password"
);
return;
}
// =================================================
// LOGIN SUCCESSFUL
// =================================================
//
// Store JWT in browser Local Storage.
//
// We NEVER store the password.
// =================================================
localStorage.setItem("token", data.token);
// Store basic user information for the UI.
localStorage.setItem(
"user",
JSON.stringify({
id: data.id,
name: data.name,
email: data.email,
role: data.role
})
);
// Go back to the home page
navigate("/");
} catch (error) {
console.error("Login error:", error);
setError(
"Unable to connect to the backend."
);
} finally {
setLoading(false);
}
};
return (
<div className="login-page">
<div className="login-card">
<h2>Login</h2>
{error && (
<p className="login-error">
{error}
</p>
)}
<form onSubmit={handleLogin}>
<div className="form-group">
<label>Email</label>
<input
type="email"
value={email}
onChange={(event) =>
setEmail(event.target.value)
}
placeholder="Enter your email"
required
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
value={password}
onChange={(event) =>
setPassword(event.target.value)
}
placeholder="Enter your password"
required
/>
</div>
<button
type="submit"
disabled={loading}
>
{loading
? "Logging in..."
: "Login"}
</button>
</form>
<p>
Don't have an account?{" "}
<button
type="button"
onClick={() => navigate("/register")}
className="link-button"
>
Register
</button>
</p>
</div>
</div>
);
}
export default Login;
Updated login page in App.jsx
import { useEffect, useState } from "react";
import { Routes, Route } from "react-router-dom";
import ProductCard from "./components/ProductCard";
import ProductDetails from "./pages/ProductDetails";
import Cart from "./pages/Cart";
import Navbar from "./components/Navbar";
import Register from "./pages/Register";
import Login from "./pages/Login"; //import for login page
function Home() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch("http://localhost:8080/api/products")
.then(response => response.json())
.then(data => {
setProducts(data);
})
.catch(error => {
console.error("Error fetching products:", error);
});
}, []);
return (
<main className="main-content">
<h2>Our Products</h2>
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</main>
);
}
function App() {
return (
<div className="app">
<Navbar />
<Routes>
<Route
path="/"
element={<Home />}
/>
<Route
path="/products/:id"
element={<ProductDetails />}
/>
<Route
path="/cart"
element={<Cart />}
/>
<Route
path="/register"
element={<Register />}
/>
<Route
path="/login"
element={<Login />}
/>
{/* NEW: Adds the /login URL and displays the login page */}
</Routes>
</div>
);
}
export default App; Now update even Navbar to get Login and Sign Up pages
Navbar.jsx
import { Link } from "react-router-dom";
import { useCart } from "../context/CartContext";
function Navbar() {
const { cartItems } = useCart();
const cartCount = cartItems.reduce(
(total, item) => total + item.quantity,
0
);
return (
<header className="navbar">
<div className="navbar-container">
<Link to="/" className="navbar-logo">
MAAYA STORE
</Link>
<nav className="navbar-links">
<Link to="/">
Products
</Link>
<Link to="/register">
Sign Up
</Link>
<Link to="/login">
Login
</Link> {/* NEW: Adds the /login URL and displays the login button*/}
<Link to="/cart">
🛒 Cart ({cartCount})
</Link>
</nav>
</div>
</header>
);
}
export default Navbar; also update index.css
/* =========================================================
LOGIN PAGE
========================================================= */
.login-page {
min-height: 80vh;
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
background: #f5f7fa;
}
.login-card {
width: 100%;
max-width: 420px;
background: white;
padding: 40px;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
}
.login-card h2 {
text-align: center;
margin-bottom: 30px;
font-size: 28px;
}
.login-card form {
display: flex;
flex-direction: column;
gap: 20px;
}
.login-card .form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.login-card label {
font-weight: 600;
font-size: 14px;
}
.login-card input {
width: 100%;
padding: 12px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 15px;
box-sizing: border-box;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.login-card input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.login-card form button[type="submit"] {
width: 100%;
padding: 13px;
border: none;
border-radius: 8px;
background: #2563eb;
color: white;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.login-card form button[type="submit"]:hover {
background: #1d4ed8;
transform: translateY(-1px);
}
.login-card form button[type="submit"]:disabled {
background: #9ca3af;
cursor: not-allowed;
transform: none;
}
.login-error {
padding: 10px 12px;
margin-bottom: 20px;
border-radius: 8px;
background: #fee2e2;
color: #b91c1c;
text-align: center;
font-size: 14px;
}
.login-card > p {
margin-top: 25px;
text-align: center;
font-size: 14px;
color: #555;
}
.link-button {
border: none;
background: none;
color: #2563eb;
font-weight: 600;
cursor: pointer;
padding: 0;
font-size: 14px;
}
.link-button:hover {
text-decoration: underline;
}
Now you can access login page
But once logged in ,it still shows sign up and login pages
Now to avoid this, we will update our Navbar to handle with userlogged in logic
simply put, if user is logged in > Hi Username in Navbar
if user logged out > sign in and sign out in Navbar
for this update our Navbar
import { Link, useNavigate } from "react-router-dom";
import { useState } from "react";
import { useCart } from "../context/CartContext";
function Navbar() {
const { cartItems } = useCart();
const navigate = useNavigate();
// =====================================================
// CHECK WHETHER USER IS LOGGED IN
// =====================================================
//
// When Login.jsx successfully logs in, it stores:
//
// localStorage.setItem("token", data.token);
//
// So if "token" exists, we consider the user logged in.
// =====================================================
const [isLoggedIn, setIsLoggedIn] = useState(
!!localStorage.getItem("token")
);
// Get the logged-in user's information
const storedUser = localStorage.getItem("user");
const user = storedUser
? JSON.parse(storedUser)
: null;
// =====================================================
// CART COUNT
// =====================================================
const cartCount = cartItems.reduce(
(total, item) => total + item.quantity,
0
);
// =====================================================
// LOGOUT
// =====================================================
const handleLogout = () => {
// Remove JWT
localStorage.removeItem("token");
// Remove user information
localStorage.removeItem("user");
// Update Navbar immediately
setIsLoggedIn(false);
// Send user to home page
navigate("/");
};
return (
<header className="navbar">
<div className="navbar-container">
{/* LOGO */}
<Link to="/" className="navbar-logo">
MAAYA STORE
</Link>
<nav className="navbar-links">
{/* PRODUCTS */}
<Link to="/">
Products
</Link>
{/* =================================================
SHOW SIGN UP + LOGIN ONLY WHEN LOGGED OUT
================================================= */}
{!isLoggedIn && (
<>
<Link to="/register">
Sign Up
</Link>
<Link to="/login">
Login
</Link>
</>
)}
{/* =================================================
SHOW USER NAME + LOGOUT WHEN LOGGED IN
================================================= */}
{isLoggedIn && (
<>
<span className="navbar-user">
Hi, {user?.name}
</span>
<button
onClick={handleLogout}
className="navbar-logout"
>
Logout
</button>
</>
)}
{/* CART */}
<Link to="/cart">
🛒 Cart ({cartCount})
</Link>
</nav>
</div>
</header>
);
}
export default Navbar;
also update index.css
/* =========================================================
NAVBAR LOGOUT
========================================================= */
.navbar-logout {
border: none;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
padding: 0;
margin: 0;
}
.navbar-logout:hover {
color: #2563eb;
} Now once you changes and rebuilt frontend , you will be able to see our application displaying name and logout button
and once logged out, it displays back the message of sign up and login
backend/src/main/java/com/maayastore/backend/security/JwtAuthenticationFilter.java
package com.maayastore.backend.security;
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.maayastore.backend.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// =================================================
// STEP 1
// Get Authorization header
// =================================================
String authorizationHeader =
request.getHeader("Authorization");
// =================================================
// STEP 2
// Check whether the header contains:
//
// Bearer <JWT>
// =================================================
if (authorizationHeader == null
|| !authorizationHeader.startsWith("Bearer ")) {
// No JWT found.
// Continue the request.
filterChain.doFilter(request, response);
return;
}
// =================================================
// STEP 3
// Remove "Bearer " and keep only the JWT
// =================================================
String token =
authorizationHeader.substring(7);
try {
// =================================================
// STEP 4
// Extract email from JWT
// =================================================
String email =
jwtService.extractEmail(token);
// =================================================
// STEP 5
// Check whether the token is valid
// =================================================
if (jwtService.isTokenValid(token, email)) {
// =================================================
// STEP 6
// Tell Spring Security that this user
// is authenticated.
// =================================================
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
email,
null,
null
);
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
}
} catch (Exception e) {
// Invalid or expired JWT.
// We don't authenticate the user.
System.out.println(
"Invalid JWT: " + e.getMessage()
);
}
// Continue to the next filter/controller
filterChain.doFilter(request, response);
}
}
Now update our SecurityConfig.java
package com.maayastore.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.maayastore.backend.security.JwtAuthenticationFilter;
@Configuration
public class SecurityConfig {
// =====================================================
// JWT FILTER
// =====================================================
//
// Spring will inject our JwtAuthenticationFilter here.
// =====================================================
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(
JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
// =====================================================
// PASSWORD ENCODER
// =====================================================
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// =====================================================
// SECURITY CONFIGURATION
// =====================================================
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
// Disable CSRF because we are using JWT
// for our REST API.
.csrf(csrf -> csrf.disable())
// JWT authentication is STATELESS.
//
// Spring will not create a server-side
// login session.
.sessionManagement(session ->
session.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)
// =================================================
// URL PERMISSIONS
// =================================================
.authorizeHttpRequests(auth -> auth
// Registration and login are public.
.requestMatchers(
"/api/auth/**"
).permitAll()
// Product APIs are currently public.
//
// We will change this later when we decide
// which product operations require login.
.requestMatchers(
"/api/products/**"
).permitAll()
// Product images are public.
.requestMatchers(
"/images/**"
).permitAll()
// Everything else requires authentication.
.anyRequest().authenticated()
)
// =================================================
// JWT FILTER
// =================================================
//
// Run our JWT filter BEFORE Spring Security's
// UsernamePasswordAuthenticationFilter.
// =================================================
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
} Not mandatory..once update you can test if we are able to authenticate using token as we know for any response now our JWT checks if our user is authenticated or not.
you can TestController.java
package com.maayastore.backend.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
public class TestController {
// =====================================================
// PROTECTED TEST API
// =====================================================
//
// This endpoint is only used to test whether JWT
// authentication is working correctly.
//
// We have NOT added permitAll() for /api/test/** in
// SecurityConfig.
//
// Therefore, SecurityConfig's:
//
// .anyRequest().authenticated()
//
// will protect this endpoint.
// =====================================================
@GetMapping("/protected")
public String protectedEndpoint() {
return "You are authenticated! JWT is working.";
}
} and then using postman
GET http://localhost:8080/api/test/protected , Authorization > token.
you can get your token from following steps as shown in video
Now let us create an Admin
Inside Database, simply update a user to Admin as we have set all users common to user only
UPDATE users
SET role = 'ADMIN'
WHERE email = 'admin@gmail.com'; Now if you see
and once done, let us get into the next as we need our JWT token to know admin level as well
Update JwtService.java
package com.maayastore.backend.service;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
@Service
public class JwtService {
// =====================================================
// SECRET KEY
// =====================================================
//
// This key is used to SIGN and VERIFY the JWT.
//
// For this workshop we keep it here for simplicity.
// In production, use an environment variable.
// =====================================================
private final String secretKey =
"maaya-store-secret-key-2026-this-is-long-enough";
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(
secretKey.getBytes(StandardCharsets.UTF_8)
);
}
// =====================================================
// GENERATE TOKEN
// =====================================================
//
// We now store TWO important pieces of information:
//
// subject = user's email
// role = USER or ADMIN
//
// Example:
//
// email = admin@gmail.com
// role = ADMIN
// =====================================================
public String generateToken(
String email,
String role) {
return Jwts.builder()
// Store email in JWT subject
.subject(email)
// Store user's role in JWT
.claim("role", role)
// Token creation time
.issuedAt(new Date())
// Token expires after 24 hours
.expiration(
new Date(
System.currentTimeMillis()
+ 1000L * 60 * 60 * 24
)
)
// Sign JWT
.signWith(getSigningKey())
// Create JWT string
.compact();
}
// =====================================================
// EXTRACT EMAIL
// =====================================================
public String extractEmail(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
// =====================================================
// EXTRACT ROLE
// =====================================================
//
// Example:
//
// JWT
// ├── email = admin@gmail.com
// └── role = ADMIN
//
// This method returns:
//
// ADMIN
// =====================================================
public String extractRole(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.get("role", String.class);
}
// =====================================================
// VALIDATE TOKEN
// =====================================================
public boolean isTokenValid(
String token,
String email) {
try {
String tokenEmail =
extractEmail(token);
return tokenEmail.equals(email)
&& !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
// =====================================================
// CHECK TOKEN EXPIRATION
// =====================================================
private boolean isTokenExpired(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getExpiration()
.before(new Date());
}
} and also update AuthController.java
package com.maayastore.backend.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.maayastore.backend.dto.ErrorResponse;
import com.maayastore.backend.dto.LoginRequest;
import com.maayastore.backend.dto.LoginResponse;
import com.maayastore.backend.entity.User;
import com.maayastore.backend.repository.UserRepository;
import com.maayastore.backend.service.JwtService; // updated import
import com.maayastore.backend.service.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService; // updated variable for JWT
public AuthController(
UserService userService,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtService jwtService) { //updated constructor
this.userService = userService;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
}
// =====================================================
// REGISTER
// =====================================================
@PostMapping("/register")
public ResponseEntity<?> registerUser(
@Valid @RequestBody User user) {
// Check whether email is already registered
if (userService.emailExists(user.getEmail())) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Email already registered"
));
}
// Anyone registering through the public
// registration API is automatically a USER.
user.setRole("USER");
return ResponseEntity.ok(
userService.registerUser(user)
);
}
// =====================================================
// LOGIN
// =====================================================
@PostMapping("/login")
public ResponseEntity<?> login(
@Valid @RequestBody LoginRequest loginRequest) {
// Find the user using the email provided
// in the login request.
User user = userRepository
.findByEmail(loginRequest.getEmail())
.orElse(null);
// Email doesn't exist
if (user == null) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Invalid email or password"
));
}
// Compare the entered password with the
// encrypted password stored in PostgreSQL.
boolean passwordMatches =
passwordEncoder.matches(
loginRequest.getPassword(),
user.getPassword()
);
// Password is incorrect
if (!passwordMatches) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(
"Invalid email or password"
));
}
// =================================================
// LOGIN SUCCESSFUL
//
// For now the token is null.
// We will generate the JWT in the next step.
// =================================================
String token = jwtService.generateToken(
user.getEmail(),
user.getRole()
); //update for JWT to get token and role
return ResponseEntity.ok(
new LoginResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getRole(),
token //updated token as response
)
);
}
} Now let us Update JWTAuthentication filter for it to recognize our admin
package com.maayastore.backend.security;
import java.io.IOException;
import java.util.Collections;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.maayastore.backend.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// =====================================================
// 1. GET JWT FROM AUTHORIZATION HEADER
// =====================================================
String authorizationHeader =
request.getHeader("Authorization");
// If there is no JWT, continue the request.
if (authorizationHeader == null
|| !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// Remove "Bearer " and keep only the token.
String token =
authorizationHeader.substring(7);
try {
// =================================================
// 2. EXTRACT EMAIL FROM JWT
// =================================================
String email =
jwtService.extractEmail(token);
// =================================================
// 3. VALIDATE JWT
// =================================================
if (jwtService.isTokenValid(token, email)) {
// =================================================
// 4. EXTRACT ROLE FROM JWT
// =================================================
String role =
jwtService.extractRole(token);
// =================================================
// 5. CREATE SPRING AUTHENTICATION
// =================================================
//
// Spring Security expects roles in the form:
//
// ROLE_USER
// ROLE_ADMIN
//
// Our database contains:
//
// USER
// ADMIN
//
// Therefore we add "ROLE_" here.
// =================================================
String authority =
"ROLE_" + role;
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
email,
null,
Collections.singletonList(
() -> authority
)
);
// =================================================
// 6. STORE AUTHENTICATION IN SECURITY CONTEXT
// =================================================
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
System.out.println(
"JWT authenticated user: "
+ email
+ " | Role: "
+ role
);
}
} catch (Exception e) {
System.out.println(
"Invalid JWT: " + e.getMessage()
);
}
// Continue the request.
filterChain.doFilter(request, response);
}
}
Also update SecurityConfig.java to update Admin priviliges
command
package com.maayastore.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.maayastore.backend.security.JwtAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(
JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
// =====================================================
// PASSWORD ENCODER
// =====================================================
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// =====================================================
// SECURITY CONFIGURATION
// =====================================================
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
// JWT REST API does not use CSRF tokens.
.csrf(csrf -> csrf.disable())
// JWT authentication is stateless.
.sessionManagement(session ->
session.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)
// =================================================
// URL PERMISSIONS
// =================================================
.authorizeHttpRequests(auth -> auth
// Registration and login are public.
.requestMatchers(
"/api/auth/**"
).permitAll()
// Products are currently public.
.requestMatchers(
"/api/products/**"
).permitAll()
// Product images are public.
.requestMatchers(
"/images/**"
).permitAll()
// =================================================
// ADMIN APIs
// =================================================
//
// ONLY users with ROLE_ADMIN can access these.
// =================================================
.requestMatchers(
"/api/admin/**"
).hasRole("ADMIN")
// Everything else requires login.
.anyRequest().authenticated()
)
// =================================================
// JWT FILTER
// =================================================
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
} And as we see, in security config we updated api/admin .. but we have not configured any admin configuration. for it let us create a new controller
backend/src/main/java/com/maayastore/backend/controller/AdminController.java
package com.maayastore.backend.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/dashboard")
public String dashboard() {
return "Welcome to Maaya Store Admin Dashboard!";
}
}
once done use POSTMAN again
POST http://localhost:8080/api/admin/dashboard and use token of admin
Now you might see Now we are able to authenticate to Admin Dashboard, Only admin user can navigate to admin dashboard
Now let us create UI for our frontend
src/pages/Admin.jsx
import { useEffect, useState } from "react";
function Admin() {
const [message, setMessage] = useState("Loading...");
useEffect(() => {
// =====================================================
// GET JWT FROM LOCAL STORAGE
// =====================================================
const token = localStorage.getItem("token");
// =====================================================
// CALL PROTECTED ADMIN API
// =====================================================
fetch("http://localhost:8080/api/admin/dashboard", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
}
})
.then(response => {
if (!response.ok) {
throw new Error("You are not authorized");
}
return response.text();
})
.then(data => {
setMessage(data);
})
.catch(error => {
setMessage(error.message);
});
}, []);
return (
<div className="admin-page">
{/* =================================================
ADMIN HEADER
================================================= */}
<div className="admin-header">
<div>
<h1>Maaya Store Admin</h1>
<p>Manage your store from one place</p>
</div>
</div>
{/* =================================================
DASHBOARD CARDS
================================================= */}
<div className="admin-cards">
<div className="admin-card">
<h3>👥 Users</h3>
<p>Registered Customers</p>
<strong>--</strong>
</div>
<div className="admin-card">
<h3>📦 Products</h3>
<p>Total Products</p>
<strong>--</strong>
</div>
<div className="admin-card">
<h3>🛒 Orders</h3>
<p>Total Orders</p>
<strong>--</strong>
</div>
</div>
{/* =================================================
ADMIN API STATUS
================================================= */}
<div className="admin-status">
<h2>Admin Dashboard</h2>
<p>{message}</p>
</div>
</div>
);
}
export default Admin; Now also update admin css
/* =====================================================
ADMIN PAGE
===================================================== */
.admin-page {
max-width: 1200px;
margin: 40px auto;
padding: 20px;
}
.admin-header {
margin-bottom: 30px;
}
.admin-header h1 {
margin-bottom: 5px;
font-size: 32px;
}
.admin-header p {
color: #666;
}
/* =====================================================
ADMIN CARDS
===================================================== */
.admin-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.admin-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
}
.admin-card h3 {
margin-bottom: 10px;
}
.admin-card p {
color: #666;
margin-bottom: 15px;
}
.admin-card strong {
font-size: 32px;
}
/* =====================================================
ADMIN STATUS
===================================================== */
.admin-status {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
}
.admin-status h2 {
margin-bottom: 10px;
}
/* =====================================================
RESPONSIVE
===================================================== */
@media (max-width: 768px) {
.admin-cards {
grid-template-columns: 1fr;
}
.admin-page {
margin: 20px auto;
}
} Now to get admin page in our app, add it to App.jsx
import { useEffect, useState } from "react";
import { Routes, Route } from "react-router-dom";
import ProductCard from "./components/ProductCard";
import ProductDetails from "./pages/ProductDetails";
import Cart from "./pages/Cart";
import Navbar from "./components/Navbar";
import Register from "./pages/Register";
import Login from "./pages/Login";
import Admin from "./pages/Admin"; // admin page
function Home() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch("http://localhost:8080/api/products")
.then(response => response.json())
.then(data => {
setProducts(data);
})
.catch(error => {
console.error("Error fetching products:", error);
});
}, []);
return (
<main className="main-content">
<h2>Our Products</h2>
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</main>
);
}
function App() {
return (
<div className="app">
<Navbar />
<Routes>
<Route
path="/"
element={<Home />}
/>
<Route
path="/products/:id"
element={<ProductDetails />}
/>
<Route
path="/cart"
element={<Cart />}
/>
<Route
path="/register"
element={<Register />}
/>
<Route
path="/login"
element={<Login />}
/>
{/* NEW: Adds the /login URL and displays the login page */}
<Route path="/admin" element={<Admin />} /> {/* NEW: Adds the /admin URL and displays the admin page */}
</Routes>
</div>
);
}
export default App;
Now if you check localhost:5173/admin, you will be able to see admin page
Now as we failed to fetch products because of CORS policy and we discussed it earlier already during previous sessions
and now we added additional layer of spring security and it is blocked at first level of security
let us update our SecurityConfig file to allow CORS
Update SecurityConfig.java
package com.maayastore.backend.config;
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.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.maayastore.backend.security.JwtAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(
JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
// =====================================================
// PASSWORD ENCODER
// =====================================================
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// =====================================================
// CORS CONFIGURATION
// =====================================================
//
// Our React frontend runs on:
//
// http://localhost:5173
//
// Our Spring Boot backend runs on:
//
// http://localhost:8080
//
// CORS allows the frontend to communicate with
// the backend.
// =====================================================
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration =
new CorsConfiguration();
// Allow our React frontend
configuration.setAllowedOrigins(
List.of("http://localhost:5173")
);
// Allow these HTTP methods
configuration.setAllowedMethods(
List.of(
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
)
);
// Allow headers such as Authorization and Content-Type
configuration.setAllowedHeaders(
List.of("*")
);
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration(
"/**",
configuration
);
return source;
}
// =====================================================
// SECURITY CONFIGURATION
// =====================================================
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
// Enable CORS
.cors(cors -> {})
// Disable CSRF because we're using JWT
.csrf(csrf -> csrf.disable())
// JWT authentication is stateless
.sessionManagement(session ->
session.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)
// =================================================
// URL PERMISSIONS
// =================================================
.authorizeHttpRequests(auth -> auth
// OPTIONS requests are used by the browser
// for CORS preflight.
.requestMatchers(
HttpMethod.OPTIONS,
"/**"
).permitAll()
// Login and registration are public
.requestMatchers(
"/api/auth/**"
).permitAll()
// Products are currently public
.requestMatchers(
"/api/products/**"
).permitAll()
// Product images are public
.requestMatchers(
"/images/**"
).permitAll()
// ADMIN APIs require ADMIN role
.requestMatchers(
"/api/admin/**"
).hasRole("ADMIN")
// Everything else requires authentication
.anyRequest().authenticated()
)
// =================================================
// JWT FILTER
// =================================================
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
once it is fixed, you will be able to see your admin panel works perfectly fine
This was not able to get information about Users, Products or Orders ( have not created yet)
let us configure Admin Controller to get the desired results
Update AdminController.java
package com.maayastore.backend.controller;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.maayastore.backend.entity.User;
import com.maayastore.backend.entity.Product;
import com.maayastore.backend.repository.UserRepository;
import com.maayastore.backend.repository.ProductRepository;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
private final UserRepository userRepository;
private final ProductRepository productRepository;
public AdminController(
UserRepository userRepository,
ProductRepository productRepository) {
this.userRepository = userRepository;
this.productRepository = productRepository;
}
// =====================================================
// ADMIN DASHBOARD TEST
// =====================================================
@GetMapping("/dashboard")
public String dashboard() {
return "Welcome to Maaya Store Admin Dashboard!";
}
// =====================================================
// GET ALL USERS
// =====================================================
@GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll();
}
// =====================================================
// GET ALL PRODUCTS
// =====================================================
@GetMapping("/products")
public List<Product> getProducts() {
return productRepository.findAll();
}
} And as our Adminpage in UI , we configured it to get only "Welcome message", let us update to get actual data
Admin.jsx
import { useEffect, useState } from "react";
function Admin() {
const [users, setUsers] = useState([]);
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
// =====================================================
// GET JWT
// =====================================================
const token = localStorage.getItem("token");
// =====================================================
// LOAD ADMIN DATA
// =====================================================
useEffect(() => {
const headers = {
"Authorization": `Bearer ${token}`
};
// Get users
fetch("http://localhost:8080/api/admin/users", {
headers
})
.then(response => {
if (!response.ok) {
throw new Error("Unable to load users");
}
return response.json();
})
.then(data => {
setUsers(data);
})
.catch(error => {
console.error("Users error:", error);
});
// Get products
fetch("http://localhost:8080/api/admin/products", {
headers
})
.then(response => {
if (!response.ok) {
throw new Error("Unable to load products");
}
return response.json();
})
.then(data => {
setProducts(data);
})
.catch(error => {
console.error("Products error:", error);
})
.finally(() => {
setLoading(false);
});
}, [token]);
return (
<div className="admin-page">
{/* =================================================
HEADER
================================================= */}
<div className="admin-header">
<h1>Maaya Store Admin</h1>
<p>
Manage your store from one place
</p>
</div>
{/* =================================================
DASHBOARD CARDS
================================================= */}
<div className="admin-cards">
<div className="admin-card">
<h3>👥 Users</h3>
<p>
Registered Customers
</p>
<strong>
{users.length}
</strong>
</div>
<div className="admin-card">
<h3>📦 Products</h3>
<p>
Total Products
</p>
<strong>
{products.length}
</strong>
</div>
<div className="admin-card">
<h3>🛒 Orders</h3>
<p>
Total Orders
</p>
<strong>
0
</strong>
</div>
</div>
{/* =================================================
USERS
================================================= */}
<div className="admin-status">
<h2>Registered Users</h2>
{loading ? (
<p>Loading users...</p>
) : (
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* =================================================
PRODUCTS
================================================= */}
<div className="admin-status">
<h2>Products</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
{products.map(product => (
<tr key={product.id}>
<td>{product.id}</td>
<td>{product.name}</td>
<td>€{product.price}</td>
<td>{product.stock}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export default Admin; Also update index.css for admin page
/* =====================================================
ADMIN TABLES
===================================================== */
.admin-status {
margin-top: 30px;
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
overflow-x: auto;
}
.admin-status h2 {
margin-bottom: 20px;
}
.admin-status table {
width: 100%;
border-collapse: collapse;
}
.admin-status th,
.admin-status td {
padding: 14px;
text-align: left;
border-bottom: 1px solid #eee;
}
.admin-status th {
font-weight: 600;
background: #f8f8f8;
}
.admin-status tr:hover {
background: #fafafa;
}
/* =====================================================
ADMIN ROLE
===================================================== */
.admin-status td:last-child {
font-weight: 600;
}
/* =====================================================
ADMIN PAGE SPACING
===================================================== */
.admin-page {
padding-bottom: 50px;
}
Now when you check Admin page As our admin page is working as expected, and everytime user creates an order, we want seperate entity for it
backend/src/main/java/com/maayastore/backend/entity/Order.java
package com.maayastore.backend.entity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "orders")
public class Order {
// =====================================================
// ORDER ID
// =====================================================
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// =====================================================
// CUSTOMER
// =====================================================
//
// We store the customer's email.
//
// For this workshop we don't create a complicated
// @ManyToOne relationship with User.
// =====================================================
private String customerEmail;
// =====================================================
// PRODUCTS
// =====================================================
//
// The cart products will be stored as JSON text.
//
// Example:
//
// [
// {"id":1,"name":"Phone","quantity":2},
// {"id":3,"name":"Mouse","quantity":1}
// ]
// =====================================================
private String products;
// =====================================================
// TOTAL PRICE
// =====================================================
private BigDecimal totalPrice;
// =====================================================
// ORDER STATUS
// =====================================================
private String status;
// =====================================================
// ORDER DATE
// =====================================================
private LocalDateTime orderDate;
// =====================================================
// DEFAULT CONSTRUCTOR
// =====================================================
public Order() {
}
// =====================================================
// GETTERS AND SETTERS
// =====================================================
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCustomerEmail() {
return customerEmail;
}
public void setCustomerEmail(String customerEmail) {
this.customerEmail = customerEmail;
}
public String getProducts() {
return products;
}
public void setProducts(String products) {
this.products = products;
}
public BigDecimal getTotalPrice() {
return totalPrice;
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getOrderDate() {
return orderDate;
}
public void setOrderDate(LocalDateTime orderDate) {
this.orderDate = orderDate;
}
}
backend/src/main/java/com/maayastore/backend/repository/OrderRepository.java
package com.maayastore.backend.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.maayastore.backend.entity.Order;
public interface OrderRepository
extends JpaRepository<Order, Long> {
}
backend/src/main/java/com/maayastore/backend/controller/OrderController.java
package com.maayastore.backend.controller;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.maayastore.backend.entity.Order;
import com.maayastore.backend.repository.OrderRepository;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderRepository orderRepository;
public OrderController(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
// =====================================================
// PLACE ORDER
// =====================================================
//
// POST /api/orders
//
// The frontend sends:
//
// customerEmail
// products
// totalPrice
//
// =====================================================
@PostMapping
public ResponseEntity<Order> placeOrder(
@RequestBody Order order) {
// Set order status automatically.
order.setStatus("PENDING");
// Set current date/time automatically.
order.setOrderDate(
LocalDateTime.now()
);
// Save order into PostgreSQL.
Order savedOrder =
orderRepository.save(order);
return ResponseEntity.ok(savedOrder);
}
// =====================================================
// GET ALL ORDERS
// =====================================================
//
// This endpoint will be used by the Admin Dashboard.
//
// GET /api/orders
//
// =====================================================
@GetMapping
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
} Update SecurityConfig.java for our API Orders
package com.maayastore.backend.config;
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.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.maayastore.backend.security.JwtAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(
JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
// =====================================================
// PASSWORD ENCODER
// =====================================================
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// =====================================================
// CORS CONFIGURATION
// =====================================================
//
// Our React frontend runs on:
//
// http://localhost:5173
//
// Our Spring Boot backend runs on:
//
// http://localhost:8080
//
// CORS allows the frontend to communicate with
// the backend.
// =====================================================
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration =
new CorsConfiguration();
// Allow our React frontend
configuration.setAllowedOrigins(
List.of("http://localhost:5173")
);
// Allow these HTTP methods
configuration.setAllowedMethods(
List.of(
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
)
);
// Allow headers such as Authorization and Content-Type
configuration.setAllowedHeaders(
List.of("*")
);
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration(
"/**",
configuration
);
return source;
}
// =====================================================
// SECURITY CONFIGURATION
// =====================================================
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
// Enable CORS
.cors(cors -> {})
// Disable CSRF because we're using JWT
.csrf(csrf -> csrf.disable())
// JWT authentication is stateless
.sessionManagement(session ->
session.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)
// =================================================
// URL PERMISSIONS
// =================================================
.authorizeHttpRequests(auth -> auth
// OPTIONS requests are used by the browser
// for CORS preflight.
.requestMatchers(
HttpMethod.OPTIONS,
"/**"
).permitAll()
// Login and registration are public
.requestMatchers(
"/api/auth/**"
).permitAll()
// Products are currently public
.requestMatchers(
"/api/products/**"
).permitAll()
// Product images are public
.requestMatchers(
"/images/**"
).permitAll()
// ADMIN APIs require ADMIN role
.requestMatchers(
"/api/admin/**"
).hasRole("ADMIN")
// Added below rules for order
.requestMatchers(
HttpMethod.POST,
"/api/orders"
).authenticated()
.requestMatchers(
HttpMethod.GET,
"/api/orders"
).hasRole("ADMIN")
// Everything else requires authentication
.anyRequest().authenticated()
)
// =================================================
// JWT FILTER
// =================================================
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
} Now update Cart with Placing Order
Cart.jsx
import { useCart } from "../context/CartContext";
import { useNavigate } from "react-router-dom";
function Cart() {
const {
cartItems,
removeFromCart,
increaseQuantity,
decreaseQuantity,
clearCart
} = useCart();
const navigate = useNavigate();
const total = cartItems.reduce(
(sum, item) =>
sum + item.price * item.quantity,
0
);
// =====================================================
// PLACE ORDER
// =====================================================
const placeOrder = async () => {
// Get JWT
const token =
localStorage.getItem("token");
// Get logged-in user's email
const user =
JSON.parse(
localStorage.getItem("user")
);
// User must login before ordering
if (!token || !user) {
alert("Please login before placing an order.");
navigate("/login");
return;
}
try {
const response = await fetch(
"http://localhost:8080/api/orders",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
customerEmail: user.email,
products: JSON.stringify(
cartItems
),
totalPrice: total
})
}
);
if (!response.ok) {
throw new Error(
"Unable to place order"
);
}
// Clear cart after successful order
clearCart();
alert(
"Order placed successfully!"
);
} catch (error) {
console.error(
"Order error:",
error
);
alert(
"Failed to place order."
);
}
};
return (
<div className="cart-page">
<h1>Your Cart</h1>
{cartItems.length === 0 ? (
<p>Your cart is empty.</p>
) : (
<>
{cartItems.map(item => (
<div
className="cart-item"
key={item.id}
>
<img
src={`http://localhost:8080${item.imagePath}`}
alt={item.name}
/>
<div>
<h3>{item.name}</h3>
<p>
₹{item.price}
</p>
<div>
<button
onClick={() =>
decreaseQuantity(item.id)
}
>
-
</button>
<span>
{" "}
{item.quantity}
{" "}
</span>
<button
onClick={() =>
increaseQuantity(item.id)
}
>
+
</button>
</div>
<button
onClick={() =>
removeFromCart(item.id)
}
>
Remove
</button>
</div>
</div>
))}
<h2>
Total: ₹{total}
</h2>
{/* =================================================
PLACE ORDER
================================================= */}
<button
className="place-order-button"
onClick={placeOrder}
>
Place Order
</button>
</>
)}
</div>
);
}
export default Cart; Now login with any other user and try to login and now you will be able to see in admin console
here in Orders database, we saved products too but in here, we are not displaying it as want to understand how extra column is added
<tr>
<th>ID</th>
<th>Customer</th>
<th>Total</th>
<th>Status</th>
<th>Date</th>
</tr> with
<tr>
<th>ID</th>
<th>Customer</th>
<th>Products</th> // newly added
<th>Total</th>
<th>Status</th>
<th>Date</th>
</tr> Add Product column with quantity by user
your complete Admin.jsx becomes
import { useEffect, useState } from "react";
function Admin() {
const [users, setUsers] = useState([]);
const [products, setProducts] = useState([]);
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
// =====================================================
// GET JWT
// =====================================================
const token = localStorage.getItem("token");
// =====================================================
// LOAD ADMIN DATA
// =====================================================
useEffect(() => {
const headers = {
"Authorization": `Bearer ${token}`
};
// =================================================
// GET USERS
// =================================================
fetch("http://localhost:8080/api/admin/users", {
headers
})
.then(response => {
if (!response.ok) {
throw new Error("Unable to load users");
}
return response.json();
})
.then(data => {
setUsers(data);
})
.catch(error => {
console.error("Users error:", error);
});
// =================================================
// GET PRODUCTS
// =================================================
fetch("http://localhost:8080/api/admin/products", {
headers
})
.then(response => {
if (!response.ok) {
throw new Error("Unable to load products");
}
return response.json();
})
.then(data => {
setProducts(data);
})
.catch(error => {
console.error("Products error:", error);
});
// =================================================
// GET ORDERS
// =================================================
//
// IMPORTANT:
// This MUST be inside useEffect().
// =================================================
fetch("http://localhost:8080/api/orders", {
headers
})
.then(response => {
if (!response.ok) {
throw new Error("Unable to load orders");
}
return response.json();
})
.then(data => {
setOrders(data);
})
.catch(error => {
console.error("Orders error:", error);
})
.finally(() => {
setLoading(false);
});
}, [token]);
// =====================================================
// PAGE
// =====================================================
return (
<div className="admin-page">
{/* =================================================
HEADER
================================================= */}
<div className="admin-header">
<h1>Maaya Store Admin</h1>
<p>
Manage your store from one place
</p>
</div>
{/* =================================================
DASHBOARD CARDS
================================================= */}
<div className="admin-cards">
{/* USERS CARD */}
<div className="admin-card">
<h3>👥 Users</h3>
<p>
Registered Customers
</p>
<strong>
{users.length}
</strong>
</div>
{/* PRODUCTS CARD */}
<div className="admin-card">
<h3>📦 Products</h3>
<p>
Total Products
</p>
<strong>
{products.length}
</strong>
</div>
{/* ORDERS CARD */}
<div className="admin-card">
<h3>🛒 Orders</h3>
<p>
Total Orders
</p>
<strong>
{orders.length}
</strong>
</div>
</div>
{/* =================================================
USERS TABLE
================================================= */}
<div className="admin-status">
<h2>Registered Users</h2>
{loading ? (
<p>Loading users...</p>
) : (
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* =================================================
PRODUCTS TABLE
================================================= */}
<div className="admin-status">
<h2>Products</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
{products.map(product => (
<tr key={product.id}>
<td>{product.id}</td>
<td>{product.name}</td>
<td>€{product.price}</td>
<td>{product.stock}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* =================================================
ORDERS TABLE
================================================= */}
<div className="admin-status">
<h2>Orders</h2>
{loading ? (
<p>Loading orders...</p>
) : orders.length === 0 ? (
<p>No orders have been placed yet.</p>
) : (
<table>
<thead>
<tr>
<th>ID</th>
<th>Customer</th>
<th>Products</th>
<th>Total</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{orders.map(order => (
<tr key={order.id}>
<td>
{order.id}
</td>
<td>
{order.customerEmail}
</td>
<td>
{(() => {
try {
const products =
JSON.parse(order.products);
return products.map(
(product, index) => (
<div key={index}>
{product.name}
{" × "}
{product.quantity}
</div>
)
);
} catch (error) {
return "Unable to read products";
}
})()}
</td>
<td>
₹{order.totalPrice}
</td>
<td>
{order.status}
</td>
<td>
{order.orderDate}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}
export default Admin; and now when you check Admin dashboard
This concludes our Workshop here.












Comments
Post a Comment