Skip to content

Commit

Permalink
Implement Permission Context and Update User Permissions Handling
Browse files Browse the repository at this point in the history
- Introduced a new PermissionContext to manage user permissions and super admin status across the application.
- Updated AppRouter to utilize PermissionProvider for managing permissions.
- Changed UserModel to store permissions as strings instead of objects.
- Refactored OrganizationFacilities and OrganizationPatients components to use updated permission checks and organization identifiers.
- Enhanced OrganizationLayout to conditionally render navigation items based on user permissions.

This commit improves the permission management system and ensures consistent handling of user permissions throughout the application.
  • Loading branch information
bodhish committed Jan 5, 2025
1 parent 35c0436 commit a6f4ab6
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 63 deletions.
54 changes: 30 additions & 24 deletions src/Routers/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import FacilityRoutes from "@/Routers/routes/FacilityRoutes";
import PatientRoutes from "@/Routers/routes/PatientRoutes";
import ResourceRoutes from "@/Routers/routes/ResourceRoutes";
import UserRoutes from "@/Routers/routes/UserRoutes";
import { PermissionProvider } from "@/context/PermissionContext";
import { PlugConfigEdit } from "@/pages/Apps/PlugConfigEdit";
import { PlugConfigList } from "@/pages/Apps/PlugConfigList";
import UserDashboard from "@/pages/UserDashboard";
Expand Down Expand Up @@ -87,32 +88,37 @@ export default function AppRouter() {

return (
<SidebarProvider>
{shouldShowSidebar && <AppSidebar user={user} />}
<main
id="pages"
className="flex-1 overflow-y-auto bg-gray-100 focus:outline-none md:pb-2 md:pr-2"
<PermissionProvider
userPermissions={user?.permissions || []}
isSuperAdmin={user?.is_superuser || false}
>
<div className="relative z-10 flex h-16 shrink-0 bg-white shadow md:hidden">
<div className="flex items-center">
{shouldShowSidebar && <SidebarTrigger />}
</div>
<a className="flex h-full w-full items-center px-4 md:hidden">
<img
className="h-8 w-auto"
src={careConfig.mainLogo?.dark}
alt="care logo"
/>
</a>
</div>
<div
className="max-w-8xl mx-auto mt-4 min-h-[96vh] rounded-lg border bg-gray-50 p-3 shadow"
data-cui-page
{shouldShowSidebar && <AppSidebar user={user} />}
<main
id="pages"
className="flex-1 overflow-y-auto bg-gray-100 focus:outline-none md:pb-2 md:pr-2"
>
<ErrorBoundary fallback={<ErrorPage forError="PAGE_LOAD_ERROR" />}>
{pages}
</ErrorBoundary>
</div>
</main>
<div className="relative z-10 flex h-16 shrink-0 bg-white shadow md:hidden">
<div className="flex items-center">
{shouldShowSidebar && <SidebarTrigger />}
</div>
<a className="flex h-full w-full items-center px-4 md:hidden">
<img
className="h-8 w-auto"
src={careConfig.mainLogo?.dark}
alt="care logo"
/>
</a>
</div>
<div
className="max-w-8xl mx-auto mt-4 min-h-[96vh] rounded-lg border bg-gray-50 p-3 shadow"
data-cui-page
>
<ErrorBoundary fallback={<ErrorPage forError="PAGE_LOAD_ERROR" />}>
{pages}
</ErrorBoundary>
</div>
</main>
</PermissionProvider>
</SidebarProvider>
);
}
3 changes: 1 addition & 2 deletions src/components/Users/models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { GENDER_TYPES, UserRole } from "@/common/constants";

import { FeatureFlag } from "@/Utils/featureFlags";
import { Organization } from "@/types/organization/organization";
import { Permission } from "@/types/permission/permission";

interface HomeFacilityObjectModel {
id?: string;
Expand Down Expand Up @@ -56,7 +55,7 @@ export type UserModel = UserBareMinimum & {
user_flags?: FeatureFlag[];
facilities?: UserFacilityModel[];
organizations?: Organization[];
permissions: Permission[];
permissions: string[];
};

export type UserBaseModel = {
Expand Down
73 changes: 73 additions & 0 deletions src/context/PermissionContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createContext, useContext, useMemo } from "react";

interface PermissionContextType {
/**
* Check if user has a specific permission in either their user permissions or object permissions
* @param permission - The permission to check
* @param objectPermissions - Optional object-specific permissions
*/
hasPermission: (permission: string, objectPermissions?: string[]) => boolean;
/**
* Raw permissions array from the user
*/
userPermissions: string[];
/**
* Whether the user is a super admin
*/
isSuperAdmin: boolean;
}

const PermissionContext = createContext<PermissionContextType | null>(null);

interface PermissionProviderProps {
children: React.ReactNode;
userPermissions: string[];
isSuperAdmin?: boolean;
}

export function PermissionProvider({
children,
userPermissions,
isSuperAdmin = false,
}: PermissionProviderProps) {
const value = useMemo(() => {
const hasPermission = (
permission: string,
objectPermissions?: string[],
) => {
if (isSuperAdmin) return true;
return (
userPermissions.includes(permission) ||
(objectPermissions?.includes(permission) ?? false)
);
};

return {
hasPermission,
userPermissions,
isSuperAdmin,
};
}, [userPermissions, isSuperAdmin]);

return (
<PermissionContext.Provider value={value}>
{children}
</PermissionContext.Provider>
);
}

export function usePermissions() {
const context = useContext(PermissionContext);
if (!context) {
throw new Error("usePermissions must be used within a PermissionProvider");
}
return context;
}

export function useHasPermission(
permission: string,
objectPermissions?: string[],
) {
const { hasPermission } = usePermissions();
return hasPermission(permission, objectPermissions);
}
2 changes: 1 addition & 1 deletion src/pages/Organization/OrganizationFacilities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function OrganizationFacilities({
page: qParams.page,
limit: resultsPerPage,
offset: (qParams.page - 1) * resultsPerPage,
geo_organization: id,
organization: id,
name: qParams.name,
...advancedFilter.filter,
},
Expand Down
13 changes: 10 additions & 3 deletions src/pages/Organization/OrganizationPatients.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { Link } from "raviger";
import { useState } from "react";

import RecordMeta from "@/CAREUI/display/RecordMeta";
import CareIcon from "@/CAREUI/icons/CareIcon";
Expand All @@ -14,6 +15,7 @@ import useFilters from "@/hooks/useFilters";

import query from "@/Utils/request/query";
import { Patient } from "@/types/emr/newPatient";
import { Organization } from "@/types/organization/organization";
import organizationApi from "@/types/organization/organizationApi";

import OrganizationLayout from "./components/OrganizationLayout";
Expand All @@ -26,28 +28,33 @@ interface Props {
export default function OrganizationPatients({ id, navOrganizationId }: Props) {
const { qParams, Pagination, advancedFilter, resultsPerPage, updateQuery } =
useFilters({ limit: 14, cacheBlacklist: ["patient"] });
const [organization, setOrganization] = useState<Organization | null>(null);

const { data: patients, isLoading } = useQuery({
queryKey: ["organizationPatients", id, qParams],
queryFn: query(organizationApi.listPatients, {
pathParams: { id },
queryParams: {
geo_organization: id,
...(organization?.org_type === "govt" && { organization: id }),
page: qParams.page,
limit: resultsPerPage,
offset: (qParams.page - 1) * resultsPerPage,
...advancedFilter.filter,
},
}),
enabled: !!id,
enabled: !!id && !!organization,
});

if (!id) {
return null;
}

return (
<OrganizationLayout id={id} navOrganizationId={navOrganizationId}>
<OrganizationLayout
id={id}
navOrganizationId={navOrganizationId}
setOrganization={setOrganization}
>
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">Patients</h2>
Expand Down
85 changes: 52 additions & 33 deletions src/pages/Organization/components/OrganizationLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { Link, usePath } from "raviger";
import { useEffect } from "react";

import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon";

Expand All @@ -15,6 +16,7 @@ import { Menubar, MenubarMenu, MenubarTrigger } from "@/components/ui/menubar";
import Page from "@/components/Common/Page";

import query from "@/Utils/request/query";
import { usePermissions } from "@/context/PermissionContext";
import {
Organization,
OrganizationParent,
Expand All @@ -26,63 +28,78 @@ interface Props {
navOrganizationId?: string;
id: string;
children: React.ReactNode;
setOrganization?: (org: Organization) => void;
}

interface NavItem {
path: string;
title: string;
icon: IconName;
visibility: boolean;
}

export default function OrganizationLayout({
id,
navOrganizationId,
children,
setOrganization,
}: Props) {
const path = usePath() || "";
const { hasPermission } = usePermissions();

const baseUrl = navOrganizationId
? `/organization/${navOrganizationId}/children`
: `/organization`;

const { data: org, isLoading } = useQuery<Organization>({
queryKey: ["organization", id],
queryFn: query(organizationApi.get, {
pathParams: { id },
}),
enabled: !!id,
});

useEffect(() => {
if (org) {
setOrganization?.(org);
}
}, [org, setOrganization]);

if (isLoading) {
return <div>Loading...</div>;
}
// add loading state
if (!org) {
return <div>Not found</div>;
}

const navItems: NavItem[] = [
{
path: `${baseUrl}/${id}`,
title: "Organizations",
icon: "d-hospital",
visibility: hasPermission("can_view_organization", org.permissions),
},
{
path: `${baseUrl}/${id}/users`,
title: "Users",
icon: "d-people",
visibility: hasPermission("can_list_organization_users", org.permissions),
},
{
path: `${baseUrl}/${id}/patients`,
title: "Patients",
icon: "d-patient",
visibility: hasPermission("can_list_patients", org.permissions),
},
{
path: `${baseUrl}/${id}/facilities`,
title: "Facilities",
icon: "d-hospital",
visibility: hasPermission("can_read_facility", org.permissions),
},
];

const { data: org, isLoading } = useQuery<Organization>({
queryKey: ["organization", id],
queryFn: query(organizationApi.get, {
pathParams: { id },
}),
enabled: !!id,
});

if (isLoading) {
return <div>Loading...</div>;
}
// add loading state
if (!org) {
return <div>Not found</div>;
}

const orgParents: OrganizationParent[] = [];
let currentParent = org.parent;
while (currentParent) {
Expand Down Expand Up @@ -121,23 +138,25 @@ export default function OrganizationLayout({
{/* Navigation */}
<div className="mt-4">
<Menubar>
{navItems.map((item) => (
<MenubarMenu key={item.path}>
<MenubarTrigger
className={`${
path === item.path
? "font-medium text-primary-700 bg-gray-100"
: "hover:text-primary-500 hover:bg-gray-100 text-gray-700"
}`}
asChild
>
<Link href={item.path} className="cursor-pointer">
<CareIcon icon={item.icon} className="mr-2 h-4 w-4" />
{item.title}
</Link>
</MenubarTrigger>
</MenubarMenu>
))}
{navItems
.filter((item) => item.visibility)
.map((item) => (
<MenubarMenu key={item.path}>
<MenubarTrigger
className={`${
path === item.path
? "font-medium text-primary-700 bg-gray-100"
: "hover:text-primary-500 hover:bg-gray-100 text-gray-700"
}`}
asChild
>
<Link href={item.path} className="cursor-pointer">
<CareIcon icon={item.icon} className="mr-2 h-4 w-4" />
{item.title}
</Link>
</MenubarTrigger>
</MenubarMenu>
))}
</Menubar>
</div>
{/* Page Content */}
Expand Down
1 change: 1 addition & 0 deletions src/types/organization/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface Organization {
created_at: string;
updated_at: string;
metadata: Metadata | null;
permissions: string[];
}

export interface OrganizationUserRole {
Expand Down

0 comments on commit a6f4ab6

Please sign in to comment.