From dc4a119a7a5bf5596a6e8907fda889ffd7f26a7a Mon Sep 17 00:00:00 2001 From: suryacp23 Date: Thu, 30 Jul 2026 10:36:58 +0530 Subject: [PATCH] added role based access --- src/App.tsx | 60 +++++++++------- src/api/client.ts | 11 +++ src/api/clients.ts | 6 ++ src/api/types.ts | 1 + src/auth/AuthProvider.tsx | 26 ++++++- src/auth/ProtectedRoute.tsx | 63 ++++++++++++++++ src/auth/context.ts | 2 + src/routesConfig.tsx | 107 +++++++++++++++++++++++++++ src/screens/ConsoleLayout.tsx | 111 +++++++++++++++++++---------- src/screens/admin/DatasetsPage.tsx | 14 +++- src/screens/tabs.ts | 22 +++--- 11 files changed, 344 insertions(+), 79 deletions(-) create mode 100644 src/auth/ProtectedRoute.tsx create mode 100644 src/routesConfig.tsx diff --git a/src/App.tsx b/src/App.tsx index 9a3a848..2efbe6c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,15 @@ import { DailySalesReportPage } from './screens/DailySalesReportPage' import { ReportPage } from './screens/ReportPage' import { DatasetsPage } from './screens/admin/DatasetsPage' import { DatasetItemsPage } from './screens/admin/DatasetItemsPage' +import { ProtectedRoute, getDefaultRoute } from './auth/ProtectedRoute' +import { useAuth } from './auth/context' + +function RootRedirect() { + const { roles, isAdmin } = useAuth(); + return ; +} + +import { routeConfig } from './routesConfig' function App() { return ( @@ -21,35 +30,32 @@ function App() { } /> }> - } /> - } /> + {routeConfig.map((route, i) => { + const path = route.path.startsWith('/') ? route.path.substring(1) : route.path; + + // Maintain nested route for Dataset items specifically + if (path === 'admin/datasets') { + return ( + {route.element}} + > + } /> + + ); + } - } /> - } /> - - } /> - } /> - - } /> - } /> - - } /> - } /> - - } /> - } /> - - } /> - } /> - - } /> - } /> + return ( + {route.element}} + /> + ); + })} - }> - } /> - - - } /> + } /> diff --git a/src/api/client.ts b/src/api/client.ts index 5261e18..22a09d7 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -122,6 +122,17 @@ export class ZinoClient { this.setToken(null); } + async getMe(): Promise { + const res = await this.request('GET', `/usr/app/${APP_ID}/me`); + if (this.user) { + this.user = { ...this.user, ...res }; + if (typeof window !== 'undefined') { + localStorage.setItem(USER_KEY, JSON.stringify(this.user)); + } + } + return res; + } + /** Decode the persisted JWT into a User (no network) or use the saved user. */ currentUser(): User | null { if (!this.token) return null; diff --git a/src/api/clients.ts b/src/api/clients.ts index a60c8c7..bbe0f12 100644 --- a/src/api/clients.ts +++ b/src/api/clients.ts @@ -30,6 +30,12 @@ const ALL = [orderBookingClient, storeClient, dailyReportsClient]; export async function loginAll(email: string, password: string, orgId?: string) { const res = await orderBookingClient.login(email, password, orgId); ALL.forEach((c) => c.setToken(res.token)); + try { + const me = await orderBookingClient.getMe(); + res.user = { ...res.user, ...me }; + } catch (e) { + console.error('Failed to fetch user profile:', e); + } return res; } diff --git a/src/api/types.ts b/src/api/types.ts index 6328468..f7e0290 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -13,6 +13,7 @@ export interface User { mobile?: string; roles: string[]; groups: string[]; + is_admin?: boolean; } export interface LoginResponse { diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index 429005e..25d8746 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -1,5 +1,5 @@ -import { useState, type ReactNode } from 'react'; -import { loginAll, logoutAll, currentToken } from '../api/clients'; +import { useState, type ReactNode, useEffect } from 'react'; +import { loginAll, logoutAll, currentToken, orderBookingClient } from '../api/clients'; import { AuthCtx, type AuthValue } from './context'; export function AuthProvider({ children }: { children: ReactNode }) { @@ -7,9 +7,27 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [userEmail, setUserEmail] = useState(() => { return typeof window !== 'undefined' ? localStorage.getItem('krishna_sales_user_email') : null; }); + const [isAdmin, setIsAdmin] = useState(() => { + return orderBookingClient.currentUser()?.is_admin ?? false; + }); + const [roles, setRoles] = useState(() => { + return orderBookingClient.currentUser()?.roles ?? []; + }); + + // If we are authenticated but don't have isAdmin from cache, we might want to fetch it + useEffect(() => { + if (authed && (!isAdmin || roles.length === 0)) { + orderBookingClient.getMe().then(me => { + if (me.is_admin) setIsAdmin(true); + if (me.roles) setRoles(me.roles); + }).catch(console.error); + } + }, [authed]); const value: AuthValue = { authed, + isAdmin, + roles, userEmail, login: async (email, password, orgId) => { const res = await loginAll(email, password, orgId); @@ -18,11 +36,15 @@ export function AuthProvider({ children }: { children: ReactNode }) { setUserEmail(res.user.email); localStorage.setItem('krishna_sales_user_email', res.user.email); } + setIsAdmin(!!res.user?.is_admin); + setRoles(res.user?.roles ?? []); }, logout: () => { logoutAll(); setAuthed(false); setUserEmail(null); + setIsAdmin(false); + setRoles([]); localStorage.removeItem('krishna_sales_user_email'); }, }; diff --git a/src/auth/ProtectedRoute.tsx b/src/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..f6b7273 --- /dev/null +++ b/src/auth/ProtectedRoute.tsx @@ -0,0 +1,63 @@ +import { Navigate, useNavigate } from 'react-router-dom'; +import { ShieldAlert } from 'lucide-react'; +import { useAuth } from './context'; +import { Button } from '../components/buttons'; +import { Card } from '../components/reusable'; + +export function getDefaultRoute(roles: string[], isAdmin: boolean) { + if (isAdmin) return '/orders'; + if (roles.includes('Manager')) return '/orders'; + if (roles.includes('Sales Officer')) return '/my-orders'; + return '/stores'; +} + +function AccessDenied() { + const { roles, isAdmin } = useAuth(); + const navigate = useNavigate(); + return ( +
+ + +

Access Denied

+

+ You don't have permission to view this page. If you believe this is an error, please contact your administrator. +

+ +
+
+ ); +} + +export function ProtectedRoute({ + children, + roles, + adminOnly +}: { + children: React.ReactNode, + roles?: string[], + adminOnly?: boolean +}) { + const { roles: userRoles, isAdmin } = useAuth(); + + if (adminOnly && !isAdmin) { + return ; + } + + if (roles && roles.length > 0) { + const hasRole = roles.some(role => userRoles.includes(role)); + if (!hasRole) { + return ; + } + } + + return <>{children}; +} diff --git a/src/auth/context.ts b/src/auth/context.ts index 04b94a8..4ebfc09 100644 --- a/src/auth/context.ts +++ b/src/auth/context.ts @@ -2,6 +2,8 @@ import { createContext, useContext } from 'react'; export interface AuthValue { authed: boolean; + isAdmin: boolean; + roles: string[]; userEmail: string | null; login: (email: string, password: string, orgId?: string) => Promise; logout: () => void; diff --git a/src/routesConfig.tsx b/src/routesConfig.tsx new file mode 100644 index 0000000..0a7b129 --- /dev/null +++ b/src/routesConfig.tsx @@ -0,0 +1,107 @@ +import { OrdersPage } from './screens/OrdersPage' +import { MyOrdersPage } from './screens/MyOrdersPage' +import { CallsPage } from './screens/CallsPage' +import { MyCallsPage } from './screens/MyCallsPage' +import { StoresPage } from './screens/StoresPage' +import { DailyLogsPage } from './screens/DailyLogsPage' +import { MyDailyLogsPage } from './screens/MyDailyLogsPage' +import { DailySalesReportPage } from './screens/DailySalesReportPage' +import { ReportPage } from './screens/ReportPage' +import { DatasetsPage } from './screens/admin/DatasetsPage' + +export const routeConfig = [ + // Sales Officer + { + path: "/my-orders", + element: , + roles: ["Sales Officer"], + }, + { + path: "/my-orders/:instanceId", + element: , + roles: ["Sales Officer"], + }, + { + path: "/my-calls", + element: , + roles: ["Sales Officer"], + }, + { + path: "/my-calls/:instanceId", + element: , + roles: ["Sales Officer"], + }, + { + path: "/my-daily", + element: , + roles: ["Sales Officer"], + }, + { + path: "/my-daily/:instanceId", + element: , + roles: ["Sales Officer"], + }, + + // Manager & Admin + { + path: "/orders", + element: , + roles: ["Manager", "Admin"], + }, + { + path: "/orders/:instanceId", + element: , + roles: ["Manager", "Admin"], + }, + { + path: "/calls", + element: , + roles: ["Manager", "Admin"], + }, + { + path: "/calls/:instanceId", + element: , + roles: ["Manager", "Admin"], + }, + { + path: "/daily", + element: , + roles: ["Manager", "Admin"], + }, + { + path: "/daily/:instanceId", + element: , + roles: ["Manager", "Admin"], + }, + + // Everyone + { + path: "/stores", + element: , + roles: ["Sales Officer", "Manager", "Admin"], + }, + { + path: "/stores/:instanceId", + element: , + roles: ["Sales Officer", "Manager", "Admin"], + }, + { + path: "/reports/:reportType", + element: , + roles: ["Manager", "Admin"], + }, + + // Manager + Admin + { + path: "/sales-report", + element: , + roles: ["Manager", "Admin", "Sales Officer"], + }, + + // Admin Panel + { + path: "/admin/datasets", + element: , + adminOnly: true, + }, +]; diff --git a/src/screens/ConsoleLayout.tsx b/src/screens/ConsoleLayout.tsx index 2b5a620..c826cae 100644 --- a/src/screens/ConsoleLayout.tsx +++ b/src/screens/ConsoleLayout.tsx @@ -9,9 +9,22 @@ import { onAuthErrorAll, orderBookingClient } from '../api/clients'; import { SCREENS } from './tabs'; import { REPORT_MAP } from './ReportPage'; +import { routeConfig } from '../routesConfig'; + +const PATH_LABELS: Record = { + "/my-orders": "My Orders", + "/my-calls": "My Calls", + "/my-daily": "My Daily Logs", + "/orders": "Orders", + "/calls": "Calls", + "/daily": "Daily Logs", + "/stores": "Stores", + "/sales-report": "DSR", +}; + /** Auth-guarded shell: navy top bar + tab nav + routed . */ export function ConsoleLayout() { - const { authed, logout } = useAuth(); + const { authed, logout, isAdmin, roles: userRoles } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const user = orderBookingClient.currentUser(); @@ -28,6 +41,13 @@ export function ConsoleLayout() { if (!authed) return ; + const reportRoute = routeConfig.find(r => r.path === '/reports/:reportType'); + const canSeeReports = reportRoute && ( + (reportRoute.adminOnly && isAdmin) || + (reportRoute.roles && reportRoute.roles.some(r => userRoles.includes(r))) || + (!reportRoute.adminOnly && !reportRoute.roles) // open to all + ); + return (
@@ -35,11 +55,21 @@ export function ConsoleLayout() { Krishna Sales
-
- cn("w-full text-left px-3 py-2 text-sm font-medium hover:bg-indigo-50 hover:text-indigo-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer", isActive ? "text-indigo-700 bg-indigo-50" : "text-gray-700")} - > - - Manage Datasets - -
+ {isAdmin && ( +
+ cn("w-full text-left px-3 py-2 text-sm font-medium hover:bg-indigo-50 hover:text-indigo-700 rounded-md transition-colors flex items-center gap-2 cursor-pointer", isActive ? "text-indigo-700 bg-indigo-50" : "text-gray-700")} + > + + Manage Datasets + +
+ )}