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) => {
return res(ctx.json({
data:{
id: 10
id: 10,
permissions:[
"manage_users"
],
}
}))
}),

View File

@@ -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"
}
}

View File

@@ -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": {

View File

@@ -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 (
<MaterialReactTable
localization={tableLocalization}
data={data ?? []}
data={data?.data ?? []}
manualFiltering
manualPagination
manualSorting
@@ -125,7 +121,7 @@ function DataTable(props) {
}}
enableColumnFilterModes
muiTablePaperProps={{elevation: 0}}
rowCount={rowCount}
rowCount={data?.meta.totalRowCount}
onColumnFilterFnsChange={setColumnFilterFns}
onColumnFiltersChange={setColumnFilters}
onPaginationChange={setPagination}
@@ -134,7 +130,7 @@ function DataTable(props) {
renderTopToolbarCustomActions={({table}) => (
<>
{props.enableCustomToolbar /* send condition */
? <props.CustomToolbar fetchUrl={fetchUrl} mutate={mutate} /> /* send component */
? <props.CustomToolbar fetchUrl={fetchUrl} mutate={mutate}/> /* send component */
: "<span></span>"}
</>
)}
@@ -159,6 +155,7 @@ function DataTable(props) {
</>
)}
state={{
showProgressBars: isValidating,
columnFilters,
columnFilterFns,
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 {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 (
<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}</>;
return !hasPermission ? <RolePermissionComponent /> : <>{children}</>;
};
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 {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 (
<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}</>;
return isAuth ? <>{children}</> : <WithAuthComponent />;
};
export default WithAuthMiddleware;

View File

@@ -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 (
<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>
}
/>
);
return <>{children}</>;
return isAuth ? (
<WithoutAuthComponent backUrlDecodedPath={backUrlDecodedPath} />
) : (
<>{children}</>
);
};
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();
})
});
});
});