0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

feat(console): support assign organization resource scopes for 3rd-party app (#5812)

This commit is contained in:
Xiao Yijun 2024-05-09 18:52:38 +08:00 committed by GitHub
parent ae5284b1d3
commit 3227f61fad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 830 additions and 351 deletions

View file

@ -8,7 +8,7 @@ import type { Props as TextLinkProps } from '@/ds-components/TextLink';
import FormCardLayout from './FormCardLayout';
import * as styles from './index.module.scss';
type Props = {
export type Props = {
readonly title: AdminConsoleKey;
readonly tag?: ReactNode;
readonly description?: AdminConsoleKey;

View file

@ -1,27 +0,0 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { ApplicationUserConsentScopeType } from '@logto/schemas';
export const permissionTabs = Object.freeze({
[ApplicationUserConsentScopeType.UserScopes]: {
title: 'application_details.permissions.user_profile',
key: ApplicationUserConsentScopeType.UserScopes,
},
[ApplicationUserConsentScopeType.ResourceScopes]: {
title: 'application_details.permissions.api_resource',
key: ApplicationUserConsentScopeType.ResourceScopes,
},
[ApplicationUserConsentScopeType.OrganizationResourceScopes]: {
// TODO @xiaoyijun: update the title
title: 'application_details.permissions.api_resource',
key: ApplicationUserConsentScopeType.OrganizationResourceScopes,
},
[ApplicationUserConsentScopeType.OrganizationScopes]: {
title: 'application_details.permissions.organization',
key: ApplicationUserConsentScopeType.OrganizationScopes,
},
}) satisfies {
[key in ApplicationUserConsentScopeType]: {
title: AdminConsoleKey;
key: key;
};
};

View file

@ -1,114 +0,0 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { ApplicationUserConsentScopeType } from '@logto/schemas';
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 { permissionTabs } from './constants';
import useApplicationScopesAssignment from './use-application-scopes-assignment';
const modalText = Object.freeze({
title: 'application_details.permissions.table_name',
subtitle: 'application_details.permissions.permissions_assignment_description',
saveBtn: 'general.save',
}) satisfies Record<string, AdminConsoleKey>;
type Props = {
readonly isOpen: boolean;
readonly onClose: () => void;
readonly applicationId: string;
};
function ApplicationScopesAssignmentModal({ isOpen, onClose, applicationId }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { activeTab, setActiveTab, scopesAssignment, clearSelectedData, onSubmit, isLoading } =
useApplicationScopesAssignment(applicationId);
const onCloseHandler = useCallback(() => {
onClose();
clearSelectedData();
setActiveTab(ApplicationUserConsentScopeType.UserScopes);
}, [clearSelectedData, onClose, setActiveTab]);
const onSubmitHandler = useCallback(async () => {
await onSubmit();
onCloseHandler();
}, [onCloseHandler, onSubmit]);
// If any of the tabs has selected scopes, the modal is dirty
const isDirty = Object.values(scopesAssignment).some(
({ selectedData }) => selectedData.length > 0
);
const tabs = useMemo(
() =>
Object.values(permissionTabs)
/**
* Hide the organization resource scopes tab since the feature is not ready.
* We don't need the `isDevFeaturesEnabled` flag since the feature will change the UI.
* Todo @xiaoyijun Implement the new organization resource scopes feature. LOG-8823
*/
.filter(({ key }) => key !== ApplicationUserConsentScopeType.OrganizationResourceScopes)
.map(({ title, key }) => {
const selectedDataCount = scopesAssignment[key].selectedData.length;
return (
<TabNavItem
key={key}
isActive={key === activeTab}
onClick={() => {
setActiveTab(key);
}}
>
{`${t(title)}${selectedDataCount ? ` (${selectedDataCount})` : ''}`}
</TabNavItem>
);
}),
[activeTab, scopesAssignment, setActiveTab, t]
);
return (
<ConfirmModal
isOpen={isOpen}
isLoading={isLoading}
title={modalText.title}
subtitle={modalText.subtitle}
isConfirmButtonDisabled={!isDirty}
confirmButtonType="primary"
confirmButtonText={modalText.saveBtn}
isCancelButtonVisible={false}
size="large"
onCancel={onCloseHandler}
onConfirm={onSubmitHandler}
>
<TabNav>{tabs}</TabNav>
<TabWrapper
key={ApplicationUserConsentScopeType.UserScopes}
isActive={ApplicationUserConsentScopeType.UserScopes === activeTab}
>
<DataTransferBox {...scopesAssignment[ApplicationUserConsentScopeType.UserScopes]} />
</TabWrapper>
<TabWrapper
key={ApplicationUserConsentScopeType.ResourceScopes}
isActive={ApplicationUserConsentScopeType.ResourceScopes === activeTab}
>
<DataTransferBox {...scopesAssignment[ApplicationUserConsentScopeType.ResourceScopes]} />
</TabWrapper>
<TabWrapper
key={ApplicationUserConsentScopeType.OrganizationScopes}
isActive={ApplicationUserConsentScopeType.OrganizationScopes === activeTab}
>
<DataTransferBox
{...scopesAssignment[ApplicationUserConsentScopeType.OrganizationScopes]}
/>
</TabWrapper>
</ConfirmModal>
);
}
export default ApplicationScopesAssignmentModal;

View file

@ -0,0 +1,40 @@
import { ApplicationUserConsentScopeType } from '@logto/schemas';
import { type PermissionTabType } from './type';
export const allLevelPermissionTabs: PermissionTabType = Object.freeze({
[ApplicationUserConsentScopeType.UserScopes]: {
title: 'application_details.permissions.user_profile',
key: ApplicationUserConsentScopeType.UserScopes,
},
[ApplicationUserConsentScopeType.ResourceScopes]: {
title: 'application_details.permissions.api_permissions',
key: ApplicationUserConsentScopeType.ResourceScopes,
},
[ApplicationUserConsentScopeType.OrganizationScopes]: {
title: 'application_details.permissions.organization',
key: ApplicationUserConsentScopeType.OrganizationScopes,
},
});
export const userLevelPermissionsTabs: PermissionTabType = Object.freeze({
[ApplicationUserConsentScopeType.UserScopes]: {
title: 'application_details.permissions.user_profile',
key: ApplicationUserConsentScopeType.UserScopes,
},
[ApplicationUserConsentScopeType.ResourceScopes]: {
title: 'application_details.permissions.api_permissions',
key: ApplicationUserConsentScopeType.ResourceScopes,
},
});
export const organizationLevelPermissionsTab: PermissionTabType = Object.freeze({
[ApplicationUserConsentScopeType.OrganizationScopes]: {
title: 'application_details.permissions.organization',
key: ApplicationUserConsentScopeType.OrganizationScopes,
},
[ApplicationUserConsentScopeType.OrganizationResourceScopes]: {
title: 'application_details.permissions.api_permissions',
key: ApplicationUserConsentScopeType.OrganizationResourceScopes,
},
});

View file

@ -0,0 +1,160 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { ApplicationUserConsentScopeType } from '@logto/schemas';
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 {
allLevelPermissionTabs,
organizationLevelPermissionsTab,
userLevelPermissionsTabs,
} from './constants';
import { ScopeLevel } from './type';
import useApplicationScopesAssignment from './use-application-scopes-assignment';
type Props = {
readonly isOpen: boolean;
readonly onClose: () => void;
readonly applicationId: string;
readonly scopeLevel: ScopeLevel;
};
function ApplicationScopesAssignmentModal({ isOpen, onClose, applicationId, scopeLevel }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { activeTab, setActiveTab, scopesAssignment, clearSelectedData, onSubmit, isLoading } =
useApplicationScopesAssignment(applicationId, scopeLevel);
const onCloseHandler = useCallback(() => {
onClose();
clearSelectedData();
setActiveTab(
scopeLevel === ScopeLevel.Organization
? ApplicationUserConsentScopeType.OrganizationResourceScopes
: ApplicationUserConsentScopeType.UserScopes
);
}, [clearSelectedData, onClose, scopeLevel, setActiveTab]);
const onSubmitHandler = useCallback(async () => {
await onSubmit();
onCloseHandler();
}, [onCloseHandler, onSubmit]);
// If any of the tabs has selected scopes, the modal is dirty
const isDirty = useMemo(
() => Object.values(scopesAssignment).some(({ selectedData }) => selectedData.length > 0),
[scopesAssignment]
);
const tabs = useMemo(() => {
const getPermissionTabs = () => {
if (scopeLevel === ScopeLevel.All) {
return allLevelPermissionTabs;
}
return scopeLevel === ScopeLevel.User
? userLevelPermissionsTabs
: organizationLevelPermissionsTab;
};
return Object.values(getPermissionTabs()).map(({ title, key }) => {
const selectedDataCount = scopesAssignment[key].selectedData.length;
return (
<TabNavItem
key={key}
isActive={key === activeTab}
onClick={() => {
setActiveTab(key);
}}
>
{`${String(t(title))}${selectedDataCount ? ` (${selectedDataCount})` : ''}`}
</TabNavItem>
);
});
}, [activeTab, scopeLevel, scopesAssignment, setActiveTab, t]);
const modalText = useMemo<{
title: AdminConsoleKey;
subtitle: AdminConsoleKey;
saveButton: AdminConsoleKey;
}>(() => {
if (scopeLevel === ScopeLevel.All) {
return {
title: 'application_details.permissions.table_name',
subtitle: 'application_details.permissions.permissions_assignment_description',
saveButton: 'general.save',
};
}
const scopeLevelPhrase = scopeLevel === ScopeLevel.User ? 'user' : 'organization';
return {
title: `application_details.permissions.grant_${scopeLevelPhrase}_level_permissions`,
subtitle: `application_details.permissions.${scopeLevelPhrase}_description`,
saveButton: 'general.save',
};
}, [scopeLevel]);
return (
<ConfirmModal
isOpen={isOpen}
isLoading={isLoading}
title={modalText.title}
subtitle={modalText.subtitle}
isConfirmButtonDisabled={!isDirty}
confirmButtonType="primary"
confirmButtonText={modalText.saveButton}
isCancelButtonVisible={false}
size="large"
onCancel={onCloseHandler}
onConfirm={onSubmitHandler}
>
<TabNav>{tabs}</TabNav>
{(scopeLevel === ScopeLevel.All || scopeLevel === ScopeLevel.User) && (
<>
<TabWrapper
key={ApplicationUserConsentScopeType.UserScopes}
isActive={ApplicationUserConsentScopeType.UserScopes === activeTab}
>
<DataTransferBox {...scopesAssignment[ApplicationUserConsentScopeType.UserScopes]} />
</TabWrapper>
<TabWrapper
key={ApplicationUserConsentScopeType.ResourceScopes}
isActive={ApplicationUserConsentScopeType.ResourceScopes === activeTab}
>
<DataTransferBox
{...scopesAssignment[ApplicationUserConsentScopeType.ResourceScopes]}
/>
</TabWrapper>
</>
)}
{(scopeLevel === ScopeLevel.All || scopeLevel === ScopeLevel.Organization) && (
<TabWrapper
key={ApplicationUserConsentScopeType.OrganizationScopes}
isActive={ApplicationUserConsentScopeType.OrganizationScopes === activeTab}
>
<DataTransferBox
{...scopesAssignment[ApplicationUserConsentScopeType.OrganizationScopes]}
/>
</TabWrapper>
)}
{scopeLevel === ScopeLevel.Organization && (
<TabWrapper
key={ApplicationUserConsentScopeType.OrganizationResourceScopes}
isActive={ApplicationUserConsentScopeType.OrganizationResourceScopes === activeTab}
>
<DataTransferBox
{...scopesAssignment[ApplicationUserConsentScopeType.OrganizationResourceScopes]}
/>
</TabWrapper>
)}
</ConfirmModal>
);
}
export default ApplicationScopesAssignmentModal;

View file

@ -1,4 +1,6 @@
import { type AdminConsoleKey } from '@logto/phrases';
import {
type JsonObject,
type ApplicationUserConsentScopesResponse,
type ApplicationUserConsentScopeType,
} from '@logto/schemas';
@ -24,6 +26,25 @@ type CamelCase<T> = T extends `${infer A}-${infer B}` ? `${A}${Capitalize<CamelC
export type ScopeAssignmentHook<
T extends ApplicationUserConsentScopeType,
V extends DataEntry = DataEntry,
U extends JsonObject = JsonObject,
> = (
assignedScopes?: ApplicationUserConsentScopesResponse[CamelCase<T>]
assignedScopes?: ApplicationUserConsentScopesResponse[CamelCase<T>],
options?: U
) => ScopeAssignmentHookReturnType<T, V>;
export enum ScopeLevel {
User = 'user',
Organization = 'organization',
/**
* Only used when the new organization resource scope feature is not ready.
* Todo @xiaoyijun remove this when the new organization resource scope feature is ready.
*/
All = 'all',
}
export type PermissionTabType = Partial<{
[Key in ApplicationUserConsentScopeType]: {
title: AdminConsoleKey;
key: Key;
};
}>;

View file

@ -8,13 +8,16 @@ import useSWR from 'swr';
import useApi, { type RequestError } from '@/hooks/use-api';
import { ScopeLevel } from './type';
import useOrganizationScopesAssignment from './use-organization-scopes-assignment';
import useResourceScopesAssignment from './use-resource-scopes-assignment';
import useUserScopesAssignment from './use-user-scopes-assignment';
const useApplicationScopesAssignment = (applicationId: string) => {
const useApplicationScopesAssignment = (applicationId: string, scopeLevel: ScopeLevel) => {
const [activeTab, setActiveTab] = useState<ApplicationUserConsentScopeType>(
ApplicationUserConsentScopeType.UserScopes
scopeLevel === ScopeLevel.Organization
? ApplicationUserConsentScopeType.OrganizationScopes
: ApplicationUserConsentScopeType.UserScopes
);
const [isLoading, setIsLoading] = useState(false);
@ -27,18 +30,31 @@ const useApplicationScopesAssignment = (applicationId: string) => {
const userScopesAssignment = useUserScopesAssignment(data?.userScopes);
const organizationScopesAssignment = useOrganizationScopesAssignment(data?.organizationScopes);
const resourceScopesAssignment = useResourceScopesAssignment(data?.resourceScopes);
const organizationResourceScopesAssignment = useResourceScopesAssignment(
data?.organizationResourceScopes,
{ isForOrganization: true }
);
const clearSelectedData = useCallback(() => {
userScopesAssignment.setSelectedData([]);
organizationScopesAssignment.setSelectedData([]);
resourceScopesAssignment.setSelectedData([]);
}, [organizationScopesAssignment, resourceScopesAssignment, userScopesAssignment]);
organizationScopesAssignment.setSelectedData([]);
organizationResourceScopesAssignment.setSelectedData([]);
}, [
organizationResourceScopesAssignment,
organizationScopesAssignment,
resourceScopesAssignment,
userScopesAssignment,
]);
const onSubmit = useCallback(async () => {
setIsLoading(true);
const newUserScopes = userScopesAssignment.selectedData.map(({ id }) => id);
const newOrganizationScopes = organizationScopesAssignment.selectedData.map(({ id }) => id);
const newResourceScopes = resourceScopesAssignment.selectedData.map(({ id }) => id);
const newOrganizationResourceScopes = organizationResourceScopesAssignment.selectedData.map(
({ id }) => id
);
await api
.post(`api/applications/${applicationId}/user-consent-scopes`, {
@ -50,6 +66,11 @@ const useApplicationScopesAssignment = (applicationId: string) => {
}
),
...conditional(newResourceScopes.length > 0 && { resourceScopes: newResourceScopes }),
...conditional(
newOrganizationResourceScopes.length > 0 && {
organizationResourceScopes: newOrganizationResourceScopes,
}
),
},
})
.finally(() => {
@ -61,6 +82,7 @@ const useApplicationScopesAssignment = (applicationId: string) => {
api,
applicationId,
mutate,
organizationResourceScopesAssignment.selectedData,
organizationScopesAssignment.selectedData,
resourceScopesAssignment.selectedData,
userScopesAssignment.selectedData,
@ -70,12 +92,17 @@ const useApplicationScopesAssignment = (applicationId: string) => {
const scopesAssignment = useMemo(
() => ({
[ApplicationUserConsentScopeType.UserScopes]: userScopesAssignment,
[ApplicationUserConsentScopeType.OrganizationScopes]: organizationScopesAssignment,
[ApplicationUserConsentScopeType.ResourceScopes]: resourceScopesAssignment,
// TODO @xiaoyijun: Replace with correct scopes
[ApplicationUserConsentScopeType.OrganizationResourceScopes]: resourceScopesAssignment,
[ApplicationUserConsentScopeType.OrganizationScopes]: organizationScopesAssignment,
[ApplicationUserConsentScopeType.OrganizationResourceScopes]:
organizationResourceScopesAssignment,
}),
[organizationScopesAssignment, resourceScopesAssignment, userScopesAssignment]
[
userScopesAssignment,
resourceScopesAssignment,
organizationScopesAssignment,
organizationResourceScopesAssignment,
]
);
return {

View file

@ -8,21 +8,30 @@ import { useState, useMemo } from 'react';
import useSWR from 'swr';
import { type RequestError } from '@/hooks/use-api';
import { buildUrl } from '@/utils/url';
import { type ScopeAssignmentHook } from './type';
type HookType = ScopeAssignmentHook<
ApplicationUserConsentScopeType.ResourceScopes,
ApplicationUserConsentScopesResponse['resourceScopes'][number]['scopes'][number]
| ApplicationUserConsentScopeType.ResourceScopes
| ApplicationUserConsentScopeType.OrganizationResourceScopes,
ApplicationUserConsentScopesResponse['resourceScopes'][number]['scopes'][number],
{
/** Whether the assignment is for an organization */
isForOrganization?: boolean;
}
>;
type SelectedDataType = ReturnType<HookType>['selectedData'][number];
const useResourceScopesAssignment: HookType = (assignedResourceScopes) => {
const useResourceScopesAssignment: HookType = (assignedResourceScopes, options) => {
const [selectedData, setSelectedData] = useState<SelectedDataType[]>([]);
const { isForOrganization } = options ?? {};
const { data: allResources } = useSWR<ResourceResponse[], RequestError>(
'api/resources?includeScopes=true'
buildUrl('api/resources', {
includeScopes: String(true),
})
);
const availableDataGroups = useMemo(() => {
@ -58,11 +67,17 @@ const useResourceScopesAssignment: HookType = (assignedResourceScopes) => {
}, [allResources, assignedResourceScopes]);
return {
scopeType: ApplicationUserConsentScopeType.ResourceScopes,
scopeType: isForOrganization
? ApplicationUserConsentScopeType.OrganizationResourceScopes
: ApplicationUserConsentScopeType.ResourceScopes,
selectedData,
setSelectedData,
availableDataGroups,
title: 'application_details.permissions.api_resource_permissions_assignment_form_title',
title: `application_details.permissions.${
isForOrganization
? 'add_permissions_for_organization'
: 'api_resource_permissions_assignment_form_title'
}`,
};
};

View file

@ -0,0 +1,20 @@
@use '@/scss/underscore' as _;
// Override the Table component styles
.permissionsModal [class*='tableContainer'] [class*='bodyTable'] tr.sectionTitleRow {
height: _.unit(9);
td {
font: var(--font-label-2);
color: var(--color-text-secondary);
background-color: var(--color-layer-light);
padding-top: _.unit(2);
padding-bottom: _.unit(2);
}
}
.label {
display: flex;
gap: _.unit(1);
align-items: center;
}

View file

@ -0,0 +1,179 @@
import { type AdminConsoleKey } from '@logto/phrases';
import {
ApplicationUserConsentScopeType,
type ApplicationUserConsentScopesResponse,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import ActionsButton from '@/components/ActionsButton';
import Breakable from '@/components/Breakable';
import FormCard, { type Props as FormCardProps } from '@/components/FormCard';
import TemplateTable from '@/components/TemplateTable';
import { logtoThirdPartyAppPermissionsLink } from '@/consts';
import Tag from '@/ds-components/Tag';
import { type RequestError } from '@/hooks/use-api';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import ApplicationScopesAssignmentModal from './ApplicationScopesAssignmentModal';
import { ScopeLevel } from './ApplicationScopesAssignmentModal/type';
import ApplicationScopesManagementModal, {
type EditableScopeData,
} from './ApplicationScopesManagementModal';
import * as styles from './index.module.scss';
import useScopesTable from './use-scopes-table';
type Props = {
readonly applicationId: string;
readonly scopeLevel: ScopeLevel;
};
function PermissionsCard({ applicationId, scopeLevel }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();
const { data, error, mutate, isLoading } = useSWR<
ApplicationUserConsentScopesResponse,
RequestError
>(`api/applications/${applicationId}/user-consent-scopes`);
const { parseRowGroup, deleteScope, editScope } = useScopesTable();
const [editScopeModalData, setEditScopeModalData] = useState<EditableScopeData>();
const [isAssignScopesModalOpen, setIsAssignScopesModalOpen] = useState(false);
const rowGroups = useMemo(() => {
const { userLevelRowGroups, organizationLevelGroups } = parseRowGroup(data);
if (scopeLevel === ScopeLevel.All) {
return [...userLevelRowGroups, ...organizationLevelGroups];
}
return scopeLevel === ScopeLevel.User ? userLevelRowGroups : organizationLevelGroups;
}, [data, parseRowGroup, scopeLevel]);
const displayTextProps = useMemo<{
formCard: Omit<FormCardProps, 'children'>;
tableName: AdminConsoleKey;
}>(() => {
if (scopeLevel === ScopeLevel.All) {
return {
formCard: {
title: 'application_details.permissions.name',
description: 'application_details.permissions.description',
learnMoreLink: {
href: getDocumentationUrl(logtoThirdPartyAppPermissionsLink),
targetBlank: 'noopener',
},
},
tableName: 'application_details.permissions.table_name',
};
}
const scopeLevelPhrase = scopeLevel === ScopeLevel.User ? 'user' : 'organization';
return {
formCard: {
title: `application_details.permissions.${scopeLevelPhrase}_title`,
description: `application_details.permissions.${scopeLevelPhrase}_description`,
learnMoreLink: conditional(
scopeLevel === ScopeLevel.User && {
href: getDocumentationUrl(logtoThirdPartyAppPermissionsLink),
targetBlank: 'noopener',
}
),
},
tableName: `application_details.permissions.grant_${scopeLevelPhrase}_level_permissions`,
};
}, [getDocumentationUrl, scopeLevel]);
return (
<FormCard {...displayTextProps.formCard}>
<TemplateTable
className={styles.permissionsModal}
name={displayTextProps.tableName}
rowIndexKey="id"
errorMessage={error?.body?.message ?? error?.message}
isLoading={isLoading}
rowGroups={rowGroups}
columns={[
{
title: t('application_details.permissions.field_name'),
dataIndex: 'name',
colSpan: 5,
render: ({ name }) => (
<Tag variant="cell">
<Breakable>{name}</Breakable>
</Tag>
),
},
{
title: `${t('general.description')} (${t(
'application_details.permissions.field_description'
)})`,
dataIndex: 'description',
colSpan: 5,
render: ({ description }) => <Breakable>{description ?? '-'}</Breakable>,
},
{
title: null,
dataIndex: 'delete',
render: (data) => (
<ActionsButton
fieldName="application_details.permissions.name"
deleteConfirmation="application_details.permissions.permission_delete_confirm"
textOverrides={{
delete: 'application_details.permissions.delete_text',
deleteConfirmation: 'general.remove',
}}
onEdit={
// UserScopes is not editable
data.type === ApplicationUserConsentScopeType.UserScopes
? undefined
: () => {
setEditScopeModalData(data);
}
}
onDelete={async () => {
await deleteScope(data, applicationId);
void mutate();
}}
/>
),
},
]}
onAdd={() => {
setIsAssignScopesModalOpen(true);
}}
/>
{/* Render the permissions assignment modal only if the data is fetched properly */}
{data && (
<ApplicationScopesAssignmentModal
isOpen={isAssignScopesModalOpen}
applicationId={applicationId}
scopeLevel={scopeLevel}
onClose={() => {
setIsAssignScopesModalOpen(false);
}}
/>
)}
{data && (
<ApplicationScopesManagementModal
scope={editScopeModalData}
onClose={() => {
setEditScopeModalData(undefined);
}}
onSubmit={async (scope) => {
await editScope(scope);
void mutate();
setEditScopeModalData(undefined);
}}
/>
)}
</FormCard>
);
}
export default PermissionsCard;

View file

@ -4,10 +4,12 @@ import {
type ApplicationUserConsentScopesResponse,
ApplicationUserConsentScopeType,
} from '@logto/schemas';
import { condArray } from '@silverhand/essentials';
import { useCallback, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import Tip from '@/assets/icons/tip.svg';
import { isDevFeaturesEnabled } from '@/consts/env';
import IconButton from '@/ds-components/IconButton';
import { ToggleTip } from '@/ds-components/Tip';
import useApi from '@/hooks/use-api';
@ -26,7 +28,9 @@ type OrganizationScopeTableRowDataType = {
} & ApplicationUserConsentScopesResponse['organizationScopes'][number];
type ResourceScopeTableRowDataType = {
type: ApplicationUserConsentScopeType.ResourceScopes;
type:
| ApplicationUserConsentScopeType.ResourceScopes
| ApplicationUserConsentScopeType.OrganizationResourceScopes;
// Resource ID is required for resource scope patch request
resourceId: string;
resourceName: string;
@ -54,12 +58,20 @@ const useScopesTable = () => {
const api = useApi();
const parseRowGroup = useCallback(
(data?: ApplicationUserConsentScopesResponse): ScopesTableRowGroupType[] => {
(
data?: ApplicationUserConsentScopesResponse
): {
userLevelRowGroups: ScopesTableRowGroupType[];
organizationLevelGroups: ScopesTableRowGroupType[];
} => {
if (!data) {
return [];
return {
userLevelRowGroups: [],
organizationLevelGroups: [],
};
}
const { organizationScopes, userScopes, resourceScopes } = data;
const { userScopes, resourceScopes, organizationScopes, organizationResourceScopes } = data;
const userScopesGroup: ScopesTableRowGroupType = {
key: ApplicationUserConsentScopeType.UserScopes,
@ -85,16 +97,6 @@ const useScopesTable = () => {
})),
};
const organizationScopesGroup: ScopesTableRowGroupType = {
key: ApplicationUserConsentScopeType.OrganizationScopes,
label: t('application_details.permissions.organization_permissions'),
labelRowClassName: styles.sectionTitleRow,
data: organizationScopes.map((scope) => ({
type: ApplicationUserConsentScopeType.OrganizationScopes,
...scope,
})),
};
const resourceScopesGroups = resourceScopes.map<ScopesTableRowGroupType>(
({ resource, scopes }) => ({
key: resource.indicator,
@ -109,13 +111,48 @@ const useScopesTable = () => {
})
);
return [
// Hide the user scopes group if there is no user scopes
...(userScopesGroup.data.length > 0 ? [userScopesGroup] : []),
...resourceScopesGroups,
// Hide the organization scopes group if there is no organization scopes
...(organizationScopesGroup.data.length > 0 ? [organizationScopesGroup] : []),
];
const organizationScopesGroup: ScopesTableRowGroupType = {
key: ApplicationUserConsentScopeType.OrganizationScopes,
label: t('application_details.permissions.organization_permissions'),
labelRowClassName: styles.sectionTitleRow,
data: organizationScopes.map((scope) => ({
type: ApplicationUserConsentScopeType.OrganizationScopes,
...scope,
})),
};
const organizationResourceScopesGroup =
organizationResourceScopes.map<ScopesTableRowGroupType>(({ resource, scopes }) => ({
key: resource.indicator,
label: resource.name,
labelRowClassName: styles.sectionTitleRow,
data: scopes.map((scope) => ({
type: ApplicationUserConsentScopeType.OrganizationResourceScopes,
...scope,
resourceId: resource.id,
resourceName: resource.name,
})),
}));
return {
userLevelRowGroups: [
// Hide the user scopes group if there is no user scopes
...(userScopesGroup.data.length > 0 ? [userScopesGroup] : []),
...resourceScopesGroups,
],
organizationLevelGroups: [
// Hide the organization scopes group if there is no organization scopes
...(organizationScopesGroup.data.length > 0 ? [organizationScopesGroup] : []),
...condArray(
/**
* Hide the organization resource scopes group if the organization resource scopes feature is not ready
*/
isDevFeaturesEnabled &&
organizationResourceScopesGroup.length > 0 &&
organizationResourceScopesGroup
),
],
};
},
[experienceT, t]
);

View file

@ -1,21 +1,8 @@
@use '@/scss/underscore' as _;
// Override the Table component styles
.permissionsModal [class*='tableContainer'] [class*='bodyTable'] tr.sectionTitleRow {
height: _.unit(9);
td {
font: var(--font-label-2);
color: var(--color-text-secondary);
background-color: var(--color-layer-light);
padding-top: _.unit(2);
padding-bottom: _.unit(2);
}
}
.label {
.container {
display: flex;
gap: _.unit(1);
align-items: center;
flex-direction: column;
gap: _.unit(4);
padding-bottom: _.unit(6);
}

View file

@ -1,140 +1,26 @@
import {
type Application,
type ApplicationUserConsentScopesResponse,
ApplicationUserConsentScopeType,
} from '@logto/schemas';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import { type Application } from '@logto/schemas';
import ActionsButton from '@/components/ActionsButton';
import Breakable from '@/components/Breakable';
import FormCard from '@/components/FormCard';
import TemplateTable from '@/components/TemplateTable';
import { logtoThirdPartyAppPermissionsLink } from '@/consts';
import Tag from '@/ds-components/Tag';
import { type RequestError } from '@/hooks/use-api';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import { isDevFeaturesEnabled } from '@/consts/env';
import ApplicationScopesAssignmentModal from './ApplicationScopesAssignmentModal';
import ApplicationScopesManagementModal, {
type EditableScopeData,
} from './ApplicationScopesManagementModal';
import PermissionsCard from './PermissionsCard';
import { ScopeLevel } from './PermissionsCard/ApplicationScopesAssignmentModal/type';
import * as styles from './index.module.scss';
import useScopesTable from './use-scopes-table';
type Props = {
readonly application: Application;
};
function Permissions({ application }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();
const [isAssignScopesModalOpen, setIsAssignScopesModalOpen] = useState(false);
const [editScopeModalData, setEditScopeModalData] = useState<EditableScopeData>();
const { parseRowGroup, deleteScope, editScope } = useScopesTable();
const { data, error, mutate, isLoading } = useSWR<
ApplicationUserConsentScopesResponse,
RequestError
>(`api/applications/${application.id}/user-consent-scopes`);
const rowGroups = useMemo(() => parseRowGroup(data), [data, parseRowGroup]);
const displayScopeLevels = isDevFeaturesEnabled
? [ScopeLevel.User, ScopeLevel.Organization]
: [ScopeLevel.All];
return (
<>
<FormCard
title="application_details.permissions.name"
description="application_details.permissions.description"
learnMoreLink={{
href: getDocumentationUrl(logtoThirdPartyAppPermissionsLink),
targetBlank: 'noopener',
}}
>
<TemplateTable
className={styles.permissionsModal}
name="application_details.permissions.table_name"
rowIndexKey="id"
errorMessage={error?.body?.message ?? error?.message}
isLoading={isLoading}
rowGroups={rowGroups}
columns={[
{
title: t('application_details.permissions.field_name'),
dataIndex: 'name',
colSpan: 5,
render: ({ name }) => (
<Tag variant="cell">
<Breakable>{name}</Breakable>
</Tag>
),
},
{
title: `${t('general.description')} (${t(
'application_details.permissions.field_description'
)})`,
dataIndex: 'description',
colSpan: 5,
render: ({ description }) => <Breakable>{description ?? '-'}</Breakable>,
},
{
title: null,
dataIndex: 'delete',
render: (data) => (
<ActionsButton
fieldName="application_details.permissions.name"
deleteConfirmation="application_details.permissions.permission_delete_confirm"
textOverrides={{
delete: 'application_details.permissions.delete_text',
deleteConfirmation: 'general.remove',
}}
onEdit={
// UserScopes is not editable
data.type === ApplicationUserConsentScopeType.UserScopes
? undefined
: () => {
setEditScopeModalData(data);
}
}
onDelete={async () => {
await deleteScope(data, application.id);
void mutate();
}}
/>
),
},
]}
onAdd={() => {
setIsAssignScopesModalOpen(true);
}}
/>
</FormCard>
{/* Render the permissions assignment modal only if the data is fetched properly */}
{data && (
<ApplicationScopesAssignmentModal
applicationId={application.id}
isOpen={isAssignScopesModalOpen}
onClose={() => {
setIsAssignScopesModalOpen(false);
}}
/>
)}
{data && (
<ApplicationScopesManagementModal
scope={editScopeModalData}
onClose={() => {
setEditScopeModalData(undefined);
}}
onSubmit={async (scope) => {
await editScope(scope);
void mutate();
setEditScopeModalData(undefined);
}}
/>
)}
</>
<div className={styles.container}>
{displayScopeLevels.map((scopeLevel) => (
<PermissionsCard key={scopeLevel} applicationId={application.id} scopeLevel={scopeLevel} />
))}
</div>
);
}

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Rolle',

View file

@ -111,8 +111,8 @@ const application_details = {
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
user_profile: 'User data',
api_resource: 'API resource',
organization: 'Organization',
api_permissions: 'API permissions',
organization: 'Organization permissions',
user_permissions_assignment_form_title: 'Add the user profile permissions',
organization_permissions_assignment_form_title: 'Add the organization permissions',
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
@ -120,6 +120,16 @@ const application_details = {
'You can modify the description of the personal user data permissions via "Sign-in Experience > Content > Manage Language"',
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
user_title: 'User',
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
grant_user_level_permissions: 'Grant permissions of user data',
organization_title: 'Organization',
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
grant_organization_level_permissions: 'Grant permissions of organization data',
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Role',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Rol',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Rôle',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Ruolo',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: '役割',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: '역할',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Role',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Função',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Nome da função',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Роль',

View file

@ -156,9 +156,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -171,6 +171,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: 'Rol',

View file

@ -154,9 +154,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -169,6 +169,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: '角色',

View file

@ -154,9 +154,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -169,6 +169,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: '角色',

View file

@ -155,9 +155,9 @@ const application_details = {
/** UNTRANSLATED */
user_profile: 'User data',
/** UNTRANSLATED */
api_resource: 'API resource',
api_permissions: 'API permissions',
/** UNTRANSLATED */
organization: 'Organization',
organization: 'Organization permissions',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
@ -170,6 +170,23 @@ const application_details = {
/** UNTRANSLATED */
permission_description_tips:
'When Logto is used as an Identity Provider (IdP) for authentication in third-party apps, and users are asked for authorization, this description appears on the consent screen.',
/** UNTRANSLATED */
user_title: 'User',
/** UNTRANSLATED */
user_description:
'Select the permissions requested by the third-party app for accessing specific user data.',
/** UNTRANSLATED */
grant_user_level_permissions: 'Grant permissions of user data',
/** UNTRANSLATED */
organization_title: 'Organization',
/** UNTRANSLATED */
organization_description:
'Select the permissions requested by the third-party app for accessing specific organization data.',
/** UNTRANSLATED */
grant_organization_level_permissions: 'Grant permissions of organization data',
/** UNTRANSLATED */
add_permissions_for_organization:
'Add the API resource permissions used in the "Organization template"',
},
roles: {
name_column: '角色',