From aa122113ade36f02dc8fdbebdf1232b5c4b8742c Mon Sep 17 00:00:00 2001 From: Kamal Wickramanayake Date: Fri, 3 Jul 2026 19:02:36 +0530 Subject: Microservice sample projects --- microservices/04-react-app/src/App.jsx | 36 ++++++ microservices/04-react-app/src/assets/hero.png | Bin 0 -> 13057 bytes microservices/04-react-app/src/assets/react.svg | 1 + microservices/04-react-app/src/assets/vite.svg | 1 + .../04-react-app/src/components/Navbar.jsx | 67 +++++++++++ .../04-react-app/src/components/RoleGuard.jsx | 15 +++ .../04-react-app/src/contexts/AuthContext.jsx | 99 ++++++++++++++++ .../src/features/auth/components/Login.jsx | 9 ++ .../src/features/auth/components/Register.jsx | 9 ++ .../src/features/auth/layouts/AuthLayout.jsx | 12 ++ .../src/features/posts/components/PostList.jsx | 42 +++++++ microservices/04-react-app/src/index.css | 127 +++++++++++++++++++++ microservices/04-react-app/src/main.jsx | 16 +++ microservices/04-react-app/src/pages/AboutPage.jsx | 9 ++ microservices/04-react-app/src/pages/AdminPage.jsx | 9 ++ microservices/04-react-app/src/pages/HomePage.jsx | 9 ++ microservices/04-react-app/src/services/api.js | 37 ++++++ 17 files changed, 498 insertions(+) create mode 100644 microservices/04-react-app/src/App.jsx create mode 100644 microservices/04-react-app/src/assets/hero.png create mode 100644 microservices/04-react-app/src/assets/react.svg create mode 100644 microservices/04-react-app/src/assets/vite.svg create mode 100644 microservices/04-react-app/src/components/Navbar.jsx create mode 100644 microservices/04-react-app/src/components/RoleGuard.jsx create mode 100644 microservices/04-react-app/src/contexts/AuthContext.jsx create mode 100644 microservices/04-react-app/src/features/auth/components/Login.jsx create mode 100644 microservices/04-react-app/src/features/auth/components/Register.jsx create mode 100644 microservices/04-react-app/src/features/auth/layouts/AuthLayout.jsx create mode 100644 microservices/04-react-app/src/features/posts/components/PostList.jsx create mode 100644 microservices/04-react-app/src/index.css create mode 100644 microservices/04-react-app/src/main.jsx create mode 100644 microservices/04-react-app/src/pages/AboutPage.jsx create mode 100644 microservices/04-react-app/src/pages/AdminPage.jsx create mode 100644 microservices/04-react-app/src/pages/HomePage.jsx create mode 100644 microservices/04-react-app/src/services/api.js (limited to 'microservices/04-react-app/src') diff --git a/microservices/04-react-app/src/App.jsx b/microservices/04-react-app/src/App.jsx new file mode 100644 index 0000000..aedd87e --- /dev/null +++ b/microservices/04-react-app/src/App.jsx @@ -0,0 +1,36 @@ +import { Routes, Route, Navigate } from "react-router"; + +import Navbar from "./components/Navbar"; +import HomePage from "./pages/HomePage" +import AboutPage from "./pages/AboutPage" +import AuthLayout from "./features/auth/layouts/AuthLayout" +import Login from "./features/auth/components/Login" +import Register from "./features/auth/components/Register" +import PostList from "./features/posts/components/PostList" +import AdminPage from "./pages/AdminPage"; +import { useAuth } from "./contexts/AuthContext"; + +function App() { + const { isAdmin } = useAuth(); + + return ( +
+ + + + } /> + } /> + } /> + + : } /> + + }> + } /> + } /> + + +
+ ) +} + +export default App diff --git a/microservices/04-react-app/src/assets/hero.png b/microservices/04-react-app/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/microservices/04-react-app/src/assets/hero.png differ diff --git a/microservices/04-react-app/src/assets/react.svg b/microservices/04-react-app/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/microservices/04-react-app/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/microservices/04-react-app/src/assets/vite.svg b/microservices/04-react-app/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/microservices/04-react-app/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/microservices/04-react-app/src/components/Navbar.jsx b/microservices/04-react-app/src/components/Navbar.jsx new file mode 100644 index 0000000..9697c73 --- /dev/null +++ b/microservices/04-react-app/src/components/Navbar.jsx @@ -0,0 +1,67 @@ +import { Link, NavLink } from "react-router"; +import { useAuth } from "../contexts/AuthContext" +import RoleGuard from "./RoleGuard"; + +const Navbar = () => { + const {user, login, logout, isLoggedIn} = useAuth(); + + return ( + + ) +} + +export default Navbar \ No newline at end of file diff --git a/microservices/04-react-app/src/components/RoleGuard.jsx b/microservices/04-react-app/src/components/RoleGuard.jsx new file mode 100644 index 0000000..5585908 --- /dev/null +++ b/microservices/04-react-app/src/components/RoleGuard.jsx @@ -0,0 +1,15 @@ +import { useAuth } from '../contexts/AuthContext'; + +const RoleGuard = ({ allowedRoles, children }) => { + const { isInRoles } = useAuth(); + + // If at least one of the user's role is in the allowed list, show the content + if (isInRoles(allowedRoles)) { + return <>{children}; + } + + // Otherwise, hide the content + return null; +}; + +export default RoleGuard; \ No newline at end of file diff --git a/microservices/04-react-app/src/contexts/AuthContext.jsx b/microservices/04-react-app/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..9bb5937 --- /dev/null +++ b/microservices/04-react-app/src/contexts/AuthContext.jsx @@ -0,0 +1,99 @@ +import { createContext, useState, useEffect, use } from 'react'; +import axios from 'axios'; + +// 1. Initialize Context +const AuthContext = createContext(null); + +// 2. Create a Custom Provider Component +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + + useEffect(() => { + // Fetch user data from your backend API + const fetchUser = async () => { + // axios is directly used from Axios library to avoid interceptor + // added in services/api.js + try { + const response = await axios.get("/bff/api/security/me", { + timeout: 30000, // 30 seconds + headers: { 'Content-Type': 'application/json' }, + withCredentials: true + }); + if (response.data && 'username' in response.data) { + setUser(response.data); + } + } catch (error) { + console.log(error); + } + }; + fetchUser(); + }, []); + + const login = () => { + // Start single sign-on process (Using OAuth2, OpenID) + // Access /ui of API gateway. + // API gateway will redirect user to authorization server if the user is not logged in + // If logged in, API gateway will redirect browser to React app itself reloading the app. + window.location.href = "/bff/ui"; + } + + const logout = async () => { + // Logout from API gateway + try { + const response = await axios.post('/bff/logout', { + timeout: 30000, // 30 seconds + headers: { 'Content-Type': 'application/json' }, + withCredentials: true + }); + // Redirect browser to logout from authorization server. + window.location.href = response.headers.location; + // setUser(null) is not needed since browser is redirected. + } catch (error) { + console.log(error); + } + }; + + const isLoggedIn = () => { + if (user) { + return true; + } + + return false; + }; + + const isAdmin = () => { + return isInRole("ADMIN"); + } + + const isInRole = (role) => { + if (user && role && user.roles) { + return user.roles.includes(role); + } + + return false; + }; + + const isInRoles = (roles) => { + if (user && roles && user.roles) { + return roles.some(value => user.roles.includes(value)); + } + + return false; + } + + // Provide the state and the modifier function as a value object + return ( + + {children} + + ); +} + +// 3. Create a clean Custom Hook to consume this specific context +export function useAuth() { + const context = use(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/microservices/04-react-app/src/features/auth/components/Login.jsx b/microservices/04-react-app/src/features/auth/components/Login.jsx new file mode 100644 index 0000000..45e3e9d --- /dev/null +++ b/microservices/04-react-app/src/features/auth/components/Login.jsx @@ -0,0 +1,9 @@ +import React from 'react' + +const Login = () => { + return ( +
Login
+ ) +} + +export default Login \ No newline at end of file diff --git a/microservices/04-react-app/src/features/auth/components/Register.jsx b/microservices/04-react-app/src/features/auth/components/Register.jsx new file mode 100644 index 0000000..bb20b14 --- /dev/null +++ b/microservices/04-react-app/src/features/auth/components/Register.jsx @@ -0,0 +1,9 @@ +import React from 'react' + +const Register = () => { + return ( +
Register
+ ) +} + +export default Register \ No newline at end of file diff --git a/microservices/04-react-app/src/features/auth/layouts/AuthLayout.jsx b/microservices/04-react-app/src/features/auth/layouts/AuthLayout.jsx new file mode 100644 index 0000000..20aada4 --- /dev/null +++ b/microservices/04-react-app/src/features/auth/layouts/AuthLayout.jsx @@ -0,0 +1,12 @@ +import { Outlet } from 'react-router'; + +const AuthLayout = () => { + return ( +
+
AuthLayout
+ +
+ ) +} + +export default AuthLayout \ No newline at end of file diff --git a/microservices/04-react-app/src/features/posts/components/PostList.jsx b/microservices/04-react-app/src/features/posts/components/PostList.jsx new file mode 100644 index 0000000..a7ccd3b --- /dev/null +++ b/microservices/04-react-app/src/features/posts/components/PostList.jsx @@ -0,0 +1,42 @@ +import { useState, useEffect } from 'react' +import api from '../../../services/api' + +const PostList = () => { + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch data on component mount + useEffect(() => { + api + .get("/posts") // Replace with your actual API endpoint + .then((response) => { + setPosts(response.data); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }, []); + + if (loading) return

Loading posts...

; + if (error) return

Error: {error}

; + + return ( +
+

Latest posts

+ + {/* 1. LIST VIEW */} + +
+ ); +} + +export default PostList \ No newline at end of file diff --git a/microservices/04-react-app/src/index.css b/microservices/04-react-app/src/index.css new file mode 100644 index 0000000..0e27e33 --- /dev/null +++ b/microservices/04-react-app/src/index.css @@ -0,0 +1,127 @@ +/* 1. Global Resets & Variables */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #2563eb; + --text-color: #1f2937; + --bg-color: #ffffff; + --hover-bg: #f3f4f6; + --transition: all 0.3s ease; +} + +body { + font-family: system-ui, -apple-system, sans-serif; + background-color: #f9fafb; +} + +/* 2. Navbar Layout */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background-color: var(--bg-color); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + position: sticky; + top: 0; + z-index: 100; +} + +/* 3. Logo Styles */ +.nav-logo { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); + text-decoration: none; +} + +/* 4. Desktop Menu Items */ +.nav-menu { + display: flex; + align-items: center; + gap: 1.5rem; + list-style: none; +} + +.nav-link { + text-decoration: none; + color: var(--text-color); + font-weight: 500; + font-size: 1rem; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + transition: var(--transition); +} + +.nav-link:hover, +.nav-link.active { + color: var(--primary-color); + background-color: var(--hover-bg); +} + +/* 5. Mobile Layout (Hidden Elements by Default) */ +.nav-toggle { + display: none; +} + +.hamburger { + display: none; + flex-direction: column; + gap: 5px; + cursor: pointer; +} + +.hamburger span { + width: 25px; + height: 3px; + background-color: var(--text-color); + border-radius: 2px; + transition: var(--transition); +} + +/* 6. Responsive Media Query */ +@media (max-width: 768px) { + .hamburger { + display: flex; + } + + .nav-menu { + position: absolute; + top: 100%; + left: 0; + width: 100%; + flex-direction: column; + background-color: var(--bg-color); + padding: 1.5rem; + gap: 1rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + + /* Hide off-screen by default */ + transform: translateY(-150%); + opacity: 0; + transition: var(--transition); + pointer-events: none; + } + + /* Target open state using checkbox selector */ + .nav-toggle:checked ~ .nav-menu { + transform: translateY(0); + opacity: 1; + pointer-events: auto; + } + + /* Hamburger Animate to 'X' when active */ + .nav-toggle:checked ~ .hamburger span:nth-child(1) { + transform: translateY(8px) rotate(45deg); + } + .nav-toggle:checked ~ .hamburger span:nth-child(2) { + opacity: 0; + } + .nav-toggle:checked ~ .hamburger span:nth-child(3) { + transform: translateY(-8px) rotate(-45deg); + } +} diff --git a/microservices/04-react-app/src/main.jsx b/microservices/04-react-app/src/main.jsx new file mode 100644 index 0000000..44b2ec6 --- /dev/null +++ b/microservices/04-react-app/src/main.jsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from "react-router"; +import './index.css' +import App from './App.jsx'; +import { AuthProvider } from './contexts/AuthContext.jsx'; + +createRoot(document.getElementById('root')).render( + + + + + + + , +) \ No newline at end of file diff --git a/microservices/04-react-app/src/pages/AboutPage.jsx b/microservices/04-react-app/src/pages/AboutPage.jsx new file mode 100644 index 0000000..fb55b44 --- /dev/null +++ b/microservices/04-react-app/src/pages/AboutPage.jsx @@ -0,0 +1,9 @@ +import React from 'react' + +const AboutPage = () => { + return ( +
AboutPage
+ ) +} + +export default AboutPage \ No newline at end of file diff --git a/microservices/04-react-app/src/pages/AdminPage.jsx b/microservices/04-react-app/src/pages/AdminPage.jsx new file mode 100644 index 0000000..766d828 --- /dev/null +++ b/microservices/04-react-app/src/pages/AdminPage.jsx @@ -0,0 +1,9 @@ +import React from 'react' + +const AdminPage = () => { + return ( +
AdminPage
+ ) +} + +export default AdminPage \ No newline at end of file diff --git a/microservices/04-react-app/src/pages/HomePage.jsx b/microservices/04-react-app/src/pages/HomePage.jsx new file mode 100644 index 0000000..a082c29 --- /dev/null +++ b/microservices/04-react-app/src/pages/HomePage.jsx @@ -0,0 +1,9 @@ +import React from 'react' + +const HomePage = () => { + return ( +
HomePage
+ ) +} + +export default HomePage \ No newline at end of file diff --git a/microservices/04-react-app/src/services/api.js b/microservices/04-react-app/src/services/api.js new file mode 100644 index 0000000..215ee1b --- /dev/null +++ b/microservices/04-react-app/src/services/api.js @@ -0,0 +1,37 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: '/bff/api', // Example value: https://www.example.com + timeout: 30000, // 30 seconds + headers: { 'Content-Type': 'application/json' }, + withCredentials: true +}); + +// Request Interceptor (Optional: Add Authorization Headers) +/* +api.interceptors.request.use( + (request) => { + console.log("API Request:", request); + return request; + }, + (error) => Promise.reject(error) +); +*/ + +// Response Interceptor for Centralized Error Handling +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + console.error("API ERROR:", error.response || error.message); + + if (window.confirm("A network error occured. Try reloading the page?")) { + window.location.href = "/bff/ui"; + } + + return Promise.reject(error); + } +); + +export default api; \ No newline at end of file -- cgit v1.2.3