CFE-4 Merge branch 'develop' into 'feature/CFE-4_role_management_page'

This commit is contained in:
AmirHossein Mahmoodi
2023-10-10 13:40:58 +00:00
14 changed files with 274 additions and 129 deletions

View File

@@ -5,7 +5,10 @@ export const handler = [
rest.get(GET_USER_ROUTE, (req, res, ctx) => { rest.get(GET_USER_ROUTE, (req, res, ctx) => {
return res(ctx.json({ return res(ctx.json({
data:{ data:{
id: 10 id: 10,
permissions:[
"manage_users"
],
} }
})) }))
}), }),

View File

@@ -64,7 +64,7 @@
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"msw": "^1.3.1", "msw": "^1.3.1",
"next-router-mock": "^0.9.9", "next-router-mock": "^0.9.10",
"run-script-os": "^1.1.6" "run-script-os": "^1.1.6"
} }
} }

View File

@@ -43,7 +43,7 @@
"typography_your_login_is_valid_and_you_do_not_need_to_login_again": "شما دسترسی لازم به این صفحه را دارید نیاز به ورود مجدد نیست.", "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_your_access_to_this_page_has_expired_Please_login_again": "دسترسی شما منقضی شده است لطفا دوباره ورود نمایید.",
"typography_redirect_to": "درحال رفتن به صفحه", "typography_redirect_to": "درحال رفتن به صفحه",
"typography_routing_previuos_page": "صفحه قبل...", "typography_routing_previuos_page": " قبل...",
"typography_routing_dashbaord_page": "داشبورد..." "typography_routing_dashbaord_page": "داشبورد..."
}, },
"Permission": { "Permission": {

View File

@@ -9,56 +9,45 @@ import useRequest from "@/lib/app/hooks/useRequest";
function DataTable(props) { function DataTable(props) {
const requestServer = useRequest({auth: true}) 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 t = useTranslations();
const {languageApp, languageList} = useLanguage(); const {languageApp, languageList} = useLanguage();
const [columnFilters, setColumnFilters] = useState([]); const [columnFilters, setColumnFilters] = useState([]);
const [sorting, setSorting] = useState([]); const [sorting, setSorting] = useState([]);
const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 10}); const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 10});
const [rowCount, setRowCount] = useState(0);
const [columnFilterFns, setColumnFilterFns] = useState(() => { const [columnFilterFns, setColumnFilterFns] = useState(() => {
let output = {}; let output = {};
const list = props.columns.map((item) => const list = props.columns.map((item) =>
item.enableColumnFilter ? {[item.id]: item.filterFn} : {[item.id]: ""} item.enableColumnFilter ? {[item.id]: item.filterFn} : {[item.id]: ""}
); );
for (var key in list) { for (const key in list) {
var nestedObj = list[key]; const nestedObj = list[key];
for (var nestedKey in nestedObj) { for (const nestedKey in nestedObj) {
output[nestedKey] = nestedObj[nestedKey]; output[nestedKey] = nestedObj[nestedKey];
} }
} }
return output; return output;
}); });
const [updateTime, setupdateTime] = useState( const [updateTime, setUpdateTime] = useState(
moment().format("HH:mm | jYYYY/jM/jD") moment().format("HH:mm | jYYYY/jM/jD")
); );
const tableLocalization = useMemo( const tableLocalization = useMemo(
() => () =>
languageList.find((item) => item.key == languageApp).tableLocalization, languageList.find((item) => item.key === languageApp).tableLocalization,
[languageApp, languageList] [languageApp, languageList]
); );
const fetchUrl = useMemo(() => { const fetchUrl = useMemo(() => {
const url = new URL(props.tableUrl); const params = new URLSearchParams();
url.searchParams.set( params.set(
"start", "start",
`${pagination.pageIndex * pagination.pageSize}` `${pagination.pageIndex * pagination.pageSize}`
); );
const filters = columnFilters.map((filter) => { const filters = columnFilters.map((filter) => {
let datatype; let datatype;
for (const i in props.columns) { 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; datatype = props.columns[i].datatype;
} }
} }
@@ -68,10 +57,10 @@ function DataTable(props) {
datatype: datatype, datatype: datatype,
}; };
}); });
url.searchParams.set("size", pagination.pageSize); params.set("size", pagination.pageSize);
url.searchParams.set("filters", JSON.stringify(filters ?? [])); params.set("filters", JSON.stringify(filters ?? []));
url.searchParams.set("sorting", JSON.stringify(sorting ?? [])); params.set("sorting", JSON.stringify(sorting ?? []));
return url; return `${props.tableUrl}?${params}`;
}, [ }, [
props.tableUrl, props.tableUrl,
columnFilters, columnFilters,
@@ -81,20 +70,27 @@ function DataTable(props) {
props.columns, props.columns,
]); ]);
const {data, isValidating, mutate} = useSWR(fetchUrl, fetcher, { const {data, isValidating, mutate} = useSWR(fetchUrl, (...args) =>
revalidateIfStale: false, requestServer(args, 'get', {
revalidateOnFocus: false, pending: false,
revalidateOnReconnect: false, success: {notification: {show: false}}
}); }).then((response) => response.data).catch(() => {
})
, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: true,
keepPreviousData: true
});
useEffect(() => { useEffect(() => {
setupdateTime(moment().format("HH:mm | jYYYY/jM/jD")); setUpdateTime(moment().format("HH:mm | jYYYY/jM/jD"));
}, [isValidating, languageApp]); }, [isValidating, languageApp]);
return ( return (
<MaterialReactTable <MaterialReactTable
localization={tableLocalization} localization={tableLocalization}
data={data ?? []} data={data?.data ?? []}
manualFiltering manualFiltering
manualPagination manualPagination
manualSorting manualSorting
@@ -125,7 +121,7 @@ function DataTable(props) {
}} }}
enableColumnFilterModes enableColumnFilterModes
muiTablePaperProps={{elevation: 0}} muiTablePaperProps={{elevation: 0}}
rowCount={rowCount} rowCount={data?.meta.totalRowCount}
onColumnFilterFnsChange={setColumnFilterFns} onColumnFilterFnsChange={setColumnFilterFns}
onColumnFiltersChange={setColumnFilters} onColumnFiltersChange={setColumnFilters}
onPaginationChange={setPagination} onPaginationChange={setPagination}
@@ -134,7 +130,7 @@ function DataTable(props) {
renderTopToolbarCustomActions={({table}) => ( renderTopToolbarCustomActions={({table}) => (
<> <>
{props.enableCustomToolbar /* send condition */ {props.enableCustomToolbar /* send condition */
? <props.CustomToolbar fetchUrl={fetchUrl} mutate={mutate} /> /* send component */ ? <props.CustomToolbar fetchUrl={fetchUrl} mutate={mutate}/> /* send component */
: "<span></span>"} : "<span></span>"}
</> </>
)} )}
@@ -159,6 +155,7 @@ function DataTable(props) {
</> </>
)} )}
state={{ state={{
showProgressBars: isValidating,
columnFilters, columnFilters,
columnFilterFns, columnFilterFns,
pagination, pagination,

View File

@@ -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 (
<Message
text={
<Typography sx={{ textAlign: "center" }}>
{t("Permission.typography_you_dont_have_access")}
</Typography>
}
actions={
<>
<Button
variant="contained"
component={NextLinkComposed}
to={{
pathname: "/dashboard",
}}
>
{t("Permission.button_back_dashboard")}
</Button>
</>
}
/>
);
};
export default RolePermissionComponent;

View File

@@ -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 (
<Message
text={
<Typography sx={{ textAlign: "center" }}>
{t("Authorization.typography_your_access_to_this_page_has_expired_Please_login_again")}
</Typography>
}
actions={
<>
<Button
variant="contained"
component={NextLinkComposed}
to={{
pathname: "/login-expert",
query: {back_url: encodeURIComponent(router.asPath)},
}}
>
{t("login")}
</Button>
</>
}
/>
);
};
export default WithAuthComponent;

View File

@@ -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 (
<Message
text={
<Stack alignItems="center" spacing={2}>
<Typography sx={{ textAlign: "center" }}>
{t("Authorization.typography_your_login_is_valid_and_you_do_not_need_to_login_again")}
</Typography>
<Typography>
{t("Authorization.typography_redirect_to")}{" "}
{backUrlDecodedPath
? t("Authorization.typography_routing_previuos_page")
: t("Authorization.typography_routing_dashbaord_page")}
</Typography>
</Stack>
}
/>
);
};
export default WithoutAuthComponent;

View File

@@ -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(<MockAppWithProviders><RolePermissionComponent /></MockAppWithProviders>);
expect(screen.queryByText('شما دسترسی لازم برای مشاهده این صفحه را ندارید!')).toBeInTheDocument();
});
it('should render the back dashboard button when user dont have permission', () => {
render(<MockAppWithProviders><RolePermissionComponent /></MockAppWithProviders>);
const backButton = screen.queryByText('بازگشت به صفحه اصلی');
expect(backButton).toBeInTheDocument();
});
});
});

View File

@@ -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(<MockAppWithProviders><WithAuthComponent /></MockAppWithProviders>);
const authText = screen.queryByText('دسترسی شما منقضی شده است لطفا دوباره ورود نمایید.');
expect(authText).toBeInTheDocument();
});
it('should see login button in render component', () => {
render(<MockAppWithProviders><WithAuthComponent /></MockAppWithProviders>);
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(<MockAppWithProviders><WithAuthComponent /></MockAppWithProviders>);
const loginButton = screen.queryByText('ورود');
fireEvent.click(loginButton);
expect(mockRouter).toMatchObject({
query: { back_url: "back_url" },
});
});
});
});

View File

@@ -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(<MockAppWithProviders><WithoutAuthMiddleware /></MockAppWithProviders>);
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(<MockAppWithProviders><WithoutAuthComponent backUrlDecodedPath="/dashboard/change-password" /></MockAppWithProviders>);
localStorage.setItem("_token", 'token');
render(<MockAppWithProviders><WithoutAuthMiddleware /></MockAppWithProviders>);
await waitFor(()=>{
expect(screen.queryByText('درحال رفتن به صفحه قبل...')).toBeInTheDocument();
})
});
});
});

View File

@@ -1,43 +1,14 @@
import {NextLinkComposed} from "@/core/components/LinkRouting";
import Message from "@/core/components/Messages";
import useUser from "@/lib/app/hooks/useUser"; import useUser from "@/lib/app/hooks/useUser";
import {Button, Typography} from "@mui/material"; import RolePermissionComponent from "@/core/components/Middleware/RolePermissionComponent";
import {useTranslations} from "next-intl";
const RolePermissionMiddleware = ({children, requiredPermissions}) => { const RolePermissionMiddleware = ({children, requiredPermissions}) => {
const {user} = useUser(); const {user} = useUser();
const t = useTranslations();
const hasPermission = requiredPermissions.some((permission) => const hasPermission = requiredPermissions.some((permission) =>
user?.permissions?.includes(permission) user?.permissions?.includes(permission)
); );
if (!hasPermission) { return !hasPermission ? <RolePermissionComponent /> : <>{children}</>;
return (
<Message
text={
<Typography sx={{textAlign: "center"}}>
{t("Permission.typography_you_dont_have_access")}
</Typography>
}
actions={
<>
<Button
variant="contained"
component={NextLinkComposed}
to={{
pathname: "/dashboard",
}}
>
{t("Permission.button_back_dashboard")}
</Button>
</>
}
/>
);
}
return <>{children}</>;
}; };
export default RolePermissionMiddleware; export default RolePermissionMiddleware;

View File

@@ -1,42 +1,11 @@
import {NextLinkComposed} from "@/core/components/LinkRouting";
import Message from "@/core/components/Messages";
import useUser from "@/lib/app/hooks/useUser"; import useUser from "@/lib/app/hooks/useUser";
import {Button, Typography} from "@mui/material"; import WithAuthComponent from "@/core/components/Middleware/WithAuthComponent";
import {useTranslations} from "next-intl";
import {useRouter} from "next/router";
const WithAuthMiddleware = ({children}) => { const WithAuthMiddleware = ({children}) => {
const {isAuth} = useUser(); const {isAuth} = useUser();
const t = useTranslations();
const router = useRouter();
if (!isAuth) return isAuth ? <>{children}</> : <WithAuthComponent />;
return (
<Message
text={
<Typography sx={{textAlign: "center"}}>
{t(
"Authorization.typography_your_access_to_this_page_has_expired_Please_login_again"
)}
</Typography>
}
actions={
<>
<Button
variant="contained"
component={NextLinkComposed}
to={{
pathname: "/login-expert",
query: {back_url: encodeURIComponent(router.asPath)},
}}
>
{t("login")}
</Button>
</>
}
/>
);
return <>{children}</>;
}; };
export default WithAuthMiddleware; export default WithAuthMiddleware;

View File

@@ -1,19 +1,13 @@
import Message from "@/core/components/Messages";
import useUser from "@/lib/app/hooks/useUser"; 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 {useRouter} from "next/router";
import {useEffect} from "react"; import {useEffect} from "react";
import WithoutAuthComponent from "@/core/components/Middleware/WithoutAuthComponent";
const WithoutAuthMiddleware = ({children}) => { const WithoutAuthMiddleware = ({children}) => {
const {isAuth} = useUser(); const {isAuth} = useUser();
const t = useTranslations();
const router = useRouter(); const router = useRouter();
// gettin url query const backUrlDecodedPath = router.query?.back_url;
const searchParams = useSearchParams();
const backUrlDecodedPath = searchParams.get("back_url");
useEffect(() => { useEffect(() => {
if (!isAuth) return; if (!isAuth) return;
@@ -30,27 +24,11 @@ const WithoutAuthMiddleware = ({children}) => {
}; };
}, [isAuth]); }, [isAuth]);
if (isAuth) return isAuth ? (
return ( <WithoutAuthComponent backUrlDecodedPath={backUrlDecodedPath} />
<Message ) : (
text={ <>{children}</>
<Stack alignItems="center" spacing={2}> );
<Typography sx={{textAlign: "center"}}>
{t(
"Authorization.typography_your_login_is_valid_and_you_do_not_need_to_login_again"
)}
</Typography>
<Typography>
{t("Authorization.typography_redirect_to")}{" "}
{backUrlDecodedPath
? t("Authorization.typography_routing_previuos_page")
: t("Authorization.typography_routing_dashbaord_page")}
</Typography>
</Stack>
}
/>
);
return <>{children}</>;
}; };
export default WithoutAuthMiddleware; export default WithoutAuthMiddleware;

View File

@@ -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(<MockAppWithProviders><RolePermissionMiddleware requiredPermissions={requiredPermissions}/>{'children'}</MockAppWithProviders>);
await waitFor(()=>{
expect(screen.queryByText('children')).toBeInTheDocument();
})
});
it('show related error if permission not exist', async () => {
const requiredPermissions = ["other-permission"];
render(<MockAppWithProviders><RolePermissionMiddleware requiredPermissions={requiredPermissions}/></MockAppWithProviders>);
await waitFor(()=>{
expect(screen.queryByText('شما دسترسی لازم برای مشاهده این صفحه را ندارید!')).toBeInTheDocument();
})
});
});
});