From e53044e42cab98b709566ae0850d709f85523c7f Mon Sep 17 00:00:00 2001 From: swve Date: Sun, 4 Feb 2024 19:00:46 +0100 Subject: [PATCH] fix: admin pages authorization issues --- apps/web/app/install/install.tsx | 7 +- apps/web/app/orgs/[orgslug]/dash/layout.tsx | 15 +- apps/web/app/orgs/[orgslug]/dash/page.tsx | 68 +++++---- .../components/Contexts/SessionContext.tsx | 14 +- apps/web/components/Dashboard/UI/LeftMenu.tsx | 35 +++-- .../Security/AdminAuthorization.tsx | 139 ++++++++++++++++++ .../Security/AuthenticatedClientElement.tsx | 3 - .../components/Security/HeaderProfileBox.tsx | 2 +- 8 files changed, 210 insertions(+), 73 deletions(-) create mode 100644 apps/web/components/Security/AdminAuthorization.tsx diff --git a/apps/web/app/install/install.tsx b/apps/web/app/install/install.tsx index 6935b16f..01039e41 100644 --- a/apps/web/app/install/install.tsx +++ b/apps/web/app/install/install.tsx @@ -1,17 +1,12 @@ 'use client' -import React, { use, useEffect } from 'react' +import React, { useEffect } from 'react' import { INSTALL_STEPS } from './steps/steps' import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper' import { useRouter, useSearchParams } from 'next/navigation' import { Suspense } from 'react' - - - function InstallClient() { - - return ( diff --git a/apps/web/app/orgs/[orgslug]/dash/layout.tsx b/apps/web/app/orgs/[orgslug]/dash/layout.tsx index a469b6ae..cbb45810 100644 --- a/apps/web/app/orgs/[orgslug]/dash/layout.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/layout.tsx @@ -1,17 +1,20 @@ import SessionProvider from '@components/Contexts/SessionContext' import LeftMenu from '@components/Dashboard/UI/LeftMenu' +import AdminAuthorization from '@components/Security/AdminAuthorization' import React from 'react' function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) { return ( <> -
- -
- {children} -
-
+ +
+ +
+ {children} +
+
+
) diff --git a/apps/web/app/orgs/[orgslug]/dash/page.tsx b/apps/web/app/orgs/[orgslug]/dash/page.tsx index 6dc6ac7f..9ef5faa2 100644 --- a/apps/web/app/orgs/[orgslug]/dash/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/page.tsx @@ -4,6 +4,7 @@ import learnhousetextlogo from '../../../../public/learnhouse_logo.png' import learnhouseiconlogo from '../../../../public/learnhouse_bigicon.png' import { BookCopy, School, Settings, Users } from 'lucide-react' import Link from 'next/link' +import AdminAuthorization from '@components/Security/AdminAuthorization' function DashboardHome() { return ( @@ -11,39 +12,44 @@ function DashboardHome() {
learnhouse logo
-
- -
- -
Courses
-

Create and manage courses, chapters and ativities

-
- - -
- -
Organization
-

Configure your Organization general settings

-
- - -
- -
Users
-

Manage your Organization's users, roles

-
- - -
-
-
-
- - -
Learn LearnHouse
+ +
+ +
+ +
Courses
+

Create and manage courses, chapters and ativities

+
+ +
+ +
Organization
+

Configure your Organization general settings

+
+ + +
+ +
Users
+

Manage your Organization's users, roles

+
+ +
-
+
+
+ +
+
+ + +
Learn LearnHouse
+ +
+
+
+
diff --git a/apps/web/components/Contexts/SessionContext.tsx b/apps/web/components/Contexts/SessionContext.tsx index 7094b94e..26078c64 100644 --- a/apps/web/components/Contexts/SessionContext.tsx +++ b/apps/web/components/Contexts/SessionContext.tsx @@ -1,13 +1,9 @@ 'use client'; import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation' import React, { useContext, createContext, useEffect } from 'react' -import { useOrg } from './OrgContext'; export const SessionContext = createContext({}) as any; -const PATHS_THAT_REQUIRE_AUTH = ['/dash']; - type Session = { access_token: string; user: any; @@ -18,10 +14,6 @@ type Session = { function SessionProvider({ children }: { children: React.ReactNode }) { const [session, setSession] = React.useState({ access_token: "", user: {}, roles: {}, isLoading: true, isAuthenticated: false }); - const org = useOrg() as any; - const pathname = usePathname() - const router = useRouter() - async function getNewAccessTokenUsingRefreshTokenUI() { let data = await getNewAccessTokenUsingRefreshToken(); @@ -39,6 +31,10 @@ function SessionProvider({ children }: { children: React.ReactNode }) { // Set session setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true }); } + + if (!access_token) { + setSession({ access_token: "", user: {}, roles: {}, isLoading: false, isAuthenticated: false }); + } } @@ -47,8 +43,6 @@ function SessionProvider({ children }: { children: React.ReactNode }) { // Check session checkSession(); - - }, []) return ( diff --git a/apps/web/components/Dashboard/UI/LeftMenu.tsx b/apps/web/components/Dashboard/UI/LeftMenu.tsx index e832f897..abbe3237 100644 --- a/apps/web/components/Dashboard/UI/LeftMenu.tsx +++ b/apps/web/components/Dashboard/UI/LeftMenu.tsx @@ -8,8 +8,9 @@ import { ArrowLeft, Book, BookCopy, Home, LogOut, School, Settings, Users } from import Image from 'next/image'; import Link from 'next/link' import { useRouter } from 'next/navigation'; -import React, { use, useEffect } from 'react' +import React, { useEffect } from 'react' import UserAvatar from '../../Objects/UserAvatar'; +import AdminAuthorization from '@components/Security/AdminAuthorization'; function LeftMenu() { const org = useOrg() as any; @@ -42,7 +43,7 @@ function LeftMenu() { return (
@@ -59,26 +60,28 @@ function LeftMenu() { {/* */} - - - - - - - - - - - - + + + + + + + + + + + + + +
- +
-
+
diff --git a/apps/web/components/Security/AdminAuthorization.tsx b/apps/web/components/Security/AdminAuthorization.tsx new file mode 100644 index 00000000..f474ce70 --- /dev/null +++ b/apps/web/components/Security/AdminAuthorization.tsx @@ -0,0 +1,139 @@ +'use client'; +import { useOrg } from '@components/Contexts/OrgContext'; +import { useSession } from '@components/Contexts/SessionContext'; +import { usePathname, useRouter } from 'next/navigation'; +import React from 'react' + +type AuthorizationProps = { + children: React.ReactNode; + // Authorize components rendering or page rendering + authorizationMode: 'component' | 'page'; +} + +const ADMIN_PATHS = [ + '/dash/org/*', + '/dash/org', + '/dash/users/*', + '/dash/users', + '/dash/courses/*', + '/dash/courses', + '/dash/org/settings/general', +] + +function AdminAuthorization(props: AuthorizationProps) { + const session = useSession() as any; + const org = useOrg() as any; + const pathname = usePathname(); + const router = useRouter(); + + // States + const [isLoading, setIsLoading] = React.useState(true); + const [isAuthorized, setIsAuthorized] = React.useState(false); + + + // Verify if the user is authenticated + const isUserAuthenticated = () => { + if (session.isAuthenticated === true) { + return true; + } + else { + return false; + } + } + + // Verify if the user is an Admin (1), Maintainer (2) or Member (3) of the organization + const isUserAdmin = () => { + const isAdmin = session.roles.some((role: any) => { + return ( + role.org.id === org.id && + (role.role.id === 1 || + role.role.id === 2 || + role.role.role_uuid === 'role_global_admin' || + role.role.role_uuid === 'role_global_maintainer' + ) + ); + }); + return isAdmin; + }; + + function checkPathname(pattern: string, pathname: string) { + // Escape special characters in the pattern and replace '*' with a regex pattern + const regexPattern = new RegExp(`^${pattern.replace(/\//g, '\\/').replace(/\*/g, '.*')}$`); + + // Test if the pathname matches the regex pattern + const isMatch = regexPattern.test(pathname); + + return isMatch; + } + + + const Authorize = () => { + if (props.authorizationMode === 'page') { + + // Check if user is in an admin path + if (ADMIN_PATHS.some((path) => checkPathname(path, pathname))) { + console.log('Admin path') + if (isUserAuthenticated()) { + // Check if the user is an Admin + if (isUserAdmin()) { + setIsAuthorized(true); + } + else { + setIsAuthorized(false); + router.push('/dash'); + } + } + else { + router.push('/login'); + } + } + + else { + if (isUserAuthenticated()) { + setIsAuthorized(true); + } + else { + setIsAuthorized(false); + router.push('/login'); + } + } + } + + if (props.authorizationMode === 'component') { + // Component mode + if (isUserAuthenticated() && isUserAdmin()) { + setIsAuthorized(true); + } + else { + setIsAuthorized(false); + } + } + } + + React.useEffect(() => { + if (session.isLoading) { + return; + } + + Authorize(); + setIsLoading(false); + }, [session, org, pathname]) + + + + return ( + <> + + {props.authorizationMode === 'component' && isAuthorized === true && props.children} + {props.authorizationMode === 'page' && isAuthorized === true && !isLoading && props.children} + {props.authorizationMode === 'page' && isAuthorized === false && !isLoading && +
+

You are not authorized to access this page

+
+ } + + + ) +} + +export default AdminAuthorization \ No newline at end of file diff --git a/apps/web/components/Security/AuthenticatedClientElement.tsx b/apps/web/components/Security/AuthenticatedClientElement.tsx index a02ead11..ac505f34 100644 --- a/apps/web/components/Security/AuthenticatedClientElement.tsx +++ b/apps/web/components/Security/AuthenticatedClientElement.tsx @@ -1,8 +1,5 @@ 'use client'; import React from "react"; -import useSWR, { mutate } from "swr"; -import { getAPIUrl } from "@services/config/config"; -import { swrFetcher } from "@services/utils/ts/requests"; import { useSession } from "@components/Contexts/SessionContext"; import { useOrg } from "@components/Contexts/OrgContext"; diff --git a/apps/web/components/Security/HeaderProfileBox.tsx b/apps/web/components/Security/HeaderProfileBox.tsx index dabf6212..02a4fd70 100644 --- a/apps/web/components/Security/HeaderProfileBox.tsx +++ b/apps/web/components/Security/HeaderProfileBox.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { use, useEffect } from "react"; +import React, { useEffect } from "react"; import styled from "styled-components"; import Link from "next/link"; import Avvvatars from "avvvatars-react";