diff --git a/mocks/handler.js b/mocks/handler.js index c312dfc..8d22664 100644 --- a/mocks/handler.js +++ b/mocks/handler.js @@ -5,7 +5,10 @@ export const handler = [ rest.get(GET_USER_ROUTE, (req, res, ctx) => { return res(ctx.json({ data:{ - id: 10 + id: 10, + permissions:[ + "manage_users" + ], } })) }), diff --git a/package.json b/package.json index 5d6417a..f82c9f2 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "msw": "^1.3.1", - "next-router-mock": "^0.9.9", + "next-router-mock": "^0.9.10", "run-script-os": "^1.1.6" } } diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index d52b977..9e28007 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -43,7 +43,7 @@ "typography_your_login_is_valid_and_you_do_not_need_to_login_again": "شما دسترسی لازم به این صفحه را دارید نیاز به ورود مجدد نیست.", "typography_your_access_to_this_page_has_expired_Please_login_again": "دسترسی شما منقضی شده است لطفا دوباره ورود نمایید.", "typography_redirect_to": "درحال رفتن به صفحه", - "typography_routing_previuos_page": "صفحه قبل...", + "typography_routing_previuos_page": " قبل...", "typography_routing_dashbaord_page": "داشبورد..." }, "Permission": { diff --git a/src/core/components/DataTable.jsx b/src/core/components/DataTable.jsx index 213f924..adedf3d 100644 --- a/src/core/components/DataTable.jsx +++ b/src/core/components/DataTable.jsx @@ -9,56 +9,45 @@ import useRequest from "@/lib/app/hooks/useRequest"; function DataTable(props) { const requestServer = useRequest({auth: true}) - const fetcher = (...args) => { - return requestServer(args, 'get', { - pending: false, - success: {notification: {show: false}} - }).then((response) => { - setRowCount(response.data.meta.totalRowCount); - return response.data.data; - }).catch(() => { - }) - }; const t = useTranslations(); const {languageApp, languageList} = useLanguage(); const [columnFilters, setColumnFilters] = useState([]); const [sorting, setSorting] = useState([]); const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 10}); - const [rowCount, setRowCount] = useState(0); const [columnFilterFns, setColumnFilterFns] = useState(() => { let output = {}; const list = props.columns.map((item) => item.enableColumnFilter ? {[item.id]: item.filterFn} : {[item.id]: ""} ); - for (var key in list) { - var nestedObj = list[key]; - for (var nestedKey in nestedObj) { + for (const key in list) { + const nestedObj = list[key]; + for (const nestedKey in nestedObj) { output[nestedKey] = nestedObj[nestedKey]; } } return output; }); - const [updateTime, setupdateTime] = useState( + const [updateTime, setUpdateTime] = useState( moment().format("HH:mm | jYYYY/jM/jD") ); const tableLocalization = useMemo( () => - languageList.find((item) => item.key == languageApp).tableLocalization, + languageList.find((item) => item.key === languageApp).tableLocalization, [languageApp, languageList] ); const fetchUrl = useMemo(() => { - const url = new URL(props.tableUrl); - url.searchParams.set( + const params = new URLSearchParams(); + params.set( "start", `${pagination.pageIndex * pagination.pageSize}` ); const filters = columnFilters.map((filter) => { let datatype; for (const i in props.columns) { - if (props.columns[i].id == filter.id) { + if (props.columns[i].id === filter.id) { datatype = props.columns[i].datatype; } } @@ -68,10 +57,10 @@ function DataTable(props) { datatype: datatype, }; }); - url.searchParams.set("size", pagination.pageSize); - url.searchParams.set("filters", JSON.stringify(filters ?? [])); - url.searchParams.set("sorting", JSON.stringify(sorting ?? [])); - return url; + params.set("size", pagination.pageSize); + params.set("filters", JSON.stringify(filters ?? [])); + params.set("sorting", JSON.stringify(sorting ?? [])); + return `${props.tableUrl}?${params}`; }, [ props.tableUrl, columnFilters, @@ -81,20 +70,27 @@ function DataTable(props) { props.columns, ]); - const {data, isValidating, mutate} = useSWR(fetchUrl, fetcher, { - revalidateIfStale: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - }); + const {data, isValidating, mutate} = useSWR(fetchUrl, (...args) => + requestServer(args, 'get', { + pending: false, + success: {notification: {show: false}} + }).then((response) => response.data).catch(() => { + }) + , { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: true, + keepPreviousData: true + }); useEffect(() => { - setupdateTime(moment().format("HH:mm | jYYYY/jM/jD")); + setUpdateTime(moment().format("HH:mm | jYYYY/jM/jD")); }, [isValidating, languageApp]); return ( ( <> {props.enableCustomToolbar /* send condition */ - ? /* send component */ + ? /* send component */ : ""} )} @@ -159,6 +155,7 @@ function DataTable(props) { )} state={{ + showProgressBars: isValidating, columnFilters, columnFilterFns, pagination, diff --git a/src/core/components/Middleware/RolePermissionComponent.jsx b/src/core/components/Middleware/RolePermissionComponent.jsx new file mode 100644 index 0000000..869f78a --- /dev/null +++ b/src/core/components/Middleware/RolePermissionComponent.jsx @@ -0,0 +1,33 @@ +import { Button, Typography } from "@mui/material"; +import { useTranslations } from "next-intl"; +import { NextLinkComposed } from "@/core/components/LinkRouting"; +import Message from "@/core/components/Messages"; + +const RolePermissionComponent = () => { + const t = useTranslations(); + + return ( + + {t("Permission.typography_you_dont_have_access")} + + } + actions={ + <> + + + } + /> + ); +}; + +export default RolePermissionComponent; diff --git a/src/core/components/Middleware/WithAuthComponent.jsx b/src/core/components/Middleware/WithAuthComponent.jsx new file mode 100644 index 0000000..9a6ff5d --- /dev/null +++ b/src/core/components/Middleware/WithAuthComponent.jsx @@ -0,0 +1,36 @@ +import { Button, Typography } from "@mui/material"; +import { useRouter } from "next/router"; +import Message from "@/core/components/Messages"; +import { NextLinkComposed } from "@/core/components/LinkRouting"; +import {useTranslations} from "next-intl"; + +const WithAuthComponent = () => { + const router = useRouter(); + const t = useTranslations(); + + return ( + + {t("Authorization.typography_your_access_to_this_page_has_expired_Please_login_again")} + + } + actions={ + <> + + + } + /> + ); +}; + +export default WithAuthComponent; diff --git a/src/core/components/Middleware/WithoutAuthComponent.jsx b/src/core/components/Middleware/WithoutAuthComponent.jsx new file mode 100644 index 0000000..7dbb38d --- /dev/null +++ b/src/core/components/Middleware/WithoutAuthComponent.jsx @@ -0,0 +1,25 @@ +import {Button, Stack, Typography} from "@mui/material"; +import Message from "@/core/components/Messages"; +import {useTranslations} from "next-intl"; +const WithoutAuthComponent = ({ backUrlDecodedPath }) => { + const t = useTranslations(); + + return ( + + + {t("Authorization.typography_your_login_is_valid_and_you_do_not_need_to_login_again")} + + + {t("Authorization.typography_redirect_to")}{" "} + {backUrlDecodedPath + ? t("Authorization.typography_routing_previuos_page") + : t("Authorization.typography_routing_dashbaord_page")} + + + } + /> + ); +}; +export default WithoutAuthComponent; diff --git a/src/core/components/Middleware/__test__/RolePermissionComponent.test.js b/src/core/components/Middleware/__test__/RolePermissionComponent.test.js new file mode 100644 index 0000000..44a583d --- /dev/null +++ b/src/core/components/Middleware/__test__/RolePermissionComponent.test.js @@ -0,0 +1,23 @@ +import { render, screen } from '@testing-library/react'; +import MockAppWithProviders from "../../../../../mocks/AppWithProvider"; +import RolePermissionComponent from "@/core/components/Middleware/RolePermissionComponent"; + +describe('RolePermissionComponent From RolePermissionComponent page', () => { + describe('Rendering', () => { + it('should render the error message when user dont have permission', () => { + render(); + + expect(screen.queryByText('شما دسترسی لازم برای مشاهده این صفحه را ندارید!')).toBeInTheDocument(); + + }); + + it('should render the back dashboard button when user dont have permission', () => { + render(); + + const backButton = screen.queryByText('بازگشت به صفحه اصلی'); + + expect(backButton).toBeInTheDocument(); + + }); + }); +}); diff --git a/src/core/components/Middleware/__test__/WithAuthComponent.test.js b/src/core/components/Middleware/__test__/WithAuthComponent.test.js new file mode 100644 index 0000000..a43e20f --- /dev/null +++ b/src/core/components/Middleware/__test__/WithAuthComponent.test.js @@ -0,0 +1,39 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import MockAppWithProviders from "../../../../../mocks/AppWithProvider"; +import WithAuthComponent from "@/core/components/Middleware/WithAuthComponent"; +import mockRouter from 'next-router-mock'; + +describe('WithAuthComponent From WithAuthMiddleware', () => { + describe('Rendering', () => { + it('should see expired text in render component', () => { + render(); + + const authText = screen.queryByText('دسترسی شما منقضی شده است لطفا دوباره ورود نمایید.'); + + expect(authText).toBeInTheDocument(); + }); + + it('should see login button in render component', () => { + render(); + + const loginButton = screen.queryByText('ورود'); + + expect(loginButton).toBeInTheDocument(); + }); + }); + describe('behavior', () => { + it('should see if router updated in login button', () => { + + mockRouter.query.back_url = 'back_url' + render(); + + const loginButton = screen.queryByText('ورود'); + + fireEvent.click(loginButton); + + expect(mockRouter).toMatchObject({ + query: { back_url: "back_url" }, + }); + }); + }); +}); diff --git a/src/core/components/Middleware/__test__/WithoutAuthComponent.test.js b/src/core/components/Middleware/__test__/WithoutAuthComponent.test.js new file mode 100644 index 0000000..520d7bd --- /dev/null +++ b/src/core/components/Middleware/__test__/WithoutAuthComponent.test.js @@ -0,0 +1,37 @@ +import {render, screen, waitFor} from '@testing-library/react'; +import WithoutAuthComponent from "@/core/components/Middleware/WithoutAuthComponent"; +import MockAppWithProviders from "../../../../../mocks/AppWithProvider"; +import WithoutAuthMiddleware from "@/middlewares/WithoutAuth"; + +describe('WithoutAuthComponent From WithoutAuthMiddleware', () => { + describe('Rendering', () => { + it('should render the message with correct text when backUrlDecodedPath is not provided', async () => { + + localStorage.setItem("_token", 'token'); + + render(); + + await waitFor(()=>{ + expect(screen.queryByText('شما دسترسی لازم به این صفحه را دارید نیاز به ورود مجدد نیست.')).toBeInTheDocument(); + + expect(screen.queryByText('درحال رفتن به صفحه داشبورد...')).toBeInTheDocument(); + }) + + }); + + it('should render the message with a different text when backUrlDecodedPath is provided', async() => { + render(); + + localStorage.setItem("_token", 'token'); + + render(); + + await waitFor(()=>{ + + expect(screen.queryByText('درحال رفتن به صفحه قبل...')).toBeInTheDocument(); + }) + + + }); + }); +}); diff --git a/src/middlewares/RolePermission.jsx b/src/middlewares/RolePermission.jsx index 67fc3f1..4b1cc8a 100644 --- a/src/middlewares/RolePermission.jsx +++ b/src/middlewares/RolePermission.jsx @@ -1,43 +1,14 @@ -import {NextLinkComposed} from "@/core/components/LinkRouting"; -import Message from "@/core/components/Messages"; import useUser from "@/lib/app/hooks/useUser"; -import {Button, Typography} from "@mui/material"; -import {useTranslations} from "next-intl"; +import RolePermissionComponent from "@/core/components/Middleware/RolePermissionComponent"; const RolePermissionMiddleware = ({children, requiredPermissions}) => { const {user} = useUser(); - const t = useTranslations(); const hasPermission = requiredPermissions.some((permission) => user?.permissions?.includes(permission) ); - if (!hasPermission) { - return ( - - {t("Permission.typography_you_dont_have_access")} - - } - actions={ - <> - - - } - /> - ); - } - - return <>{children}; + return !hasPermission ? : <>{children}; }; export default RolePermissionMiddleware; diff --git a/src/middlewares/WithAuth.jsx b/src/middlewares/WithAuth.jsx index b20d8b3..d3b0493 100644 --- a/src/middlewares/WithAuth.jsx +++ b/src/middlewares/WithAuth.jsx @@ -1,42 +1,11 @@ -import {NextLinkComposed} from "@/core/components/LinkRouting"; -import Message from "@/core/components/Messages"; import useUser from "@/lib/app/hooks/useUser"; -import {Button, Typography} from "@mui/material"; -import {useTranslations} from "next-intl"; -import {useRouter} from "next/router"; +import WithAuthComponent from "@/core/components/Middleware/WithAuthComponent"; + const WithAuthMiddleware = ({children}) => { const {isAuth} = useUser(); - const t = useTranslations(); - const router = useRouter(); - if (!isAuth) - return ( - - {t( - "Authorization.typography_your_access_to_this_page_has_expired_Please_login_again" - )} - - } - actions={ - <> - - - } - /> - ); - return <>{children}; + return isAuth ? <>{children} : ; }; export default WithAuthMiddleware; diff --git a/src/middlewares/WithoutAuth.jsx b/src/middlewares/WithoutAuth.jsx index 594ffc5..7fbcba6 100644 --- a/src/middlewares/WithoutAuth.jsx +++ b/src/middlewares/WithoutAuth.jsx @@ -1,19 +1,13 @@ -import Message from "@/core/components/Messages"; import useUser from "@/lib/app/hooks/useUser"; -import {Stack, Typography} from "@mui/material"; -import {useTranslations} from "next-intl"; -import {useSearchParams} from "next/navigation"; import {useRouter} from "next/router"; import {useEffect} from "react"; +import WithoutAuthComponent from "@/core/components/Middleware/WithoutAuthComponent"; const WithoutAuthMiddleware = ({children}) => { const {isAuth} = useUser(); - const t = useTranslations(); const router = useRouter(); - // gettin url query - const searchParams = useSearchParams(); - const backUrlDecodedPath = searchParams.get("back_url"); + const backUrlDecodedPath = router.query?.back_url; useEffect(() => { if (!isAuth) return; @@ -30,27 +24,11 @@ const WithoutAuthMiddleware = ({children}) => { }; }, [isAuth]); - if (isAuth) - return ( - - - {t( - "Authorization.typography_your_login_is_valid_and_you_do_not_need_to_login_again" - )} - - - {t("Authorization.typography_redirect_to")}{" "} - {backUrlDecodedPath - ? t("Authorization.typography_routing_previuos_page") - : t("Authorization.typography_routing_dashbaord_page")} - - - } - /> - ); - return <>{children}; + return isAuth ? ( + + ) : ( + <>{children} + ); }; export default WithoutAuthMiddleware; diff --git a/src/middlewares/__test__/RolePermission.test.js b/src/middlewares/__test__/RolePermission.test.js new file mode 100644 index 0000000..f37796f --- /dev/null +++ b/src/middlewares/__test__/RolePermission.test.js @@ -0,0 +1,34 @@ +import {render, screen, waitFor} from '@testing-library/react'; +import MockAppWithProviders from "../../../mocks/AppWithProvider"; +import RolePermissionMiddleware from "@/middlewares/RolePermission"; + + +describe('RolePermissionMiddleware From RolePermissionMiddleware', () => { + describe('Behavior', () => { + it('show related child if permission exist', async () => { + + const requiredPermissions = ["manage_users"]; + render({'children'}); + + await waitFor(()=>{ + expect(screen.queryByText('children')).toBeInTheDocument(); + + }) + + }); + it('show related error if permission not exist', async () => { + + const requiredPermissions = ["other-permission"]; + + render(); + + await waitFor(()=>{ + + expect(screen.queryByText('شما دسترسی لازم برای مشاهده این صفحه را ندارید!')).toBeInTheDocument(); + + }) + + + }); + }); +});