0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(console): assign permissions for org roles (#5664)

This commit is contained in:
Xiao Yijun 2024-04-11 09:50:33 +08:00 committed by GitHub
parent c1c91b6ab8
commit 07ed139d6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 416 additions and 14 deletions

View file

@ -0,0 +1,125 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import ConfirmModal from '@/ds-components/ConfirmModal';
import DataTransferBox from '@/ds-components/DataTransferBox';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import TabWrapper from '@/ds-components/TabWrapper';
import { PermissionType } from './types';
import useOrganizationRolePermissionsAssignment from './use-organization-role-permissions-assignment';
const permissionTabs = {
[PermissionType.Organization]: {
title: 'organization_role_details.permissions.organization_permissions',
key: PermissionType.Organization,
},
[PermissionType.Api]: {
title: 'organization_role_details.permissions.api_permissions',
key: PermissionType.Api,
},
} satisfies {
[key in PermissionType]: {
title: AdminConsoleKey;
key: key;
};
};
type Props = {
organizationRoleId: string;
isOpen: boolean;
onClose: () => void;
};
function OrganizationRolePermissionsAssignmentModal({
organizationRoleId,
isOpen,
onClose,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
activeTab,
setActiveTab,
onSubmit,
organizationScopesAssignment,
resourceScopesAssignment,
clearSelectedData,
isLoading,
} = useOrganizationRolePermissionsAssignment(organizationRoleId);
const onCloseHandler = useCallback(() => {
onClose();
clearSelectedData();
setActiveTab(PermissionType.Organization);
}, [clearSelectedData, onClose, setActiveTab]);
const onSubmitHandler = useCallback(async () => {
await onSubmit();
onCloseHandler();
}, [onCloseHandler, onSubmit]);
const tabs = useMemo(
() =>
Object.values(permissionTabs).map(({ title, key }) => {
const selectedDataCount =
key === PermissionType.Organization
? organizationScopesAssignment.selectedData.length
: resourceScopesAssignment.selectedData.length;
return (
<TabNavItem
key={key}
isActive={key === activeTab}
onClick={() => {
setActiveTab(key);
}}
>
{`${t(title)}${selectedDataCount ? ` (${selectedDataCount})` : ''}`}
</TabNavItem>
);
}),
[
activeTab,
organizationScopesAssignment.selectedData.length,
resourceScopesAssignment.selectedData.length,
setActiveTab,
t,
]
);
return (
<ConfirmModal
isOpen={isOpen}
isLoading={isLoading}
title="organization_role_details.permissions.assign_permissions"
subtitle="organization_role_details.permissions.assign_description"
confirmButtonType="primary"
confirmButtonText="general.save"
cancelButtonText="general.discard"
size="large"
onCancel={onCloseHandler}
onConfirm={onSubmitHandler}
>
<TabNav>{tabs}</TabNav>
<TabWrapper
key={PermissionType.Organization}
isActive={PermissionType.Organization === activeTab}
>
<DataTransferBox
title="organization_role_details.permissions.assign_organization_permissions"
{...organizationScopesAssignment}
/>
</TabWrapper>
<TabWrapper key={PermissionType.Api} isActive={PermissionType.Api === activeTab}>
<DataTransferBox
title="organization_role_details.permissions.assign_api_permissions"
{...resourceScopesAssignment}
/>
</TabWrapper>
</ConfirmModal>
);
}
export default OrganizationRolePermissionsAssignmentModal;

View file

@ -0,0 +1,4 @@
export enum PermissionType {
Organization = 'Organization',
Api = 'Api',
}

View file

@ -0,0 +1,82 @@
import { cond } from '@silverhand/essentials';
import { useCallback, useMemo, useState } from 'react';
import useApi from '@/hooks/use-api';
import useOrganizationRoleScopes from '@/pages/OrganizationRoleDetails/Permissions/use-organization-role-scopes';
import { PermissionType } from './types';
import useOrganizationScopesAssignment from './use-organization-scopes-assignment';
import useResourceScopesAssignment from './use-resource-scopes-assignment';
function useOrganizationRolePermissionsAssignment(organizationRoleId: string) {
const organizationRolePath = `api/organization-roles/${organizationRoleId}`;
const [activeTab, setActiveTab] = useState<PermissionType>(PermissionType.Organization);
const [isLoading, setIsLoading] = useState(false);
const api = useApi();
const { organizationScopes, resourceScopes, mutate } =
useOrganizationRoleScopes(organizationRoleId);
const organizationScopesAssignment = useOrganizationScopesAssignment(organizationScopes);
const resourceScopesAssignment = useResourceScopesAssignment(resourceScopes);
const clearSelectedData = useCallback(() => {
organizationScopesAssignment.setSelectedData([]);
resourceScopesAssignment.setSelectedData([]);
}, [organizationScopesAssignment, resourceScopesAssignment]);
const onSubmit = useCallback(async () => {
setIsLoading(true);
const newOrganizationScopes = organizationScopesAssignment.selectedData.map(({ id }) => id);
const newResourceScopes = resourceScopesAssignment.selectedData.map(({ id }) => id);
await Promise.all(
[
cond(
newOrganizationScopes.length > 0 &&
api.post(`${organizationRolePath}/scopes`, {
json: { organizationScopeIds: newOrganizationScopes },
})
),
cond(
newResourceScopes.length > 0 &&
api.post(`${organizationRolePath}/resource-scopes`, {
json: { scopeIds: newResourceScopes },
})
),
].filter(Boolean)
).finally(() => {
setIsLoading(false);
});
mutate();
}, [
api,
mutate,
organizationRolePath,
organizationScopesAssignment.selectedData,
resourceScopesAssignment.selectedData,
]);
return useMemo(
() => ({
activeTab,
setActiveTab,
isLoading,
organizationScopesAssignment,
resourceScopesAssignment,
clearSelectedData,
onSubmit,
}),
[
activeTab,
clearSelectedData,
isLoading,
onSubmit,
organizationScopesAssignment,
resourceScopesAssignment,
]
);
}
export default useOrganizationRolePermissionsAssignment;

View file

@ -0,0 +1,28 @@
import { type OrganizationScope } from '@logto/schemas';
import { useMemo, useState } from 'react';
import useSWR from 'swr';
function useOrganizationScopesAssignment(assignedScopes: OrganizationScope[] = []) {
const [selectedData, setSelectedData] = useState<OrganizationScope[]>([]);
const { data: organizationScopes } = useSWR<OrganizationScope[]>('api/organization-scopes');
const availableDataList = useMemo(
() =>
(organizationScopes ?? []).filter(
({ id }) => !assignedScopes.some((scope) => scope.id === id)
),
[organizationScopes, assignedScopes]
);
return useMemo(
() => ({
selectedData,
setSelectedData,
availableDataList,
}),
[selectedData, setSelectedData, availableDataList]
);
}
export default useOrganizationScopesAssignment;

View file

@ -0,0 +1,42 @@
import { isManagementApi, type Scope, type ResourceResponse } from '@logto/schemas';
import { useMemo, useState } from 'react';
import useSWR from 'swr';
import { type DataGroup } from '@/ds-components/DataTransferBox/type';
function useResourceScopesAssignment(assignedScopes?: Scope[]) {
const [selectedData, setSelectedData] = useState<Scope[]>([]);
const { data: allResources } = useSWR<ResourceResponse[]>('api/resources?includeScopes=true');
const availableDataGroups: Array<DataGroup<Scope>> = useMemo(() => {
if (!allResources) {
return [];
}
const resourcesWithScopes = allResources
// Filter out the management APIs
.filter((resource) => !isManagementApi(resource.indicator))
.map(({ name, scopes, id: resourceId }) => ({
groupId: resourceId,
groupName: name,
dataList: scopes
// Filter out the scopes that have been assigned
.filter(({ id: scopeId }) => !assignedScopes?.some((scope) => scope.id === scopeId)),
}));
// Filter out the resources that have no scopes
return resourcesWithScopes.filter(({ dataList }) => dataList.length > 0);
}, [allResources, assignedScopes]);
return useMemo(
() => ({
selectedData,
setSelectedData,
availableDataGroups,
}),
[availableDataGroups, selectedData]
);
}
export default useResourceScopesAssignment;

View file

@ -8,6 +8,7 @@ import ActionsButton from '@/components/ActionsButton';
import Breakable from '@/components/Breakable';
import EditScopeModal, { type EditScopeData } from '@/components/EditScopeModal';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import OrganizationRolePermissionsAssignmentModal from '@/components/OrganizationRolePermissionsAssignmentModal';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
import Search from '@/ds-components/Search';
@ -77,6 +78,8 @@ function Permissions({ organizationRoleId }: Props) {
mutate();
};
const [isAssignScopesModalOpen, setIsAssignScopesModalOpen] = useState(false);
return (
<>
<Table
@ -161,7 +164,7 @@ function Permissions({ organizationRoleId }: Props) {
type="primary"
icon={<Plus />}
onClick={() => {
// Todo @xiaoyijun Assign permissions to org role
setIsAssignScopesModalOpen(true);
}}
/>
</div>
@ -199,6 +202,13 @@ function Permissions({ organizationRoleId }: Props) {
}}
/>
)}
<OrganizationRolePermissionsAssignmentModal
organizationRoleId={organizationRoleId}
isOpen={isAssignScopesModalOpen}
onClose={() => {
setIsAssignScopesModalOpen(false);
}}
/>
</>
);
}

View file

@ -1,4 +1,5 @@
import { type OrganizationRole } from '@logto/schemas';
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
@ -15,18 +16,29 @@ import { trySubmitSafe } from '@/utils/form';
type FormData = Pick<OrganizationRole, 'name' | 'description'>;
type Props = {
isOpen: boolean;
onClose: (createdOrganizationRole?: OrganizationRole) => void;
};
function CreateOrganizationRoleModal({ onClose }: Props) {
function CreateOrganizationRoleModal({ isOpen, onClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
} = useForm<FormData>();
const onCloseHandler = useCallback(
(createdData?: OrganizationRole) => {
// Reset form when modal is closed
reset();
onClose(createdData);
},
[onClose, reset]
);
const api = useApi();
const submit = handleSubmit(
@ -37,17 +49,17 @@ function CreateOrganizationRoleModal({ onClose }: Props) {
toast.success(
t('organization_template.roles.create_modal.created', { name: createdData.name })
);
onClose(createdData);
onCloseHandler(createdData);
})
);
return (
<ReactModal
isOpen
isOpen={isOpen}
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
onClose();
onCloseHandler();
}}
>
<ModalLayout
@ -60,7 +72,7 @@ function CreateOrganizationRoleModal({ onClose }: Props) {
onClick={submit}
/>
}
onClose={onClose}
onClose={onCloseHandler}
>
<FormField isRequired title="organization_template.roles.create_modal.name_field">
<TextInput

View file

@ -11,6 +11,7 @@ import RolesEmpty from '@/assets/images/roles-empty.svg';
import Breakable from '@/components/Breakable';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import ItemPreview from '@/components/ItemPreview';
import OrganizationRolePermissionsAssignmentModal from '@/components/OrganizationRolePermissionsAssignmentModal';
import ThemedIcon from '@/components/ThemedIcon';
import { defaultPageSize, organizationRoleLink } from '@/consts';
import Button from '@/ds-components/Button';
@ -51,6 +52,7 @@ function OrganizationRoles() {
const [orgRoles, totalCount] = data ?? [];
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [createdRole, setCreatedRole] = useState<OrganizationRoleWithScopes>();
return (
<>
@ -152,14 +154,24 @@ function OrganizationRoles() {
errorMessage={error?.body?.message ?? error?.message}
onRetry={async () => mutate(undefined, true)}
/>
{isCreateModalOpen && (
<CreateOrganizationRoleModal
onClose={(createdRole) => {
setIsCreateModalOpen(false);
if (createdRole) {
void mutate();
navigate(createdRole.id);
}
<CreateOrganizationRoleModal
isOpen={isCreateModalOpen}
onClose={(createdRole) => {
setIsCreateModalOpen(false);
if (createdRole) {
void mutate();
setCreatedRole({ ...createdRole, scopes: [], resourceScopes: [] });
}
}}
/>
{createdRole && (
<OrganizationRolePermissionsAssignmentModal
isOpen
organizationRoleId={createdRole.id}
onClose={() => {
setCreatedRole(undefined);
navigate(createdRole.id);
void mutate();
}}
/>
)}

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'Wenn diese Berechtigung entfernt wird, verliert der Benutzer mit dieser Organisationsrolle den Zugriff, der durch diese Berechtigung gewährt wurde.',
removed: 'Die Berechtigung {{name}} wurde erfolgreich aus dieser Organisationsrolle entfernt',
assign_description:
'Weisen Sie Berechtigungen den Rollen innerhalb dieser Organisation zu. Diese können sowohl Organisationsberechtigungen als auch API-Berechtigungen umfassen.',
organization_permissions: 'Organisationsberechtigungen',
api_permissions: 'API-Berechtigungen',
assign_organization_permissions: 'Organisationsberechtigungen zuweisen',
assign_api_permissions: 'API-Berechtigungen zuweisen',
},
general: {
tab: 'Allgemein',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'If this permission is removed, the user with this organization role will lose the access granted by this permission.',
removed: 'The permission {{name}} was successfully removed from this organization role',
assign_description:
'Assign permissions to the roles within this organization. These can include both organization permissions and API permissions.',
organization_permissions: 'Organization permissions',
api_permissions: 'API permissions',
assign_organization_permissions: 'Assign organization permissions',
assign_api_permissions: 'Assign API permissions',
},
general: {
tab: 'General',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'Si este permiso se elimina, el usuario con este rol de organización perderá el acceso otorgado por este permiso.',
removed: 'El permiso {{name}} se eliminó correctamente de este rol de organización',
assign_description:
'Asigne permisos a los roles dentro de esta organización. Estos pueden incluir tanto permisos de organización como permisos de API.',
organization_permissions: 'Permisos de organización',
api_permissions: 'Permisos de API',
assign_organization_permissions: 'Asignar permisos de organización',
assign_api_permissions: 'Asignar permisos de API',
},
general: {
tab: 'General',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
"Si cette permission est supprimée, l'utilisateur avec ce rôle d'organisation perdra l'accès accordé par cette permission.",
removed: "La permission {{name}} a été supprimée avec succès de ce rôle d'organisation",
assign_description:
"Attribuez des autorisations aux rôles au sein de cette organisation. Celles-ci peuvent inclure à la fois des autorisations d'organisation et des autorisations d'API.",
organization_permissions: "Autorisations de l'organisation",
api_permissions: "Autorisations de l'API",
assign_organization_permissions: 'Attribuer des permissions dorganisation',
assign_api_permissions: 'Attribuer des permissions dAPI',
},
general: {
tab: 'Général',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
"Se questo permesso viene rimosso, l'utente con questo ruolo organizzativo perderà l'accesso concessogli da questo permesso.",
removed: 'Il permesso {{name}} è stato rimosso con successo da questo ruolo organizzativo',
assign_description:
"Assegna le autorizzazioni ai ruoli all'interno di questa organizzazione. Queste possono includere sia autorizzazioni dell'organizzazione che autorizzazioni API.",
organization_permissions: "Autorizzazioni dell'organizzazione",
api_permissions: 'Autorizzazioni API',
assign_organization_permissions: 'Assegna permessi di organizzazione',
assign_api_permissions: 'Assegna permessi API',
},
general: {
tab: 'Generale',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'この権限を削除すると、この組織の役割を持つユーザーはこの権限によって付与されたアクセスを失います。',
removed: 'この組織の役割から権限 {{name}} が正常に削除されました',
assign_description:
'この組織内のロールに権限を割り当てます。これには組織の権限とAPIの権限の両方が含まれる場合があります。',
organization_permissions: '組織の権限',
api_permissions: 'APIの権限',
assign_organization_permissions: '組織の権限を割り当てる',
assign_api_permissions: 'APIの権限を割り当てる',
},
general: {
tab: '一般',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'이 권한을 제거하면이 조직 역할을하는 사용자는이 권한으로 부여된 액세스를 잃게됩니다.',
removed: '권한 {{name}}이(가)이 조직 역할에서 성공적으로 제거되었습니다',
assign_description:
'이 조직 내의 역할에 권한을 할당합니다. 이는 조직 권한과 API 권한을 모두 포함할 수 있습니다.',
organization_permissions: '조직 권한',
api_permissions: 'API 권한',
assign_organization_permissions: '조직 권한 할당',
assign_api_permissions: 'API 권한 할당',
},
general: {
tab: '일반',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'Jeśli to uprawnienie zostanie usunięte, użytkownik z tą rolą organizacyjną utraci dostęp udzielony przez to uprawnienie.',
removed: 'Uprawnienie {{name}} zostało pomyślnie usunięte z tej roli organizacyjnej',
assign_description:
'Przypisz uprawnienia do ról w tej organizacji. Mogą one obejmować zarówno uprawnienia organizacyjne, jak i uprawnienia interfejsu API.',
organization_permissions: 'Uprawnienia organizacyjne',
api_permissions: 'Uprawnienia interfejsu API',
assign_organization_permissions: 'Przydziel uprawnienia organizacyjne',
assign_api_permissions: 'Przydziel uprawnienia API',
},
general: {
tab: 'Ogólne',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'Se esta permissão for removida, o usuário com essa função organizacional perderá o acesso concedido por esta permissão.',
removed: 'A permissão {{name}} foi removida com sucesso desta função organizacional',
assign_description:
'Atribua permissões aos papéis dentro desta organização. Estas podem incluir tanto permissões de organização quanto permissões de API.',
organization_permissions: 'Permissões de organização',
api_permissions: 'Permissões de API',
assign_organization_permissions: 'Atribuir permissões de organização',
assign_api_permissions: 'Atribuir permissões de API',
},
general: {
tab: 'Geral',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'Se esta permissão for removida, o utilizador com esta função organizacional perderá o acesso concedido por esta permissão.',
removed: 'A permissão {{name}} foi removida com sucesso desta função organizacional',
assign_description:
'Atribuir permissões aos cargos dentro desta organização. Estas podem incluir permissões de organização e permissões de API.',
organization_permissions: 'Permissões de organização',
api_permissions: 'Permissões de API',
assign_organization_permissions: 'Atribuir permissões de organização',
assign_api_permissions: 'Atribuir permissões de API',
},
general: {
tab: 'Geral',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'Если это разрешение будет удалено, пользователь с этой организационной ролью потеряет доступ, предоставленный этим разрешением.',
removed: 'Разрешение {{name}} успешно удалено из этой организационной роли',
assign_description:
'Назначьте разрешения ролям в этой организации. Они могут включать как организационные разрешения, так и разрешения API.',
organization_permissions: 'Организационные разрешения',
api_permissions: 'Разрешения API',
assign_organization_permissions: 'Назначить разрешения организации',
assign_api_permissions: 'Назначить разрешения API',
},
general: {
tab: 'Общее',

View file

@ -19,6 +19,12 @@ const organization_role_details = {
remove_confirmation:
'Bu izin kaldırılırsa, bu organizasyon rolüne sahip kullanıcı bu izin tarafından verilen erişimi kaybeder.',
removed: '{{name}} izni bu organizasyon rolünden başarıyla kaldırıldı',
assign_description:
'Bu organizasyon içindeki roller için izinleri atayın. Bunlar hem organizasyon izinlerini hem de API izinlerini içerebilir.',
organization_permissions: 'Organizasyon izinleri',
api_permissions: 'API izinleri',
assign_organization_permissions: 'Kuruluş izinleri ata',
assign_api_permissions: 'API izinleri ata',
},
general: {
tab: 'Genel',

View file

@ -18,6 +18,11 @@ const organization_role_details = {
remove_permission: '移除权限',
remove_confirmation: '如果移除此权限,拥有此组织角色的用户将失去此权限授予的访问权限。',
removed: '权限 {{name}} 已成功从此组织角色中移除',
assign_description: '为此组织中的角色分配权限。这些权限可以包括组织权限和 API 权限。',
organization_permissions: '组织权限',
api_permissions: 'API 权限',
assign_organization_permissions: '分配组织权限',
assign_api_permissions: '分配API权限',
},
general: {
tab: '常规',

View file

@ -18,6 +18,11 @@ const organization_role_details = {
remove_permission: '移除權限',
remove_confirmation: '如果移除此權限,擁有此組織角色的使用者將失去此權限所授予的存取權。',
removed: '權限 {{name}} 已成功從此組織角色中移除',
assign_description: '為此組織中的角色分配權限。這些可以包括組織權限和 API 權限。',
organization_permissions: '組織權限',
api_permissions: 'API 權限',
assign_organization_permissions: '分配組織權限',
assign_api_permissions: '分配API權限',
},
general: {
tab: '一般',

View file

@ -18,6 +18,11 @@ const organization_role_details = {
remove_permission: '移除權限',
remove_confirmation: '如果移除此權限,擁有此組織角色的使用者將失去此權限所授予的存取權。',
removed: '權限 {{name}} 已成功從此組織角色中移除',
assign_description: '為此組織中的角色分配權限。這些可以包括組織權限和 API 權限。',
organization_permissions: '組織權限',
api_permissions: 'API 權限',
assign_organization_permissions: '分配組織權限',
assign_api_permissions: '分配API許可權',
},
general: {
tab: '一般',