From 90d892090dffc0d008393689e058a453269882c1 Mon Sep 17 00:00:00 2001 From: suryacp23 Date: Thu, 30 Jul 2026 11:54:40 +0530 Subject: [PATCH] roles based access given --- src/App.tsx | 63 ++++++++++++------------- src/api/client.ts | 17 +++++++ src/api/clients.ts | 6 +++ src/api/types.ts | 1 + src/auth/AuthProvider.tsx | 44 ++++++++++++++++-- src/auth/ProtectedRoute.tsx | 63 +++++++++++++++++++++++++ src/auth/context.ts | 3 ++ src/routesConfig.tsx | 86 +++++++++++++++++++++++++++++++++++ src/screens/ConsoleLayout.tsx | 48 ++++++++++++------- src/screens/LoginPage.tsx | 2 - 10 files changed, 281 insertions(+), 52 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 e3d382a..784bc84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,15 +2,16 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' import { AuthProvider } from './auth/AuthProvider' import { LoginPage } from './screens/LoginPage' import { ConsoleLayout } from './screens/ConsoleLayout' -import { OrdersPage } from './screens/OrdersPage' -import { CallsPage } from './screens/CallsPage' -import { StoresPage } from './screens/StoresPage' -import { DailyLogsPage } from './screens/DailyLogsPage' -import { AllDailyLogsPage } from './screens/AllDailyLogsPage' -import { AnalyticsPage } from './screens/AnalyticsPage' -import { DailySalesReportPage } from './screens/DailySalesReportPage' -import { DatasetsPage } from './screens/admin/DatasetsPage' +import { ProtectedRoute, getDefaultRoute } from './auth/ProtectedRoute' +import { useAuth } from './auth/context' +import { routeConfig } from './routesConfig' import { DatasetItemsPage } from './screens/admin/DatasetItemsPage' + +function RootRedirect() { + const { roles, isAdmin } = useAuth(); + return ; +} + function App() { return ( @@ -18,31 +19,31 @@ function App() { } /> }> + {routeConfig.map((route, i) => { + const path = route.path.startsWith('/') ? route.path.substring(1) : route.path; + + if (path === 'admin/datasets') { + return ( + {route.element}} + > + } /> + + ); + } - } /> - } /> - - } /> - } /> - - } /> - } /> - - } /> - } /> - - } /> - } /> - - - } /> - } /> - - }> - } /> - + return ( + {route.element}} + /> + ); + })} - } /> + } /> diff --git a/src/api/client.ts b/src/api/client.ts index 291f3b2..4f354fd 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -110,6 +110,23 @@ export class ZinoClient { this.setToken(null); } + async getMe(): Promise { + const res = await this.request('GET', `/usr/app/${APP_ID}/me`); + if (typeof window !== 'undefined') { + try { + const storedUser = localStorage.getItem(TOKEN_KEY + '_user'); + if (storedUser) { + const u = JSON.parse(storedUser); + const updated = { ...u, ...res }; + localStorage.setItem(TOKEN_KEY + '_user', JSON.stringify(updated)); + } + } catch (e) { + // ignore + } + } + return res; + } + /** Decode the persisted JWT into a User (no network). */ 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 f2c9077..e1dc5ea 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -12,6 +12,7 @@ export interface User { email: string; roles: string[]; groups: string[]; + is_admin?: boolean; } export interface LoginResponse { diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index 5c59eab..0d25797 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -1,19 +1,57 @@ -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 }) { const [authed, setAuthed] = useState(() => !!currentToken()); + const [userEmail, setUserEmail] = useState(() => { + return typeof window !== 'undefined' ? localStorage.getItem('krishna_sales_mobile_user_email') : null; + }); + const [isAdmin, setIsAdmin] = useState(() => { + return typeof window !== 'undefined' ? localStorage.getItem('krishna_sales_mobile_is_admin') === 'true' : false; + }); + const [roles, setRoles] = useState(() => { + const r = typeof window !== 'undefined' ? localStorage.getItem('krishna_sales_mobile_roles') : null; + return r ? JSON.parse(r) : []; + }); + + 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) => { - await loginAll(email, password, orgId); + const res = await loginAll(email, password, orgId); setAuthed(true); + if (res.user?.email) { + setUserEmail(res.user.email); + localStorage.setItem('krishna_sales_mobile_user_email', res.user.email); + } + const userRoles = res.user?.roles ?? []; + const userIsAdmin = !!res.user?.is_admin; + setIsAdmin(userIsAdmin); + localStorage.setItem('krishna_sales_mobile_is_admin', userIsAdmin ? 'true' : 'false'); + setRoles(userRoles); + localStorage.setItem('krishna_sales_mobile_roles', JSON.stringify(userRoles)); }, logout: () => { logoutAll(); setAuthed(false); + setUserEmail(null); + setIsAdmin(false); + setRoles([]); + localStorage.removeItem('krishna_sales_mobile_user_email'); + localStorage.removeItem('krishna_sales_mobile_is_admin'); + localStorage.removeItem('krishna_sales_mobile_roles'); }, }; diff --git a/src/auth/ProtectedRoute.tsx b/src/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..4a9abcd --- /dev/null +++ b/src/auth/ProtectedRoute.tsx @@ -0,0 +1,63 @@ +import { 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 '/daily'; + 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 5dc72b7..4ebfc09 100644 --- a/src/auth/context.ts +++ b/src/auth/context.ts @@ -2,6 +2,9 @@ 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..395163d --- /dev/null +++ b/src/routesConfig.tsx @@ -0,0 +1,86 @@ +import { OrdersPage } from './screens/OrdersPage' +import { CallsPage } from './screens/CallsPage' +import { StoresPage } from './screens/StoresPage' +import { DailyLogsPage } from './screens/DailyLogsPage' +import { AllDailyLogsPage } from './screens/AllDailyLogsPage' +import { AnalyticsPage } from './screens/AnalyticsPage' +import { DailySalesReportPage } from './screens/DailySalesReportPage' +import { DatasetsPage } from './screens/admin/DatasetsPage' + +export const routeConfig = [ + // Manager & Admin (and maybe Sales Officer for daily logs, adapting from krishna_sales where my-daily was Sales Officer and daily was Manager/Admin. In mobile we have daily and all-daily) + { + path: "/daily", + element: , + roles: ["Sales Officer", "Manager", "Admin"], + }, + { + path: "/daily/:instanceId", + element: , + roles: ["Sales Officer", "Manager", "Admin"], + }, + { + path: "/all-daily", + element: , + roles: ["Manager", "Admin"], + }, + { + path: "/all-daily/:instanceId", + element: , + roles: ["Manager", "Admin"], + }, + + { + path: "/orders", + element: , + roles: ["Manager", "Admin", "Sales Officer"], + }, + { + path: "/orders/:instanceId", + element: , + roles: ["Manager", "Admin", "Sales Officer"], + }, + { + path: "/calls", + element: , + roles: ["Manager", "Admin", "Sales Officer"], + }, + { + path: "/calls/:instanceId", + element: , + roles: ["Manager", "Admin", "Sales Officer"], + }, + + // Everyone + { + path: "/stores", + element: , + roles: ["Sales Officer", "Manager", "Admin"], + }, + { + path: "/stores/:instanceId", + element: , + roles: ["Manager", "Admin", "Sales Officer"], + }, + + // Analytics + { + path: "/analytics", + element: , + roles: ["Manager", "Admin", "Sales Officer"], + }, + + // 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 e08ce7e..690f98c 100644 --- a/src/screens/ConsoleLayout.tsx +++ b/src/screens/ConsoleLayout.tsx @@ -5,11 +5,22 @@ import { cn } from '../lib/cn'; import { useAuth } from '../auth/context'; import { onAuthErrorAll, orderBookingClient } from '../api/clients'; import { SCREENS } from './tabs'; +import { routeConfig } from '../routesConfig'; import './screen.css'; +const canAccessScreen = (key: string, isAdmin: boolean, roles: string[]) => { + const route = routeConfig.find(r => r.path === `/${key}`); + if (!route) return true; + if (route.adminOnly && !isAdmin) return false; + if (!route.adminOnly && route.roles) { + return route.roles.some(r => roles.includes(r)); + } + return true; +}; + /** Auth-guarded shell: slim navy top bar + fixed bottom tab nav + routed . */ export function ConsoleLayout() { - const { authed, logout } = useAuth(); + const { authed, logout, isAdmin, roles: userRoles } = useAuth(); const navigate = useNavigate(); const user = orderBookingClient.currentUser(); @@ -52,9 +63,9 @@ export function ConsoleLayout() {