From 42cc8147df585a30257645332fc6cadf8eeae9dd Mon Sep 17 00:00:00 2001 From: Praveen Yadav Date: Sat, 9 Sep 2023 17:09:54 +0530 Subject: [PATCH] feat: support check permission for workspace actions --- .../components/organization/domain/index.tsx | 50 ++++++++++--- .../general/general.workspace.tsx | 18 ++--- .../components/organization/general/index.tsx | 72 ++++++------------ .../components/organization/members/index.tsx | 46 +++++++++--- .../organization/members/member.types.tsx | 3 +- .../components/organization/project/index.tsx | 48 ++++++++++-- .../organization/security/index.tsx | 39 ++++++++-- .../organization/security/security.types.tsx | 1 + .../components/organization/teams/index.tsx | 54 ++++++++++--- .../core/react/hooks/usePermissions.ts | 48 ++++++++++++ sdks/js/packages/core/utils/index.ts | 75 +++++++++++++++++-- 11 files changed, 344 insertions(+), 110 deletions(-) create mode 100644 sdks/js/packages/core/react/hooks/usePermissions.ts diff --git a/sdks/js/packages/core/react/components/organization/domain/index.tsx b/sdks/js/packages/core/react/components/organization/domain/index.tsx index c2edea493..ba75dca66 100644 --- a/sdks/js/packages/core/react/components/organization/domain/index.tsx +++ b/sdks/js/packages/core/react/components/organization/domain/index.tsx @@ -4,7 +4,9 @@ import { Button, DataTable, EmptyState, Flex, Text } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFrontier } from '~/react/contexts/FrontierContext'; +import { usePermissions } from '~/react/hooks/usePermissions'; import { V1Beta1Domain } from '~/src'; +import { PERMISSIONS, shouldShowComponent } from '~/utils'; import { styles } from '../styles'; import { getColumns } from './domain.columns'; @@ -50,6 +52,28 @@ export default function Domain() { [isDomainsLoading, domains] ); + const resource = `app/organization:${organization?.id}`; + const listOfPermissionsToCheck = [ + { + permission: PERMISSIONS.UpdatePermission, + resource + } + ]; + + const { permissions } = usePermissions( + listOfPermissionsToCheck, + !!organization?.id + ); + + const { canCreateDomain } = useMemo(() => { + return { + canCreateDomain: shouldShowComponent( + permissions, + `${PERMISSIONS.UpdatePermission}::${resource}` + ) + }; + }, [permissions, resource]); + return ( @@ -59,7 +83,11 @@ export default function Domain() { {/* @ts-ignore */} - + @@ -84,10 +112,12 @@ const AllowedEmailDomains = () => { const Domains = ({ domains, - isLoading + isLoading, + canCreateDomain }: { domains: V1Beta1Domain[]; isLoading?: boolean; + canCreateDomain?: boolean; }) => { let navigate = useNavigate({ from: '/domains' }); @@ -115,13 +145,15 @@ const Domains = ({ /> - + {canCreateDomain ? ( + + ) : null} diff --git a/sdks/js/packages/core/react/components/organization/general/general.workspace.tsx b/sdks/js/packages/core/react/components/organization/general/general.workspace.tsx index 53ddcc0d5..7e129b382 100644 --- a/sdks/js/packages/core/react/components/organization/general/general.workspace.tsx +++ b/sdks/js/packages/core/react/components/organization/general/general.workspace.tsx @@ -14,7 +14,7 @@ import { toast } from 'sonner'; import * as yup from 'yup'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { V1Beta1Organization } from '~/src'; -import { PERMISSIONS } from '~/utils'; +import { PERMISSIONS, shouldShowComponent } from '~/utils'; const generalSchema = yup .object({ @@ -26,13 +26,13 @@ const generalSchema = yup export const GeneralOrganization = ({ organization, isLoading, - organizationPermissions, - permissionMap + permissions, + resourceMap }: { organization?: V1Beta1Organization; isLoading?: boolean; - organizationPermissions?: Record; - permissionMap?: Record; + permissions?: Record; + resourceMap?: Record; }) => { const { client } = useFrontier(); const { @@ -116,10 +116,10 @@ export const GeneralOrganization = ({ - {organizationPermissions && - organizationPermissions[ - `${PERMISSIONS.PUT}::${permissionMap?.Organization}` - ] ? ( + {shouldShowComponent( + permissions, + `${PERMISSIONS.UPDATE}::${resourceMap?.organization}` + ) ? ( + {canCreateInvite ? ( + + ) : null} diff --git a/sdks/js/packages/core/react/components/organization/members/member.types.tsx b/sdks/js/packages/core/react/components/organization/members/member.types.tsx index 027e87aea..7e933c51f 100644 --- a/sdks/js/packages/core/react/components/organization/members/member.types.tsx +++ b/sdks/js/packages/core/react/components/organization/members/member.types.tsx @@ -9,7 +9,8 @@ export enum MemberActionmethods { } export type MembersTableType = { + isLoading?: boolean; users: User[]; organizationId: string; - isLoading?: boolean; + canCreateInvite?: boolean; }; diff --git a/sdks/js/packages/core/react/components/organization/project/index.tsx b/sdks/js/packages/core/react/components/organization/project/index.tsx index 51fb49504..f90add6ec 100644 --- a/sdks/js/packages/core/react/components/organization/project/index.tsx +++ b/sdks/js/packages/core/react/components/organization/project/index.tsx @@ -4,7 +4,9 @@ import { Button, DataTable, EmptyState, Flex, Text } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFrontier } from '~/react/contexts/FrontierContext'; +import { usePermissions } from '~/react/hooks/usePermissions'; import { V1Beta1Project } from '~/src'; +import { PERMISSIONS, shouldShowComponent } from '~/utils'; import { styles } from '../styles'; import { getColumns } from './projects.columns'; @@ -49,6 +51,28 @@ export default function WorkspaceProjects() { : [], [isProjectsLoading, projects] ); + const resource = `app/organization:${organization?.id}`; + const listOfPermissionsToCheck = [ + { + permission: PERMISSIONS.ProjectCreatePermission, + resource + } + ]; + + const { permissions } = usePermissions( + listOfPermissionsToCheck, + !!organization?.id + ); + + const { canCreateProject } = useMemo(() => { + return { + canCreateProject: shouldShowComponent( + permissions, + `${PERMISSIONS.ProjectCreatePermission}::${resource}` + ) + }; + }, [permissions, resource]); + return ( @@ -60,6 +84,7 @@ export default function WorkspaceProjects() { // @ts-ignore projects={updatedProjects} isLoading={isProjectsLoading} + canCreateProject={canCreateProject} /> @@ -71,9 +96,14 @@ export default function WorkspaceProjects() { interface WorkspaceProjectsProps { projects: V1Beta1Project[]; isLoading?: boolean; + canCreateProject?: boolean; } -const ProjectsTable = ({ projects, isLoading }: WorkspaceProjectsProps) => { +const ProjectsTable = ({ + projects, + isLoading, + canCreateProject +}: WorkspaceProjectsProps) => { let navigate = useNavigate({ from: '/projects' }); const tableStyle = projects?.length @@ -100,13 +130,15 @@ const ProjectsTable = ({ projects, isLoading }: WorkspaceProjectsProps) => { /> - + {canCreateProject ? ( + + ) : null} diff --git a/sdks/js/packages/core/react/components/organization/security/index.tsx b/sdks/js/packages/core/react/components/organization/security/index.tsx index 8b39d8f9c..b0d8a4bee 100644 --- a/sdks/js/packages/core/react/components/organization/security/index.tsx +++ b/sdks/js/packages/core/react/components/organization/security/index.tsx @@ -3,7 +3,9 @@ import { Box, Flex, Separator, Switch, Text } from '@raystack/apsara'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFrontier } from '~/react/contexts/FrontierContext'; +import { usePermissions } from '~/react/hooks/usePermissions'; import { V1Beta1Preference } from '~/src'; +import { PERMISSIONS, shouldShowComponent } from '~/utils'; import { styles } from '../styles'; import type { SecurityCheckboxTypes } from './security.types'; @@ -68,6 +70,23 @@ export default function WorkspaceSecurity() { [client, organization?.id] ); + const listOfPermissionsToCheck = [ + { + permission: PERMISSIONS.UPDATE, + resource: `app/organization:${organization?.id}` + } + ]; + + const { permissions } = usePermissions( + listOfPermissionsToCheck, + !!organization?.id + ); + + const canUpdatePrefrence = shouldShowComponent( + permissions, + `${PERMISSIONS.UPDATE}::app/organization:${organization?.id}` + ); + return ( @@ -79,6 +98,7 @@ export default function WorkspaceSecurity() { text="Allow logins through Google's single sign-on functionality" name="social_login" value={socialLogin} + canUpdatePrefrence={canUpdatePrefrence} onValueChange={onValueChange} /> @@ -88,6 +108,7 @@ export default function WorkspaceSecurity() { over email." name="mail_link" value={mailLink} + canUpdatePrefrence={canUpdatePrefrence} onValueChange={onValueChange} /> @@ -112,7 +133,8 @@ export const SecurityCheckbox = ({ text, name, value, - onValueChange + onValueChange, + canUpdatePrefrence }: SecurityCheckboxTypes) => { return ( @@ -122,12 +144,15 @@ export const SecurityCheckbox = ({ {text} - {/* @ts-ignore */} - onValueChange(name, checked)} - /> + + {canUpdatePrefrence ? ( + onValueChange(name, checked)} + /> + ) : null} ); }; diff --git a/sdks/js/packages/core/react/components/organization/security/security.types.tsx b/sdks/js/packages/core/react/components/organization/security/security.types.tsx index 2d1959369..1b37aa6b7 100644 --- a/sdks/js/packages/core/react/components/organization/security/security.types.tsx +++ b/sdks/js/packages/core/react/components/organization/security/security.types.tsx @@ -3,5 +3,6 @@ export type SecurityCheckboxTypes = { name: string; text: string; value: boolean; + canUpdatePrefrence?: boolean; onValueChange: (key: string, checked: boolean) => void; }; diff --git a/sdks/js/packages/core/react/components/organization/teams/index.tsx b/sdks/js/packages/core/react/components/organization/teams/index.tsx index b4634d1da..4fc1c4cff 100644 --- a/sdks/js/packages/core/react/components/organization/teams/index.tsx +++ b/sdks/js/packages/core/react/components/organization/teams/index.tsx @@ -4,13 +4,16 @@ import { Button, DataTable, EmptyState, Flex, Text } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFrontier } from '~/react/contexts/FrontierContext'; +import { usePermissions } from '~/react/hooks/usePermissions'; import { V1Beta1Group } from '~/src'; +import { PERMISSIONS, shouldShowComponent } from '~/utils'; import { styles } from '../styles'; import { getColumns } from './teams.columns'; interface WorkspaceTeamProps { teams: V1Beta1Group[]; isLoading?: boolean; + canCreateGroup?: boolean; } export default function WorkspaceTeams() { @@ -55,6 +58,28 @@ export default function WorkspaceTeams() { [isTeamsLoading, teams] ); + const resource = `app/organization:${organization?.id}`; + const listOfPermissionsToCheck = [ + { + permission: PERMISSIONS.GroupCreatePermission, + resource + } + ]; + + const { permissions } = usePermissions( + listOfPermissionsToCheck, + !!organization?.id + ); + + const { canCreateGroup } = useMemo(() => { + return { + canCreateGroup: shouldShowComponent( + permissions, + `${PERMISSIONS.GroupCreatePermission}::${resource}` + ) + }; + }, [permissions, resource]); + return ( @@ -62,8 +87,11 @@ export default function WorkspaceTeams() { - {/* @ts-ignore */} - + @@ -71,7 +99,11 @@ export default function WorkspaceTeams() { ); } -const TeamsTable = ({ teams, isLoading }: WorkspaceTeamProps) => { +const TeamsTable = ({ + teams, + isLoading, + canCreateGroup +}: WorkspaceTeamProps) => { let navigate = useNavigate({ from: '/members' }); const tableStyle = teams?.length @@ -99,13 +131,15 @@ const TeamsTable = ({ teams, isLoading }: WorkspaceTeamProps) => { /> - + {canCreateGroup ? ( + + ) : null} diff --git a/sdks/js/packages/core/react/hooks/usePermissions.ts b/sdks/js/packages/core/react/hooks/usePermissions.ts new file mode 100644 index 000000000..263715a44 --- /dev/null +++ b/sdks/js/packages/core/react/hooks/usePermissions.ts @@ -0,0 +1,48 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { V1Beta1BatchCheckPermissionBody } from '~/src'; +import { formatPermissions } from '~/utils'; +import { useFrontier } from '../contexts/FrontierContext'; + +export const usePermissions = ( + permissions: V1Beta1BatchCheckPermissionBody[] = [], + shouldCalled: boolean | undefined = true +) => { + const [permisionValues, setPermisionValues] = useState([]); + const [fetchingPermissions, setFetchingOrgPermissions] = useState(false); + + const { client, activeOrganization: organization } = useFrontier(); + + const fetchOrganizationPermissions = useCallback(async () => { + try { + setFetchingOrgPermissions(true); + const { + // @ts-ignore + data: { pairs } + } = await client?.frontierServiceBatchCheckPermission({ + bodies: permissions + }); + setPermisionValues(pairs); + } catch (err) { + console.error(err); + } finally { + setFetchingOrgPermissions(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [client]); + + useEffect(() => { + if (shouldCalled) { + fetchOrganizationPermissions(); + } + }, [fetchOrganizationPermissions, shouldCalled]); + + const permissionsMap = useMemo(() => { + if (permisionValues.length) { + return formatPermissions(permisionValues); + } else { + return {}; + } + }, [permisionValues]); + + return { isFetching: fetchingPermissions, permissions: permissionsMap }; +}; diff --git a/sdks/js/packages/core/utils/index.ts b/sdks/js/packages/core/utils/index.ts index 5322471f1..1fa942fb4 100644 --- a/sdks/js/packages/core/utils/index.ts +++ b/sdks/js/packages/core/utils/index.ts @@ -26,6 +26,11 @@ export const filterUsersfromUsers = ( export const PERMISSIONS = { + GET: 'get', + PUT: 'put', + POST: 'post', + UPDATE: 'update', + DELETE: 'delete', ADMINISTER: 'administer', GROUPCREATE: 'groupcreate', GROUPLIST: 'grouplist', @@ -37,18 +42,74 @@ export const PERMISSIONS = { RESOURCELIST: 'resourcelist', ROLEMANAGE: 'rolemanage', SERVICEUSERMANAGE: 'serviceusermanage', - GET: 'get', - PUT: 'put', - POST: 'post', - UPDATE: 'update', - DELETE: 'delete' + + // namespace + PlatformNamespace: 'app/platform', + OrganizationNamespace: 'app/organization', + ProjectNamespace: 'app/project', + GroupNamespace: 'app/group', + RoleBindingNamespace: 'app/rolebinding', + RoleNamespace: 'app/role', + InvitationNamespace: 'app/invitation', + + // relations + PlatformRelationName: 'platform', + AdminRelationName: 'admin', + OrganizationRelationName: 'org', + UserRelationName: 'user', + ProjectRelationName: 'project', + GroupRelationName: 'group', + MemberRelationName: 'member', + OwnerRelationName: 'owner', + RoleRelationName: 'role', + RoleGrantRelationName: 'granted', + RoleBearerRelationName: 'bearer', + + // permissions + ListPermission: 'list', + GetPermission: 'get', + CreatePermission: 'create', + UpdatePermission: 'update', + DeletePermission: 'delete', + SudoPermission: 'superuser', + RoleManagePermission: 'rolemanage', + PolicyManagePermission: 'policymanage', + ProjectListPermission: 'projectlist', + GroupListPermission: 'grouplist', + ProjectCreatePermission: 'projectcreate', + GroupCreatePermission: 'groupcreate', + ResourceListPermission: 'resourcelist', + InvitationListPermission: 'invitationlist', + InvitationCreatePermission: 'invitationcreate', + AcceptPermission: 'accept', + ServiceUserManagePermission: 'serviceusermanage', + ManagePermission: 'manage', + + // synthetic permission + MembershipPermission: 'membership', + + // principals + UserPrincipal: 'app/user', + ServiceUserPrincipal: 'app/serviceuser', + GroupPrincipal: 'app/group', + SuperUserPrincipal: 'app/superuser', + + // Roles + RoleProjectOwner: 'app_project_owner' }; export const formatPermissions = ( permisions: { body: any; status: boolean }[] = [] -): Record => +): Record => permisions.reduce((acc: any, p: any) => { const { body, status } = p; acc[`${body.permission}::${body.resource}`] = status; return acc; - }, {}); \ No newline at end of file + }, {}); + +export const shouldShowComponent = ( + permissions: Record = {}, + permisionsRequired: string +) => { + return permissions[permisionsRequired] === true; +}; \ No newline at end of file