diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 630afed..6b25c6b 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,25 +1,36 @@ import { MantineProvider } from "@mantine/core"; import React from "react"; import DashboardLayout from "@/modules/dashboard/components/DashboardLayout"; -import getUser from "@/modules/auth/actions/getMyDetailAction"; -import { redirect } from "next/navigation"; import { Notifications } from "@mantine/notifications"; +import getCurrentUser from "@/modules/auth/services/getCurrentUser"; +import { AuthContextProvider } from "@/modules/auth/contexts/AuthContext"; +import getSidebarMenus from "@/modules/dashboard/services/getSidebarMenus"; interface Props { children: React.ReactNode; } export default async function Layout(props: Props) { - const user = await getUser(); + const user = (await getCurrentUser()); - if (!user) { - redirect("/login"); - } + // if (!user) { + // redirect("/dashboard/login"); + // } + + const userData = user ? { + id: user.id, + name: user.name ?? "", + email: user.email ?? "", + photoProfile: user.photoProfile, + sidebarMenus: await getSidebarMenus() + } : null; return ( - {props.children} + + {props.children} + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 265823d..21c7457 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,7 +6,6 @@ import "@mantine/core/styles.css"; import '@mantine/notifications/styles.css'; import { ColorSchemeScript } from "@mantine/core"; -import { AuthContextProvider } from "@/modules/auth/contexts/AuthContext"; const inter = Inter({ subsets: ["latin"] }); @@ -26,7 +25,7 @@ export default function RootLayout({ - {children} + {children} ); diff --git a/src/modules/auth/actions/getMyDetailAction.ts b/src/modules/auth/actions/getMyDetailAction.ts index dc3d65e..d9306de 100644 --- a/src/modules/auth/actions/getMyDetailAction.ts +++ b/src/modules/auth/actions/getMyDetailAction.ts @@ -12,6 +12,7 @@ import { cookies } from "next/headers"; * This function uses a JWT token obtained from cookies to authenticate the user and fetch their details. * If the authentication fails due to an invalid JWT token, or if any other error occurs, the function handles these errors gracefully. * + * @deprecated * @returns A promise that resolves to a `ServerResponseAction` object. This object includes a `success` flag indicating the operation's outcome, the user's details in the `data` field if successful, or an error object in the `error` field if an error occurs. * @throws an unhandled error if an unexpected error occurs during the function execution. */ diff --git a/src/modules/auth/actions/guestOnly.ts b/src/modules/auth/actions/guestOnly.ts index ad1535f..a3ce170 100644 --- a/src/modules/auth/actions/guestOnly.ts +++ b/src/modules/auth/actions/guestOnly.ts @@ -1,8 +1,7 @@ "use server"; import { redirect } from "next/navigation"; -import getMyDetail from "../services/getMyDetail"; -import { cookies } from "next/headers"; +import getCurrentUser from "../services/getCurrentUser"; /** * Enforces a guest-only access policy by redirecting authenticated users to the dashboard. @@ -12,11 +11,7 @@ import { cookies } from "next/headers"; * @returns A promise that resolves when the operation completes. The function itself does not return a value. */ export default async function guestOnly(): Promise { - const token = cookies().get("token"); - - if (!token) return; - - const user = await getMyDetail(token.value); + const user = await getCurrentUser(); // If an authenticated user is detected, redirect them to the dashboard. if (user) { diff --git a/src/modules/auth/actions/logoutAction.ts b/src/modules/auth/actions/logoutAction.ts index 2206bae..0ac96f6 100644 --- a/src/modules/auth/actions/logoutAction.ts +++ b/src/modules/auth/actions/logoutAction.ts @@ -1,5 +1,6 @@ "use server"; +import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import "server-only"; @@ -12,5 +13,6 @@ import "server-only"; */ export default async function logout() { cookies().delete("token"); + revalidatePath("/dashboard/login"); redirect("/dashboard/login"); } diff --git a/src/modules/auth/contexts/AuthContext.tsx b/src/modules/auth/contexts/AuthContext.tsx index 9cb26d9..3a681e2 100644 --- a/src/modules/auth/contexts/AuthContext.tsx +++ b/src/modules/auth/contexts/AuthContext.tsx @@ -2,30 +2,29 @@ "use client"; // Importing React functionalities and required components. -import React, { ReactElement, ReactNode, createContext, useCallback, useContext, useEffect, useState } from "react"; -import { notifications } from "@mantine/notifications"; -import getMyDetailAction from "../actions/getMyDetailAction"; -import withServerAction from "@/modules/dashboard/utils/withServerAction"; -import ClientError from "@/core/error/ClientError"; +import React, { ReactElement, ReactNode, createContext, useContext } from "react"; +import SidebarMenu from "@/modules/dashboard/types/SidebarMenu"; // Defining the structure for user data within the authentication context. interface UserData { - name: string; - email: string; - photoUrl: string | null; - // Additional user fields can be added here. + id: string, + name: string, + email: string, + photoProfile: string | null, + sidebarMenus: SidebarMenu[] } // State structure for the authentication context. interface AuthContextState { user: UserData | null; - fetchUserData: () => void; - logout: () => void; + // fetchUserData: () => void; + // logout: () => void; } // Props type definition for the AuthContextProvider component. interface Props { children: ReactNode; + userData: UserData | null; } // Creating the authentication context with an undefined initial value. @@ -39,41 +38,11 @@ const AuthContext = createContext(undefined); * @param {Props} props - Component props containing children to be rendered within the provider. * @returns {ReactElement} A provider component wrapping children with access to authentication context. */ -export const AuthContextProvider = ({ children }: Props): ReactElement => { - const [user, setUser] = useState(null); - - // Function to fetch user data and update state accordingly. - const fetchUserData = useCallback(() => { - withServerAction(getMyDetailAction) - .then((response) => { - setUser(response.data); - }) - .catch((error) => { - if (error instanceof ClientError){ - if (error.errorCode === "UNAUTHENTICATED") return; - } - notifications.show({ - title: 'Error', - message: 'Error while retrieving user data', - color: 'red', - }); - console.error("Error while retrieving user data", error); - }); - }, []); - - // Fetch user data on component mount. - useEffect(() => { - fetchUserData(); - }, [fetchUserData]); - - // Function to clear user data, effectively logging the user out. - const logout = () => { - setUser(null); - }; +export const AuthContextProvider = ({ children, userData }: Props): ReactElement => { // Providing authentication state and functions to the context consumers. return ( - + {children} ); diff --git a/src/modules/auth/services/getCurrentUser.ts b/src/modules/auth/services/getCurrentUser.ts new file mode 100644 index 0000000..67e3769 --- /dev/null +++ b/src/modules/auth/services/getCurrentUser.ts @@ -0,0 +1,13 @@ +import { cookies } from "next/headers"; +import "server-only"; +import getUserFromToken from "../utils/getUserFromToken"; + +export default async function getCurrentUser() { + const token = cookies().get("token")?.value; + + if (!token) return null; + + const userData = await getUserFromToken(token); + + return userData; +} diff --git a/src/modules/auth/services/getMyDetail.ts b/src/modules/auth/services/getMyDetail.ts index b13edb1..e5d6a5e 100644 --- a/src/modules/auth/services/getMyDetail.ts +++ b/src/modules/auth/services/getMyDetail.ts @@ -5,7 +5,10 @@ import AuthError from "../error/AuthError"; * Retrieves the details of the currently authenticated user based on the JWT token. * If the token is not present or the user cannot be found, it returns null. * Otherwise, it returns the user's name, email, and photo URL. + * + * Deprecated. use getCurrentUser() instead (see getCurrentUser.ts) * + * @deprecated * @returns An object containing the user's name, email, and photo URL, or null if the user cannot be authenticated. */ export default async function getMyDetail(token?: string) { diff --git a/src/modules/auth/utils/getUserFromToken.ts b/src/modules/auth/utils/getUserFromToken.ts index 991b649..837eb61 100644 --- a/src/modules/auth/utils/getUserFromToken.ts +++ b/src/modules/auth/utils/getUserFromToken.ts @@ -1,6 +1,7 @@ import { cache } from "react"; import decodeJwtToken from "./decodeJwtToken"; import prisma from "@/core/db"; +import "server-only"; /** * Retrieves user data from the database based on the provided JWT token. @@ -12,7 +13,7 @@ import prisma from "@/core/db"; * @returns The user's data if the user exists, or null if no user is found. * Throws an error if the token is invalid or the database query fails. */ -const getUserFromToken = async (token: string) => { +const getUserFromToken = cache(async (token: string) => { // Decode the JWT token to extract the user ID const decodedToken = decodeJwtToken(token) as { id: string; iat: number }; @@ -32,6 +33,6 @@ const getUserFromToken = async (token: string) => { }); return user; -}; +}); export default getUserFromToken; diff --git a/src/modules/dashboard/actions/getSidebarMenus.ts b/src/modules/dashboard/actions/getSidebarMenus.ts deleted file mode 100644 index cb29e95..0000000 --- a/src/modules/dashboard/actions/getSidebarMenus.ts +++ /dev/null @@ -1,67 +0,0 @@ -"use server"; -import SidebarMenu from "../types/SidebarMenu"; -import "server-only"; -import ServerResponseAction from "../types/ServerResponseAction"; -import handleCatch from "../utils/handleCatch"; -import getUserRoles from "@/modules/auth/utils/getUserRoles"; -import getUserPermissions from "@/modules/auth/utils/getUserPermissions"; -import sidebarMenus from "../data/sidebarMenus"; - -export default async function getSidebarMenus(): Promise< - ServerResponseAction -> { - try { - const filteredMenus: SidebarMenu[] = []; - - const roles = await getUserRoles(); - const permissions = await getUserPermissions(); - - for (let menu of sidebarMenus) { - //if has children - if (menu.children) { - const currentMenuChildren: SidebarMenu["children"] = []; - for (let menuChild of menu.children) { - if ( - menuChild.allowedPermissions?.some((perm) => - permissions?.includes(perm) - ) || - menuChild.allowedRoles?.some((role) => - roles?.includes(role) - ) || - menuChild.allowedPermissions?.includes("*") || - menuChild.allowedRoles?.includes("*") - || roles.includes("super-admin") - ) - currentMenuChildren.push(menuChild); - } - - if (currentMenuChildren.length > 0) { - filteredMenus.push({ - ...menu, - children: currentMenuChildren, - }); - } - } - //if does not have any children - else { - if ( - menu.allowedPermissions?.some((perm) => - permissions?.includes(perm) - ) || - menu.allowedRoles?.some((role) => roles?.includes(role)) || - menu.allowedPermissions?.includes("*") || - menu.allowedRoles?.includes("*") - ) { - filteredMenus.push(menu); - } - } - } - - return { - success: true, - data: filteredMenus, - }; - } catch (e) { - return handleCatch(e); - } -} diff --git a/src/modules/dashboard/actions/getSidebarMenusAction.ts b/src/modules/dashboard/actions/getSidebarMenusAction.ts new file mode 100644 index 0000000..ff4f3b3 --- /dev/null +++ b/src/modules/dashboard/actions/getSidebarMenusAction.ts @@ -0,0 +1,22 @@ +"use server"; +import "server-only"; +import SidebarMenu from "../types/SidebarMenu"; +import ServerResponseAction from "../types/ServerResponseAction"; +import handleCatch from "../utils/handleCatch"; +import getSidebarMenus from "../services/getSidebarMenus"; + +export default async function getSidebarMenusAction(): Promise< + ServerResponseAction +> { + try { + + const filteredMenus = await getSidebarMenus(); + + return { + success: true, + data: filteredMenus, + }; + } catch (e) { + return handleCatch(e); + } +} diff --git a/src/modules/dashboard/components/AppHeader.tsx b/src/modules/dashboard/components/AppHeader.tsx index 7267821..ead78fb 100644 --- a/src/modules/dashboard/components/AppHeader.tsx +++ b/src/modules/dashboard/components/AppHeader.tsx @@ -64,7 +64,7 @@ export default function AppHeader(props: Props) { > ([]); - // Mapping all menu items to MenuItem components - // const menus = getSidebarMenus().map((menu, i) => ); - useEffect(() => { - setFetching(true); - withServerAction(getSidebarMenus) - .then((response) => { - setSidebarMenus(response.data); - }) - .catch((e) => { - console.error(e); - }) - .finally(() => { - setFetching(false); - }); - }, []); + const {user} = useAuth(); return ( { - isFetching ? - {[...new Array(10)].map((_,i) => )} - : - sidebarMenus.map((menu, i) => ( + user?.sidebarMenus.map((menu, i) => ( - ))} + )) ?? null} ); diff --git a/src/modules/dashboard/components/DashboardLayout.tsx b/src/modules/dashboard/components/DashboardLayout.tsx index 804a11a..b4acfd4 100644 --- a/src/modules/dashboard/components/DashboardLayout.tsx +++ b/src/modules/dashboard/components/DashboardLayout.tsx @@ -1,17 +1,15 @@ -/* eslint-disable react-hooks/exhaustive-deps */ "use client"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { AppShell } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import AppHeader from "./AppHeader"; import AppNavbar from "./AppNavbar"; -import { useAuth } from "@/modules/auth/contexts/AuthContext"; import { usePathname } from "next/navigation"; -import dashboardConfig from "../dashboard.config"; interface Props { children: React.ReactNode; + isLoggedIn: boolean } /** @@ -24,22 +22,14 @@ interface Props { */ export default function DashboardLayout(props: Props) { + const pathname = usePathname(); - + + console.log(pathname) // State and toggle function for handling the disclosure of the navigation bar const [openNavbar, { toggle }] = useDisclosure(false); - const {fetchUserData} = useAuth(); - - const [withAppShell, setWithAppShell] = useState(false) - - useEffect(() => { - fetchUserData() - }, []) - - useEffect(() => { - setWithAppShell(!dashboardConfig.routesWithoutAppShell.some(v => `${dashboardConfig.baseRoute}${v}` === pathname)) - }, [pathname]) + const withAppShell = props.isLoggedIn; return withAppShell ? ( + permissions?.includes(perm) + ) || + menuChild.allowedRoles?.some((role) => + roles?.includes(role) + ) || + menuChild.allowedPermissions?.includes("*") || + menuChild.allowedRoles?.includes("*") || + roles.includes("super-admin") + ) + currentMenuChildren.push(menuChild); + } + + if (currentMenuChildren.length > 0) { + filteredMenus.push({ + ...menu, + children: currentMenuChildren, + }); + } + } + //if does not have any children + else { + if ( + menu.allowedPermissions?.some((perm) => + permissions?.includes(perm) + ) || + menu.allowedRoles?.some((role) => roles?.includes(role)) || + menu.allowedPermissions?.includes("*") || + menu.allowedRoles?.includes("*") + ) { + filteredMenus.push(menu); + } + } + } + + return filteredMenus; +} diff --git a/tsconfig.json b/tsconfig.json index e59724b..ae1e32d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, + "noErrorTruncation": true, "jsx": "preserve", "incremental": true, "plugins": [