diff --git a/samples/animetrix-react-app/README.md b/samples/animetrix-react-app/README.md index bc8c763..6af3a12 100644 --- a/samples/animetrix-react-app/README.md +++ b/samples/animetrix-react-app/README.md @@ -1,4 +1,4 @@ -# Animetrix: Anime Streaming Site +![image](https://github.com/user-attachments/assets/ea9ef8c5-50c2-4264-a487-283d1f4de97f) Welcome to Animetrix! This is a demo project designed to showcase how to simplify React app login with Asgardeo and the Asgardeo React SDK. Animetrix is an anime streaming site where users can browse and watch their favorite anime shows. @@ -38,6 +38,7 @@ Check out the live demo of Animetrix [here](#). ### Prerequisites Before you begin, ensure you have met the following requirements: + - Node.js and npm installed - An Asgardeo account @@ -45,8 +46,8 @@ Before you begin, ensure you have met the following requirements: 1. Clone the repository: ```sh - git clone https://github.com/your-username/animetrix.git - cd animetrix + git clone https://github.com/asgardeo-samples/asgardeo-react-workshop.git + cd samples/animetrix-react-app ``` 2. Install the dependencies: @@ -64,10 +65,27 @@ Before you begin, ensure you have met the following requirements: 2. **Environment Variables:** - Create a `.env` file in the root directory and add the following variables: ```env - REACT_APP_ASGARDEO_CLIENT_ID=your-asgardeo-client-id - REACT_APP_ASGARDEO_BASE_URL=your-asgardeo-base-url - REACT_APP_ASGARDEO_REDIRECT_URI=http://localhost:3000 - REACT_APP_GOOGLE_CLIENT_ID=your-google-client-id + # The base URL for the client application + # E.g., http://localhost:5173 + VITE_REACT_APP_BASE_URL="" + + # The client ID for the Asgardeo Single Page Application (SPA) application. + VITE_REACT_APP_CLIENT_ID="" + + # The base URL for the Asgardeo organization's API + # E.g., https://api.asgardeo.io/t/your-organization-name + VITE_REACT_APP_ASGARDEO_BASE_URL="https://api.asgardeo.io/t/" + + # The sign-up URL for the Asgardeo organization + VITE_REACT_APP_ASGARDEO_SIGN_UP_URL="https://accounts.asgardeo.io/t//accountrecoveryendpoint/register.do?client_id=" + + # The application name for the Asgardeo application + VITE_REACT_APP_NAME="" + + # The organization name for the Asgardeo + VITE_REACT_APP_ORG_NAME="" + + VITE_REACT_APP_RESOURCE_SERVER_URLS=["https://bdc81b0c-bae6-43e8-b4aa-0702a82aee77-prod.e1-us-east-azure.choreoapis.dev/animetrix/movie-catalog-service/v1.0/categories"] ``` ## Usage @@ -79,7 +97,7 @@ Before you begin, ensure you have met the following requirements: npm run dev ``` -2. Open your browser and navigate to `http://localhost:5173`. +2. Open your browser and navigate to `http://localhost:5173/`. 3. You should see the Animetrix homepage. Click the Sign In button to authenticate using Asgardeo. diff --git a/samples/animetrix-react-app/src/App.jsx b/samples/animetrix-react-app/src/App.jsx index fb6fe94..b17fb36 100644 --- a/samples/animetrix-react-app/src/App.jsx +++ b/samples/animetrix-react-app/src/App.jsx @@ -1,35 +1,48 @@ import "./App.css"; -import { createBrowserRouter, RouterProvider, Navigate } from "react-router-dom"; -import { LandingPage } from "./pages/LandingPage"; -import { HomePage } from "./pages/HomePage"; -import { AuthenticatedComponent } from "@asgardeo/auth-react"; -import { InsightsPage } from "./pages/InsightsPage"; - -const routers = createBrowserRouter([ - { - path: "/", - element: , - }, - { - path: "/home", - element: ( - }> - - - ), - }, - { - path: "/insights", - element: ( - }> - - - ), - } -]); +import React, { useEffect, useMemo, useState } from "react"; +import { createBrowserRouter } from "react-router-dom"; +import { useAuthContext } from "@asgardeo/auth-react"; +import AppRouter from "./components/AppRouter"; +import { Loader } from "./components/Loader"; +import { getAppRoutes } from "./configs/Routes"; function App() { - return ; + const { state, getDecodedIDToken } = useAuthContext(); + const [ userRoles, setUserRoles ] = useState(undefined); + + // Fetch the user roles and update the state + useEffect(() => { + const fetchUserRoles = async () => { + try { + const decodedIdToken = await getDecodedIDToken(); + + if (!decodedIdToken?.roles) { + return; + } + + if (decodedIdToken?.roles && decodedIdToken?.roles?.length > 0) { + setUserRoles(decodedIdToken.roles); + } + } catch (error) { + console.error("Error occurred while decoding the ID token", error); + } + }; + + if (state?.isAuthenticated) { + fetchUserRoles(); + } + }, [ getDecodedIDToken, state?.isAuthenticated ]); + + // Memoize the router to avoid recreating it on every render + const router = useMemo(() => { + return createBrowserRouter(getAppRoutes(userRoles)); + }, [ userRoles ]); + + return ( +
+ { router ? : } +
+ ); } export default App; diff --git a/samples/animetrix-react-app/src/components/AppRouter.jsx b/samples/animetrix-react-app/src/components/AppRouter.jsx new file mode 100644 index 0000000..1aea431 --- /dev/null +++ b/samples/animetrix-react-app/src/components/AppRouter.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import { RouterProvider } from "react-router-dom"; + +const AppRouter = ({ router }) => { + return ; +}; + +export default AppRouter; diff --git a/samples/animetrix-react-app/src/components/Header.jsx b/samples/animetrix-react-app/src/components/Header.jsx index e52db9d..bb9764b 100644 --- a/samples/animetrix-react-app/src/components/Header.jsx +++ b/samples/animetrix-react-app/src/components/Header.jsx @@ -4,15 +4,23 @@ import { useAuthContext } from "@asgardeo/auth-react"; export const Header = () => { - const { state, getDecodedIDToken, signOut } = useAuthContext(); + const { state, getDecodedIDToken } = useAuthContext(); - const [ isResourcesAllowed, setIsResourcesAllowed ] = useState(); + const [ isResourcesAllowed, setIsResourcesAllowed ] = useState(false); // Filter the display of Insights section based on the application role. useEffect(() => { getDecodedIDToken() .then((decodedIdToken) => { - if (decodedIdToken?.application_roles === "Anime-App-Admin") { + if (!decodedIdToken?.roles) { + return; + } + + if (decodedIdToken?.roles === "Anime-App-Admin") { + setIsResourcesAllowed(true); + } + + if (decodedIdToken?.roles.includes("Anime-App-Admin")) { setIsResourcesAllowed(true); } }) diff --git a/samples/animetrix-react-app/src/components/ProtectedRoute.jsx b/samples/animetrix-react-app/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..0dbc6ed --- /dev/null +++ b/samples/animetrix-react-app/src/components/ProtectedRoute.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Navigate, Outlet } from "react-router-dom"; + +const ProtectedRoute = ({ allowedRoles, userRoles }) => { + + // If the user doesn't have the required roles, redirect them to unauthorized page. + if (!allowedRoles?.every((role) => userRoles?.includes(role))) { + return ; + } + + // Render the child routes (Outlet) if the user has the required roles + return ; +}; + +export default ProtectedRoute; diff --git a/samples/animetrix-react-app/src/configs/Routes.jsx b/samples/animetrix-react-app/src/configs/Routes.jsx new file mode 100644 index 0000000..67fcaa2 --- /dev/null +++ b/samples/animetrix-react-app/src/configs/Routes.jsx @@ -0,0 +1,38 @@ +import { Navigate } from "react-router-dom"; +import { LandingPage } from "../pages/LandingPage"; +import { Unauthorized } from "../pages/UnauthorizedPage"; +import { HomePage } from "../pages/HomePage"; +import { AuthenticatedComponent } from "@asgardeo/auth-react"; +import ProtectedRoute from "../components/ProtectedRoute"; +import { InsightsPage } from "../pages/InsightsPage"; + +// Static routes configuration function +export const getAppRoutes = (currentUserRoles) => [ + { + path: "/", + element: , + }, + { + path: "/unauthorized", + element: , + }, + { + path: "/home", + element: ( + }> + + + ), + exact: true, + }, + { + path: "/insights", + element: , + children: [ + { + path: "", + element: , + }, + ], + }, +]; diff --git a/samples/animetrix-react-app/src/pages/LandingPage.jsx b/samples/animetrix-react-app/src/pages/LandingPage.jsx index 830a7b8..7f8e640 100644 --- a/samples/animetrix-react-app/src/pages/LandingPage.jsx +++ b/samples/animetrix-react-app/src/pages/LandingPage.jsx @@ -10,6 +10,13 @@ export const LandingPage = () => { const navigate = useNavigate(); const { on, state, signIn, signOut } = useAuthContext(); + const handleSignupRedirect = () => { + const signupURL = `${import.meta.env.VITE_REACT_APP_ASGARDEO_SIGN_UP_URL}${import.meta.env.VITE_REACT_APP_CLIENT_ID} + &sp=${import.meta.env.VITE_REACT_APP_NAME}&redirect_url=${import.meta.env.VITE_REACT_APP_BASE_URL}`; + + window.location.href = signupURL; + } + useEffect(() => { on("sign-in", () => { navigate("/home"); @@ -34,7 +41,7 @@ export const LandingPage = () => { ) } - + diff --git a/samples/animetrix-react-app/src/pages/UnauthorizedPage.jsx b/samples/animetrix-react-app/src/pages/UnauthorizedPage.jsx new file mode 100644 index 0000000..a750f0a --- /dev/null +++ b/samples/animetrix-react-app/src/pages/UnauthorizedPage.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import "../styles/UnauthorizedPage.css"; +import { useNavigate } from "react-router-dom"; + +export const Unauthorized = () => { + + const navigate = useNavigate(); + + return ( +
+
+

401

+

UNAUTHORIZED

+

You are not authorized to access this page.

+ +
+
+ ); +}; diff --git a/samples/animetrix-react-app/src/styles/UnauthorizedPage.css b/samples/animetrix-react-app/src/styles/UnauthorizedPage.css new file mode 100644 index 0000000..0aac0db --- /dev/null +++ b/samples/animetrix-react-app/src/styles/UnauthorizedPage.css @@ -0,0 +1,37 @@ +.container-center.unauthorized { + background: url("../assets/landing-bg.png") no-repeat right !important; + background-size: cover !important; + + .content-wrapper { + padding: 5em; + border-radius: 10px; + /* border: 1px solid #8eb7c3; */ + background: rgb(41 44 49 / 70%); + + h1 { + font-size: 5rem; + margin-bottom: 0; + } + + h3 { + margin-top: 0; + color: #a1a3a3; + } + + p { + margin: 20px 0; + } + + .go-back-btn { + padding: 10px 20px; + background-color: #8eb7c3; + color: white; + border: none; + width: 300px; + text-align: center; + font-size: medium !important; + font-weight: 600; + margin-top: 15px; + } + } +}