From a2899a8101c2edb70f694638724ca976dc957a6c Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 28 Mar 2024 16:58:27 +0800 Subject: [PATCH] refactor(console): update tenant settings access per user tenant scopes (#5571) --- .../src/components/ActionsButton/index.tsx | 62 ++++---- .../src/containers/ConsoleContent/index.tsx | 20 ++- .../src/hooks/use-current-tenant-scopes.ts | 61 ++++++++ .../TenantBasicSettings/ProfileForm/index.tsx | 3 + .../TenantBasicSettings/index.tsx | 44 +++--- .../TenantMembers/Invitations/index.tsx | 133 ++++++++++-------- .../TenantMembers/InviteMemberModal/index.tsx | 5 +- .../TenantMembers/Members/index.tsx | 65 +++++---- .../TenantSettings/TenantMembers/index.tsx | 44 +++--- .../src/pages/TenantSettings/index.tsx | 12 +- 10 files changed, 288 insertions(+), 161 deletions(-) create mode 100644 packages/console/src/hooks/use-current-tenant-scopes.ts diff --git a/packages/console/src/components/ActionsButton/index.tsx b/packages/console/src/components/ActionsButton/index.tsx index 65f4c698e..41d6bac0e 100644 --- a/packages/console/src/components/ActionsButton/index.tsx +++ b/packages/console/src/components/ActionsButton/index.tsx @@ -13,8 +13,10 @@ import useActionTranslation from '@/hooks/use-action-translation'; import * as styles from './index.module.scss'; type Props = { - /** A function that will be called when the user confirms the deletion. */ - onDelete: () => void | Promise; + /** A function that will be called when the user confirms the deletion. If not provided, + * the delete button will not be displayed. + */ + onDelete?: () => void | Promise; /** * A function that will be called when the user clicks the edit button. If not provided, * the edit button will not be displayed. @@ -48,6 +50,10 @@ function ActionsButton({ onDelete, onEdit, deleteConfirmation, fieldName, textOv const [isDeleting, setIsDeleting] = useState(false); const handleDelete = useCallback(async () => { + if (!onDelete) { + return; + } + setIsDeleting(true); try { await onDelete(); @@ -69,31 +75,35 @@ function ActionsButton({ onDelete, onEdit, deleteConfirmation, fieldName, textOv )} )} - } - type="danger" - onClick={() => { - setIsModalOpen(true); - }} - > - {textOverrides?.delete ? ( - - ) : ( - tAction('delete', fieldName) - )} - + {onDelete && ( + } + type="danger" + onClick={() => { + setIsModalOpen(true); + }} + > + {textOverrides?.delete ? ( + + ) : ( + tAction('delete', fieldName) + )} + + )} - { - setIsModalOpen(false); - }} - onConfirm={handleDelete} - > - - + {onDelete && ( + { + setIsModalOpen(false); + }} + onConfirm={handleDelete} + > + + + )} ); } diff --git a/packages/console/src/containers/ConsoleContent/index.tsx b/packages/console/src/containers/ConsoleContent/index.tsx index 323bac321..1652fd5e1 100644 --- a/packages/console/src/containers/ConsoleContent/index.tsx +++ b/packages/console/src/containers/ConsoleContent/index.tsx @@ -15,6 +15,7 @@ import { import { isCloud, isDevFeaturesEnabled } from '@/consts/env'; import { TenantsContext } from '@/contexts/TenantsProvider'; import OverlayScrollbar from '@/ds-components/OverlayScrollbar'; +import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes'; import ApiResourceDetails from '@/pages/ApiResourceDetails'; import ApiResourcePermissions from '@/pages/ApiResourceDetails/ApiResourcePermissions'; import ApiResourceSettings from '@/pages/ApiResourceDetails/ApiResourceSettings'; @@ -74,6 +75,7 @@ import * as styles from './index.module.scss'; function ConsoleContent() { const { scrollableContent } = useOutletContext(); const { isDevTenant } = useContext(TenantsContext); + const { canManageTenant } = useCurrentTenantScopes(); return (
@@ -197,13 +199,25 @@ function ConsoleContent() { } /> {isCloud && ( }> - } /> + + } + /> } /> {isDevFeaturesEnabled && ( } /> )} - } /> - {!isDevTenant && ( + {canManageTenant && ( + } /> + )} + {!isDevTenant && canManageTenant && ( <> } /> } /> diff --git a/packages/console/src/hooks/use-current-tenant-scopes.ts b/packages/console/src/hooks/use-current-tenant-scopes.ts new file mode 100644 index 000000000..91fdce9d3 --- /dev/null +++ b/packages/console/src/hooks/use-current-tenant-scopes.ts @@ -0,0 +1,61 @@ +import { useLogto } from '@logto/react'; +import { TenantScope, getTenantOrganizationId } from '@logto/schemas'; +import { useContext, useEffect, useState } from 'react'; + +import { TenantsContext } from '@/contexts/TenantsProvider'; + +const useCurrentTenantScopes = () => { + const { currentTenantId, isInitComplete } = useContext(TenantsContext); + const { isAuthenticated, getOrganizationTokenClaims } = useLogto(); + + const [scopes, setScopes] = useState([]); + const [canInviteMember, setCanInviteMember] = useState(false); + const [canRemoveMember, setCanRemoveMember] = useState(false); + const [canUpdateMemberRole, setCanUpdateMemberRole] = useState(false); + const [canManageTenant, setCanManageTenant] = useState(false); + + useEffect(() => { + (async () => { + if (isAuthenticated && isInitComplete) { + const organizationId = getTenantOrganizationId(currentTenantId); + const claims = await getOrganizationTokenClaims(organizationId); + const allScopes = claims?.scope?.split(' ') ?? []; + setScopes(allScopes); + + for (const scope of allScopes) { + switch (scope) { + case TenantScope.InviteMember: { + setCanInviteMember(true); + break; + } + case TenantScope.RemoveMember: { + setCanRemoveMember(true); + break; + } + case TenantScope.UpdateMemberRole: { + setCanUpdateMemberRole(true); + break; + } + case TenantScope.ManageTenant: { + setCanManageTenant(true); + break; + } + default: { + break; + } + } + } + } + })(); + }, [currentTenantId, getOrganizationTokenClaims, isAuthenticated, isInitComplete]); + + return { + canInviteMember, + canRemoveMember, + canUpdateMemberRole, + canManageTenant, + scopes, + }; +}; + +export default useCurrentTenantScopes; diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/ProfileForm/index.tsx b/packages/console/src/pages/TenantSettings/TenantBasicSettings/ProfileForm/index.tsx index c66a6028c..f050d91de 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/ProfileForm/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/ProfileForm/index.tsx @@ -4,6 +4,7 @@ import FormCard from '@/components/FormCard'; import CopyToClipboard from '@/ds-components/CopyToClipboard'; import FormField from '@/ds-components/FormField'; import TextInput from '@/ds-components/TextInput'; +import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes'; import { type TenantSettingsForm } from '../types.js'; @@ -15,6 +16,7 @@ type Props = { }; function ProfileForm({ currentTenantId }: Props) { + const { canManageTenant } = useCurrentTenantScopes(); const { register, formState: { errors }, @@ -29,6 +31,7 @@ function ProfileForm({ currentTenantId }: Props) { diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx index 8c70c54a3..0c901d1cf 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx @@ -12,6 +12,7 @@ import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar' import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import { TenantsContext } from '@/contexts/TenantsProvider'; import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes'; import { trySubmitSafe } from '@/utils/form'; import DeleteCard from './DeleteCard'; @@ -28,6 +29,7 @@ const tenantProfileToForm = (tenant?: TenantResponse): TenantSettingsForm => { function TenantBasicSettings() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const { canManageTenant } = useCurrentTenantScopes(); const api = useCloudApi(); const { currentTenant, @@ -120,26 +122,34 @@ function TenantBasicSettings() {
- + {canManageTenant && ( + + )}
- + {canManageTenant && ( + + )} - - { - setIsDeletionModalOpen(false); - }} - onDelete={onDelete} - /> + {canManageTenant && ( + <> + + { + setIsDeletionModalOpen(false); + }} + onDelete={onDelete} + /> + + )} ); } diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/Invitations/index.tsx b/packages/console/src/pages/TenantSettings/TenantMembers/Invitations/index.tsx index 5199fdd40..d51f1c400 100644 --- a/packages/console/src/pages/TenantSettings/TenantMembers/Invitations/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantMembers/Invitations/index.tsx @@ -1,4 +1,5 @@ import { OrganizationInvitationStatus } from '@logto/schemas'; +import { condArray, conditional } from '@silverhand/essentials'; import { format } from 'date-fns'; import { useContext, useState } from 'react'; import { toast } from 'react-hot-toast'; @@ -13,7 +14,7 @@ import Redo from '@/assets/icons/redo.svg'; import UsersEmptyDark from '@/assets/images/users-empty-dark.svg'; import UsersEmpty from '@/assets/images/users-empty.svg'; import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api'; -import { type TenantInvitationResponse } from '@/cloud/types/router'; +import type { InvitationResponse, TenantInvitationResponse } from '@/cloud/types/router'; import { RoleOption } from '@/components/OrganizationRolesSelect'; import { TenantsContext } from '@/contexts/TenantsProvider'; import ActionMenu, { ActionMenuItem } from '@/ds-components/ActionMenu'; @@ -24,6 +25,7 @@ import TablePlaceholder from '@/ds-components/Table/TablePlaceholder'; import Tag, { type Props as TagProps } from '@/ds-components/Tag'; import { type RequestError } from '@/hooks/use-api'; import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes'; import InviteMemberModal from '../InviteMemberModal'; @@ -50,6 +52,7 @@ function Invitations() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenant_members' }); const cloudApi = useAuthedCloudApi(); const { currentTenantId } = useContext(TenantsContext); + const { canInviteMember, canRemoveMember } = useCurrentTenantScopes(); const { data, error, isLoading, mutate } = useSWR( `api/tenant/${currentTenantId}/invitations`, @@ -105,17 +108,19 @@ function Invitations() { imageDark={} title="tenant_members.invitation_empty_placeholder.title" description="tenant_members.invitation_empty_placeholder.description" - action={ -
} /> } /> - } /> + {canInviteMember && } />} - {showInviteModal && ( + {canInviteMember && ( { diff --git a/packages/console/src/pages/TenantSettings/index.tsx b/packages/console/src/pages/TenantSettings/index.tsx index 512ebaa11..a1a459a78 100644 --- a/packages/console/src/pages/TenantSettings/index.tsx +++ b/packages/console/src/pages/TenantSettings/index.tsx @@ -7,11 +7,13 @@ import { TenantsContext } from '@/contexts/TenantsProvider'; import CardTitle from '@/ds-components/CardTitle'; import DynamicT from '@/ds-components/DynamicT'; import TabNav, { TabNavItem } from '@/ds-components/TabNav'; +import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes'; import * as styles from './index.module.scss'; function TenantSettings() { const { isDevTenant } = useContext(TenantsContext); + const { canManageTenant } = useCurrentTenantScopes(); return (
@@ -29,10 +31,12 @@ function TenantSettings() { )} - - - - {!isDevTenant && ( + {canManageTenant && ( + + + + )} + {!isDevTenant && canManageTenant && ( <>