From 7d4e2865bb626cc6e1cc960b9e0823660c26af78 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 24 Jul 2026 13:23:11 +0330 Subject: [PATCH] perm guard --- src/config/permissions.ts | 67 ++++++++++++ src/pages/auth/components/LoginStep2.tsx | 9 +- src/pages/auth/types/AuthTypes.ts | 16 +++ src/router/MainRouter.tsx | 124 +++++++++++++---------- src/shared/PermissionGuard.tsx | 35 +++++++ 5 files changed, 194 insertions(+), 57 deletions(-) create mode 100644 src/config/permissions.ts create mode 100644 src/shared/PermissionGuard.tsx diff --git a/src/config/permissions.ts b/src/config/permissions.ts new file mode 100644 index 0000000..1513a06 --- /dev/null +++ b/src/config/permissions.ts @@ -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 +} diff --git a/src/pages/auth/components/LoginStep2.tsx b/src/pages/auth/components/LoginStep2.tsx index 5ac4d9a..13d6aa2 100644 --- a/src/pages/auth/components/LoginStep2.tsx +++ b/src/pages/auth/components/LoginStep2.tsx @@ -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) { 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') diff --git a/src/pages/auth/types/AuthTypes.ts b/src/pages/auth/types/AuthTypes.ts index e649750..f09b81f 100644 --- a/src/pages/auth/types/AuthTypes.ts +++ b/src/pages/auth/types/AuthTypes.ts @@ -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; }; diff --git a/src/router/MainRouter.tsx b/src/router/MainRouter.tsx index 50ee06c..657cb34 100644 --- a/src/router/MainRouter.tsx +++ b/src/router/MainRouter.tsx @@ -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, +) => ( + {element} +); + const MainRouter: FC = () => { const location = useLocation() const isPrintPreview = /\/order\/print\/[^/]+\/preview/.test(location.pathname) @@ -74,74 +86,78 @@ const MainRouter: FC = () => { return ( - } /> - } /> + )} /> + )} /> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + )} /> + )} /> + )} /> + )} /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + )} /> - } /> - } /> + )} /> + )} /> - } /> - } /> - } /> - } /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> - } /> - } /> - } /> + )} /> + )} /> + )} /> + )} /> - } /> - } /> - } /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> - } /> - } /> - } /> + )} /> + )} /> + + )} /> + )} /> + )} /> + )} /> + + )} /> + )} /> + )} /> + + )} /> + )} /> + )} /> + + )} /> + )} /> + )} /> } /> } /> } /> - } /> - } /> - } /> + )} /> + )} /> + )} /> - } /> - } /> + )} /> + )} /> - } /> - } /> - } /> + )} /> + )} /> + )} /> ); diff --git a/src/shared/PermissionGuard.tsx b/src/shared/PermissionGuard.tsx new file mode 100644 index 0000000..85f4399 --- /dev/null +++ b/src/shared/PermissionGuard.tsx @@ -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 = ({ 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 + } + + return <>{children} +} + +export default PermissionGuard