perm guard
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-24 13:23:11 +03:30
parent 8563508674
commit 7d4e2865bb
5 changed files with 194 additions and 57 deletions
+67
View File
@@ -0,0 +1,67 @@
import { Paths } from '@/config/Paths'
/** Permission names aligned with API PermissionEnum. */
export const Permissions = {
DASHBOARD: 'dashboard',
VIEW_ORDERS: 'view_orders',
VIEW_ASSIGNED_ORDERS: 'view_assigned_orders',
VIEW_INVOICES: 'view_invoices',
VIEW_REQUESTS: 'view_requests',
MANAGE_PAYMENTS: 'manage_payments',
VIEW_USERS: 'view_users',
VIEW_PRINT_FORM: 'view_print_form',
MANAGE_ADMINS: 'manage_admins',
MANAGE_ROLES: 'manage_roles',
MANAGE_PRODUCTS: 'manage_products',
MANAGE_TICKETS: 'manage_tickets',
MANAGE_ANNOUNCEMENTS: 'manage_announcements',
MANAGE_CRITICISMS: 'manage_criticisms',
MANAGE_LEARNINGS: 'manage_learnings',
} as const
export type PermissionName = (typeof Permissions)[keyof typeof Permissions]
export const ORDER_VIEW_PERMISSIONS: PermissionName[] = [
Permissions.VIEW_ORDERS,
Permissions.VIEW_ASSIGNED_ORDERS,
]
type LandingRoute = {
permissions: PermissionName | PermissionName[]
path: string
}
/** Sidebar-ordered landing candidates when choosing a post-login / fallback path. */
const LANDING_ROUTES: LandingRoute[] = [
{ permissions: Permissions.DASHBOARD, path: Paths.home },
{ permissions: ORDER_VIEW_PERMISSIONS, path: Paths.order.list },
{ permissions: Permissions.VIEW_INVOICES, path: Paths.perfomaInvoice.list },
{ permissions: Permissions.VIEW_REQUESTS, path: Paths.requests.list },
{ permissions: Permissions.MANAGE_PAYMENTS, path: Paths.payments.list },
{ permissions: Permissions.VIEW_USERS, path: Paths.users.list },
{ permissions: Permissions.VIEW_PRINT_FORM, path: Paths.print.list },
{ permissions: Permissions.MANAGE_ADMINS, path: Paths.admin.list },
{ permissions: Permissions.MANAGE_PRODUCTS, path: Paths.product.list },
{ permissions: Permissions.MANAGE_TICKETS, path: Paths.tickets.list },
{ permissions: Permissions.MANAGE_ANNOUNCEMENTS, path: Paths.announcement.list },
{ permissions: Permissions.MANAGE_CRITICISMS, path: Paths.criticisms.list },
{ permissions: Permissions.MANAGE_LEARNINGS, path: Paths.learning.list },
]
export const hasPermission = (
userPermissions: string[],
required: string | string[],
): boolean => {
const requiredList = Array.isArray(required) ? required : [required]
return requiredList.some((permission) => userPermissions.includes(permission))
}
/** First permitted app path, or profile if none match. */
export const getDefaultAdminPath = (permissions: string[]): string => {
for (const route of LANDING_ROUTES) {
if (hasPermission(permissions, route.permissions)) {
return route.path
}
}
return Paths.profile
}
+6 -3
View File
@@ -11,9 +11,11 @@ import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
import { extractErrorMessage, setRefreshToken } from '../../../config/func'
import { setToken } from '../../../config/func'
import { toast } from '@/shared/toast'
import { Paths } from '@/config/Paths'
import { useSessionAuth } from '@/config/sessionAuth'
import { appNavigate } from '@/config/navigation'
import { getDefaultAdminPath } from '@/config/permissions'
import type { BaseResponse } from '@/shared/types/Types'
import type { OtpVerifyResponseType } from '../types/AuthTypes'
const LoginStep2: FC = () => {
@@ -39,11 +41,12 @@ const LoginStep2: FC = () => {
otp: values.otp
}
otpVerify.mutate(params, {
onSuccess(data) {
onSuccess(data: BaseResponse<OtpVerifyResponseType>) {
setToken(data?.data?.tokens?.accessToken?.token)
setRefreshToken(data?.data?.tokens?.refreshToken?.token)
useSessionAuth.getState().setAuthenticated(true)
appNavigate(Paths.home, { replace: true })
const permissions = data?.data?.admin?.permissions ?? []
appNavigate(getDefaultAdminPath(permissions), { replace: true })
},
onError(error) {
toast(extractErrorMessage(error), 'error')
+16
View File
@@ -33,6 +33,22 @@ export type OtpVerifyType = {
otp: string;
};
export type AdminLoginResponseType = {
firstName?: string;
lastName?: string;
phone: string;
role: string;
permissions: string[];
};
export type OtpVerifyResponseType = {
tokens: {
accessToken: { token: string };
refreshToken: { token: string };
};
admin: AdminLoginResponseType;
};
export type CheckHasAccountType = {
email: string;
};
+70 -54
View File
@@ -1,8 +1,13 @@
import { type FC } from "react";
import { type FC, type ReactNode } from "react";
import { Route, Routes, useLocation } from "react-router-dom";
import Home from "../pages/home/Home";
import AppLayout from "../shared/AppLayout";
import PermissionGuard from "../shared/PermissionGuard";
import { Paths } from "@/config/Paths";
import {
ORDER_VIEW_PERMISSIONS,
Permissions,
} from "@/config/permissions";
import ProformaInvoice from "@/pages/invoice/ProformaInvoice";
import CreateInvoice from "@/pages/invoice/Create";
import RequestList from "@/pages/requests/RequestList";
@@ -59,6 +64,13 @@ import TicketList from "@/pages/ticket/TicketList";
import TicketDetail from "@/pages/ticket/Detail";
import PaymentsList from "@/pages/payment/List";
const withPermission = (
permission: string | string[],
element: ReactNode,
) => (
<PermissionGuard permission={permission}>{element}</PermissionGuard>
);
const MainRouter: FC = () => {
const location = useLocation()
const isPrintPreview = /\/order\/print\/[^/]+\/preview/.test(location.pathname)
@@ -74,74 +86,78 @@ const MainRouter: FC = () => {
return (
<AppLayout>
<Routes>
<Route path="/" element={<Home />} />
<Route path={Paths.home} element={<Home />} />
<Route path="/" element={withPermission(Permissions.DASHBOARD, <Home />)} />
<Route path={Paths.home} element={withPermission(Permissions.DASHBOARD, <Home />)} />
<Route path={Paths.profile} element={<Profile />} />
<Route path={Paths.perfomaInvoice.list} element={<ProformaInvoice />} />
<Route path={Paths.payments.list} element={<PaymentsList />} />
<Route path={Paths.perfomaInvoice.create} element={<CreateInvoice />} />
<Route path={Paths.perfomaInvoice.detail + ":id"} element={<DetailPerfomaInvoice />} />
<Route path={Paths.perfomaInvoice.update + ":id"} element={<UpdateInvoice />} />
<Route path={Paths.requests.list} element={<RequestList />} />
<Route path={Paths.requests.detail + ":id"} element={<RequestDetail />} />
<Route path={Paths.product.list} element={<ProductList />} />
<Route path={Paths.product.create} element={<CreateProduct />} />
<Route path={Paths.product.update + ":id"} element={<UpdateProduct />} />
<Route path={Paths.product.attribute.create + ":id"} element={<CreateAttribute />} />
<Route path={Paths.product.attribute.list + ":id"} element={<AttributeList />} />
<Route path={Paths.product.attribute.update + ":id"} element={<UpdateAttribute />} />
<Route path={Paths.product.attributeValue.list + ":id"} element={<AttributeValues />} />
<Route path={Paths.product.category.list} element={<CategoryList />} />
<Route path={Paths.product.category.create} element={<CreateCategory />} />
<Route path={Paths.product.category.update + ":id"} element={<UpdateCategory />} />
<Route path={Paths.formBuilder.list + ":id/:type"} element={<FormBuilderList />} />
<Route path={Paths.formBuilder.create + ":id/:type"} element={<FormBuilderCreate />} />
<Route path={Paths.formBuilder.update + ":id"} element={<FormBuilderUpdate />} />
<Route path={Paths.formBuilder.values + ":id"} element={<FormBuilderValues />} />
<Route path={Paths.perfomaInvoice.list} element={withPermission(Permissions.VIEW_INVOICES, <ProformaInvoice />)} />
<Route path={Paths.perfomaInvoice.create} element={withPermission(Permissions.VIEW_INVOICES, <CreateInvoice />)} />
<Route path={Paths.perfomaInvoice.detail + ":id"} element={withPermission(Permissions.VIEW_INVOICES, <DetailPerfomaInvoice />)} />
<Route path={Paths.perfomaInvoice.update + ":id"} element={withPermission(Permissions.VIEW_INVOICES, <UpdateInvoice />)} />
<Route path={Paths.order.list} element={<OrdersList />} />
<Route path={Paths.order.details + ":id"} element={<OrderDetail />} />
<Route path={Paths.order.create} element={<NewOrder />} />
<Route path={Paths.order.edit + ":id"} element={<EditOrder />} />
<Route path={Paths.convertToOrder} element={<ConvertToOrders />} />
<Route path={Paths.orderPrint + ':id/preview'} element={<OrderPrintPreview />} />
<Route path={Paths.orderPrint + ':id'} element={<OrderPrint />} />
<Route path={Paths.payments.list} element={withPermission(Permissions.MANAGE_PAYMENTS, <PaymentsList />)} />
<Route path={Paths.tickets.list} element={<TicketList />} />
<Route path={Paths.tickets.detail + ":id"} element={<TicketDetail />} />
<Route path={Paths.requests.list} element={withPermission(Permissions.VIEW_REQUESTS, <RequestList />)} />
<Route path={Paths.requests.detail + ":id"} element={withPermission(Permissions.VIEW_REQUESTS, <RequestDetail />)} />
<Route path={Paths.print.list} element={<SectionList />} />
<Route path={Paths.print.create} element={<CreateSection />} />
<Route path={Paths.print.update + ":id"} element={<UpdateSection />} />
<Route path={Paths.print.form + ":itemId/:orderId"} element={<PrintForm />} />
<Route path={Paths.product.list} element={withPermission(Permissions.MANAGE_PRODUCTS, <ProductList />)} />
<Route path={Paths.product.create} element={withPermission(Permissions.MANAGE_PRODUCTS, <CreateProduct />)} />
<Route path={Paths.product.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateProduct />)} />
<Route path={Paths.product.attribute.create + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <CreateAttribute />)} />
<Route path={Paths.product.attribute.list + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <AttributeList />)} />
<Route path={Paths.product.attribute.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateAttribute />)} />
<Route path={Paths.product.attributeValue.list + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <AttributeValues />)} />
<Route path={Paths.product.category.list} element={withPermission(Permissions.MANAGE_PRODUCTS, <CategoryList />)} />
<Route path={Paths.product.category.create} element={withPermission(Permissions.MANAGE_PRODUCTS, <CreateCategory />)} />
<Route path={Paths.product.category.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateCategory />)} />
<Route path={Paths.users.list} element={<UsersList />} />
<Route path={Paths.users.create} element={<CreateUser />} />
<Route path={Paths.users.update + ":id"} element={<UpdateUser />} />
<Route path={Paths.formBuilder.list + ":id/:type"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderList />)} />
<Route path={Paths.formBuilder.create + ":id/:type"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderCreate />)} />
<Route path={Paths.formBuilder.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderUpdate />)} />
<Route path={Paths.formBuilder.values + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderValues />)} />
<Route path={Paths.admin.list} element={<AdminList />} />
<Route path={Paths.admin.create} element={<CreateAdmin />} />
<Route path={Paths.admin.update + ":id"} element={<EditAdmin />} />
<Route path={Paths.order.list} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrdersList />)} />
<Route path={Paths.order.details + ":id"} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderDetail />)} />
<Route path={Paths.order.create} element={withPermission(ORDER_VIEW_PERMISSIONS, <NewOrder />)} />
<Route path={Paths.order.edit + ":id"} element={withPermission(ORDER_VIEW_PERMISSIONS, <EditOrder />)} />
<Route path={Paths.convertToOrder} element={withPermission(ORDER_VIEW_PERMISSIONS, <ConvertToOrders />)} />
<Route path={Paths.orderPrint + ':id/preview'} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderPrintPreview />)} />
<Route path={Paths.orderPrint + ':id'} element={withPermission(ORDER_VIEW_PERMISSIONS, <OrderPrint />)} />
<Route path={Paths.role.list} element={<RoleList />} />
<Route path={Paths.role.create} element={<CreateRole />} />
<Route path={Paths.role.update + ":id"} element={<UpdateRole />} />
<Route path={Paths.tickets.list} element={withPermission(Permissions.MANAGE_TICKETS, <TicketList />)} />
<Route path={Paths.tickets.detail + ":id"} element={withPermission(Permissions.MANAGE_TICKETS, <TicketDetail />)} />
<Route path={Paths.print.list} element={withPermission(Permissions.VIEW_PRINT_FORM, <SectionList />)} />
<Route path={Paths.print.create} element={withPermission(Permissions.VIEW_PRINT_FORM, <CreateSection />)} />
<Route path={Paths.print.update + ":id"} element={withPermission(Permissions.VIEW_PRINT_FORM, <UpdateSection />)} />
<Route path={Paths.print.form + ":itemId/:orderId"} element={withPermission(Permissions.VIEW_PRINT_FORM, <PrintForm />)} />
<Route path={Paths.users.list} element={withPermission(Permissions.VIEW_USERS, <UsersList />)} />
<Route path={Paths.users.create} element={withPermission(Permissions.VIEW_USERS, <CreateUser />)} />
<Route path={Paths.users.update + ":id"} element={withPermission(Permissions.VIEW_USERS, <UpdateUser />)} />
<Route path={Paths.admin.list} element={withPermission(Permissions.MANAGE_ADMINS, <AdminList />)} />
<Route path={Paths.admin.create} element={withPermission(Permissions.MANAGE_ADMINS, <CreateAdmin />)} />
<Route path={Paths.admin.update + ":id"} element={withPermission(Permissions.MANAGE_ADMINS, <EditAdmin />)} />
<Route path={Paths.role.list} element={withPermission(Permissions.MANAGE_ROLES, <RoleList />)} />
<Route path={Paths.role.create} element={withPermission(Permissions.MANAGE_ROLES, <CreateRole />)} />
<Route path={Paths.role.update + ":id"} element={withPermission(Permissions.MANAGE_ROLES, <UpdateRole />)} />
<Route path={Paths.features.list} element={<FeaturesList />} />
<Route path={Paths.features.create} element={<CreateFeature />} />
<Route path={Paths.service.print} element={<PrintService />} />
<Route path={Paths.announcement.list} element={<AnnouncementList />} />
<Route path={Paths.announcement.detail + ":id"} element={<AnnouncementUpdate />} />
<Route path={Paths.announcement.create} element={<AnnouncementCreate />} />
<Route path={Paths.announcement.list} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementList />)} />
<Route path={Paths.announcement.detail + ":id"} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementUpdate />)} />
<Route path={Paths.announcement.create} element={withPermission(Permissions.MANAGE_ANNOUNCEMENTS, <AnnouncementCreate />)} />
<Route path={Paths.criticisms.list} element={<CriticismsList />} />
<Route path={Paths.criticisms.detail + ":id"} element={<CriticismsDetail />} />
<Route path={Paths.criticisms.list} element={withPermission(Permissions.MANAGE_CRITICISMS, <CriticismsList />)} />
<Route path={Paths.criticisms.detail + ":id"} element={withPermission(Permissions.MANAGE_CRITICISMS, <CriticismsDetail />)} />
<Route path={Paths.learning.list} element={<LearningList />} />
<Route path={Paths.learning.create} element={<LearningCreate />} />
<Route path={Paths.learning.category} element={<LearningCategory />} />
<Route path={Paths.learning.list} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningList />)} />
<Route path={Paths.learning.create} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningCreate />)} />
<Route path={Paths.learning.category} element={withPermission(Permissions.MANAGE_LEARNINGS, <LearningCategory />)} />
</Routes>
</AppLayout>
);
+35
View File
@@ -0,0 +1,35 @@
import { type FC, type ReactNode } from 'react'
import { Navigate } from 'react-router-dom'
import { useGetAdminMe } from '@/pages/admin/hooks/useAdminData'
import {
getDefaultAdminPath,
hasPermission,
} from '@/config/permissions'
type Props = {
permission: string | string[]
children: ReactNode
}
/**
* Blocks route access when the current admin lacks the required permission(s).
* Pass an array to allow access if the user has any of them.
*/
const PermissionGuard: FC<Props> = ({ permission, children }) => {
const { data: adminMe, isLoading } = useGetAdminMe()
if (isLoading) {
return null
}
const permissions =
adminMe?.data?.role?.permissions?.map((p) => p.name) ?? []
if (!hasPermission(permissions, permission)) {
return <Navigate to={getDefaultAdminPath(permissions)} replace />
}
return <>{children}</>
}
export default PermissionGuard