0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

refactor(console): setup m2m roles after creating m2m app (#5924)

This commit is contained in:
Xiao Yijun 2024-05-28 13:07:34 +08:00 committed by GitHub
parent 558c1bccfb
commit 58fd32e456
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 360 additions and 137 deletions

View file

@ -15,12 +15,11 @@ import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import useCurrentUser from '@/hooks/use-current-user';
import TypeDescription from '@/pages/Applications/components/TypeDescription';
import * as modalStyles from '@/scss/modal.module.scss';
import { applicationTypeI18nKey } from '@/types/applications';
import { trySubmitSafe } from '@/utils/form';
import TypeDescription from '../TypeDescription';
import Footer from './Footer';
import * as styles from './index.module.scss';
@ -31,7 +30,7 @@ type FormData = {
isThirdParty?: boolean;
};
type Props = {
export type Props = {
readonly isDefaultCreateThirdParty?: boolean;
readonly defaultCreateType?: ApplicationType;
readonly defaultCreateFrameworkName?: string;

View file

@ -0,0 +1,57 @@
import { ApplicationType, RoleType, type Application } from '@logto/schemas';
import { useCallback, useState } from 'react';
import RoleAssignmentModal from '@/components/RoleAssignmentModal';
import { isDevFeaturesEnabled } from '@/consts/env';
import CreateForm, { type Props as CreateApplicationFormProps } from './CreateForm';
type Props = Omit<CreateApplicationFormProps, 'onClose'> & {
/**
* The callback function that will be called when the application creation process is completed or canceled.
*/
readonly onCompleted?: (createdApp?: Application) => void;
};
/**
* The component for handling application creation (including create an application and setup its permissions if needed).
*/
function ApplicationCreation({ onCompleted, ...reset }: Props) {
const [createdMachineToMachineApplication, setCreatedMachineToMachineApplication] =
useState<Application>();
const createFormModalCloseHandler = useCallback(
(createdApp?: Application) => {
// Todo @xiaoyijun remove dev feature flag
if (isDevFeaturesEnabled && createdApp?.type === ApplicationType.MachineToMachine) {
setCreatedMachineToMachineApplication(createdApp);
return;
}
onCompleted?.(createdApp);
},
[onCompleted]
);
if (createdMachineToMachineApplication) {
return (
<RoleAssignmentModal
isSkippable
isMachineToMachineRoleCreationHintVisible
entity={createdMachineToMachineApplication}
type={RoleType.MachineToMachine}
modalTextOverrides={{
title: 'applications.m2m_role_assignment.title',
subtitle: 'applications.m2m_role_assignment.subtitle',
}}
onClose={() => {
onCompleted?.(createdMachineToMachineApplication);
}}
/>
);
}
return <CreateForm {...reset} onClose={createFormModalCloseHandler} />;
}
export default ApplicationCreation;

View file

@ -0,0 +1,6 @@
@use '@/scss/underscore' as _;
.hint {
margin-top: _.unit(2);
font: var(--font-body-2);
}

View file

@ -0,0 +1,168 @@
import { type AdminConsoleKey } from '@logto/phrases';
import type { RoleResponse, UserProfileResponse, Application } from '@logto/schemas';
import { RoleType } from '@logto/schemas';
import { cond } from '@silverhand/essentials';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import RolesTransfer from '@/components/RolesTransfer';
import Button from '@/ds-components/Button';
import DangerousRaw from '@/ds-components/DangerousRaw';
import ModalLayout from '@/ds-components/ModalLayout';
import TextLink from '@/ds-components/TextLink';
import useApi from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { getUserTitle } from '@/utils/user';
import * as styles from './index.module.scss';
type Props = (
| {
entity: UserProfileResponse;
type: RoleType.User;
}
| {
entity: Application;
type: RoleType.MachineToMachine;
}
) & {
readonly onClose: (success?: boolean) => void;
/**
* The overrides for the modal text.
* If specified, the title will be overridden.
* If not specified, the default title will vary based on the type.
*/
readonly modalTextOverrides?: {
title?: AdminConsoleKey;
subtitle?: AdminConsoleKey;
};
readonly isSkippable?: boolean;
readonly isMachineToMachineRoleCreationHintVisible?: boolean;
};
function RoleAssignmentModal({
entity,
onClose,
type,
modalTextOverrides,
isSkippable,
isMachineToMachineRoleCreationHintVisible,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [roles, setRoles] = useState<RoleResponse[]>([]);
const isForUser = type === RoleType.User;
const api = useApi();
const handleAssign = async () => {
if (isSubmitting || roles.length === 0) {
return;
}
setIsSubmitting(true);
try {
await api.post(`api/${isForUser ? 'users' : 'applications'}/${entity.id}/roles`, {
json: { roleIds: roles.map(({ id }) => id) },
});
toast.success(t('user_details.roles.role_assigned'));
onClose(true);
} finally {
setIsSubmitting(false);
}
};
return (
<ReactModal
isOpen
shouldCloseOnEsc
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
onClose();
}}
>
<ModalLayout
title={
cond(modalTextOverrides?.title) ?? (
<DangerousRaw>
{t(
isForUser
? 'user_details.roles.assign_title'
: 'application_details.roles.assign_title',
{ name: isForUser ? getUserTitle(entity) : entity.name }
)}
</DangerousRaw>
)
}
subtitle={
cond(modalTextOverrides?.subtitle) ?? (
<DangerousRaw>
{t(
isForUser
? 'user_details.roles.assign_subtitle'
: 'application_details.roles.assign_subtitle',
{ name: isForUser ? getUserTitle(entity) : entity.name }
)}
</DangerousRaw>
)
}
size="large"
footer={
<>
{isSkippable && (
<Button
isLoading={isSubmitting}
title="general.skip"
size="large"
onClick={() => {
onClose();
}}
/>
)}
<Button
isLoading={isSubmitting}
disabled={roles.length === 0}
htmlType="submit"
title={
isForUser
? 'user_details.roles.confirm_assign'
: 'application_details.roles.confirm_assign'
}
size="large"
type="primary"
onClick={handleAssign}
/>
</>
}
onClose={onClose}
>
<RolesTransfer
entityId={entity.id}
type={type}
value={roles}
onChange={(value) => {
setRoles(value);
}}
/>
{!isForUser && isMachineToMachineRoleCreationHintVisible && (
<div className={styles.hint}>
<Trans
components={{
a: <TextLink to="/roles" />,
}}
>
{t('applications.m2m_role_assignment.role_creation_hint')}
</Trans>
</div>
)}
</ModalLayout>
</ReactModal>
);
}
export default RoleAssignmentModal;

View file

@ -12,6 +12,7 @@ import MachineToMachineRoleIcon from '@/assets/icons/m2m-role.svg';
import Plus from '@/assets/icons/plus.svg';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import ItemPreview from '@/components/ItemPreview';
import RoleAssignmentModal from '@/components/RoleAssignmentModal';
import { defaultPageSize } from '@/consts';
import Button from '@/ds-components/Button';
import ConfirmModal from '@/ds-components/ConfirmModal';
@ -23,7 +24,6 @@ import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import useTheme from '@/hooks/use-theme';
import AssignToRoleModal from '@/pages/Roles/components/AssignToRoleModal';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import * as styles from './index.module.scss';
@ -176,7 +176,7 @@ function MachineToMachineApplicationRoles({ application }: Props) {
</ConfirmModal>
)}
{isAssignRolesModalOpen && (
<AssignToRoleModal
<RoleAssignmentModal
entity={application}
type={RoleType.MachineToMachine}
onClose={(success) => {

View file

@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import SearchIcon from '@/assets/icons/search.svg';
import ApplicationCreation from '@/components/ApplicationCreation';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import FeatureTag from '@/components/FeatureTag';
import { type SelectedGuide } from '@/components/Guide/GuideCard';
@ -21,7 +22,6 @@ import useTenantPathname from '@/hooks/use-tenant-pathname';
import { allAppGuideCategories, type AppGuideCategory } from '@/types/applications';
import { thirdPartyAppCategory } from '@/types/applications';
import CreateForm from '../CreateForm';
import ProtectedAppCard from '../ProtectedAppCard';
import * as styles from './index.module.scss';
@ -70,7 +70,7 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton }: Props) {
setSelectedGuide(data);
}, []);
const onCloseCreateForm = useCallback(
const onAppCreationCompleted = useCallback(
(newApp?: Application) => {
if (newApp && selectedGuide) {
navigate(
@ -185,11 +185,11 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton }: Props) {
</div>
</div>
{selectedGuide?.target !== 'API' && showCreateForm && (
<CreateForm
<ApplicationCreation
defaultCreateType={selectedGuide?.target}
defaultCreateFrameworkName={selectedGuide?.name}
isDefaultCreateThirdParty={selectedGuide?.isThirdParty}
onClose={onCloseCreateForm}
onCompleted={onAppCreationCompleted}
/>
)}
</OverlayScrollbar>

View file

@ -1,12 +1,12 @@
import { useState } from 'react';
import Modal from 'react-modal';
import ApplicationCreation from '@/components/ApplicationCreation';
import ModalFooter from '@/components/Guide/ModalFooter';
import ModalHeader from '@/components/Guide/ModalHeader';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import * as modalStyles from '@/scss/modal.module.scss';
import CreateForm from '../CreateForm';
import GuideLibrary from '../GuideLibrary';
import * as styles from './index.module.scss';
@ -47,8 +47,8 @@ function GuideLibraryModal({ isOpen, onClose }: Props) {
/>
</div>
{showCreateForm && (
<CreateForm
onClose={(newApp) => {
<ApplicationCreation
onCompleted={(newApp) => {
if (newApp) {
navigate(`/applications/${newApp.id}`);
}

View file

@ -9,6 +9,7 @@ import CreateRoleDark from '@/assets/icons/create-role-dark.svg';
import CreateRole from '@/assets/icons/create-role.svg';
import SocialDark from '@/assets/icons/social-dark.svg';
import Social from '@/assets/icons/social.svg';
import ApplicationCreation from '@/components/ApplicationCreation';
import { type SelectedGuide } from '@/components/Guide/GuideCard';
import GuideCardGroup from '@/components/Guide/GuideCardGroup';
import { useApiGuideMetadata, useAppGuideMetadata } from '@/components/Guide/hooks';
@ -25,7 +26,6 @@ import useTheme from '@/hooks/use-theme';
import useWindowResize from '@/hooks/use-window-resize';
import CreateApiForm from '../ApiResources/components/CreateForm';
import CreateAppForm from '../Applications/components/CreateForm';
import ProtectedAppCreationForm from './ProtectedAppCreationForm';
import * as styles from './index.module.scss';
@ -71,7 +71,7 @@ function GetStarted() {
setSelectedGuide(data);
}, []);
const onCloseCreateAppForm = useCallback(
const onAppCreationCompleted = useCallback(
(newApp?: Application) => {
if (newApp && selectedGuide) {
navigate(`/applications/${newApp.id}/guide/${selectedGuide.id}`, { replace: true });
@ -125,10 +125,10 @@ function GetStarted() {
onClickGuide={onClickAppGuide}
/>
{selectedGuide?.target !== 'API' && showCreateAppForm && (
<CreateAppForm
<ApplicationCreation
defaultCreateType={selectedGuide?.target}
defaultCreateFrameworkName={selectedGuide?.name}
onClose={onCloseCreateAppForm}
onCompleted={onAppCreationCompleted}
/>
)}
<TextLink to="/applications/create">{t('get_started.view_all')}</TextLink>

View file

@ -1,119 +0,0 @@
import type { RoleResponse, UserProfileResponse, Application } from '@logto/schemas';
import { RoleType } from '@logto/schemas';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import RolesTransfer from '@/components/RolesTransfer';
import Button from '@/ds-components/Button';
import DangerousRaw from '@/ds-components/DangerousRaw';
import ModalLayout from '@/ds-components/ModalLayout';
import useApi from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { getUserTitle } from '@/utils/user';
type Props =
| {
entity: UserProfileResponse;
onClose: (success?: boolean) => void;
type: RoleType.User;
}
| {
entity: Application;
onClose: (success?: boolean) => void;
type: RoleType.MachineToMachine;
};
function AssignToRoleModal({ entity, onClose, type }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [roles, setRoles] = useState<RoleResponse[]>([]);
const api = useApi();
const handleAssign = async () => {
if (isSubmitting || roles.length === 0) {
return;
}
setIsSubmitting(true);
try {
await api.post(
`api/${type === RoleType.User ? 'users' : 'applications'}/${entity.id}/roles`,
{
json: { roleIds: roles.map(({ id }) => id) },
}
);
toast.success(t('user_details.roles.role_assigned'));
onClose(true);
} finally {
setIsSubmitting(false);
}
};
return (
<ReactModal
isOpen
shouldCloseOnEsc
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
onClose();
}}
>
<ModalLayout
title={
<DangerousRaw>
{t(
type === RoleType.User
? 'user_details.roles.assign_title'
: 'application_details.roles.assign_title',
{ name: type === RoleType.User ? getUserTitle(entity) : entity.name }
)}
</DangerousRaw>
}
subtitle={
<DangerousRaw>
{t(
type === RoleType.User
? 'user_details.roles.assign_subtitle'
: 'application_details.roles.assign_subtitle',
{ name: type === RoleType.User ? getUserTitle(entity) : entity.name }
)}
</DangerousRaw>
}
size="large"
footer={
<Button
isLoading={isSubmitting}
disabled={roles.length === 0}
htmlType="submit"
title={
type === RoleType.User
? 'user_details.roles.confirm_assign'
: 'application_details.roles.confirm_assign'
}
size="large"
type="primary"
onClick={handleAssign}
/>
}
onClose={onClose}
>
<RolesTransfer
entityId={entity.id}
type={type}
value={roles}
onChange={(value) => {
setRoles(value);
}}
/>
</ModalLayout>
</ReactModal>
);
}
export default AssignToRoleModal;

View file

@ -13,6 +13,7 @@ import UserRoleIconDark from '@/assets/icons/user-role-dark.svg';
import UserRoleIcon from '@/assets/icons/user-role.svg';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import ItemPreview from '@/components/ItemPreview';
import RoleAssignmentModal from '@/components/RoleAssignmentModal';
import { defaultPageSize } from '@/consts';
import Button from '@/ds-components/Button';
import ConfirmModal from '@/ds-components/ConfirmModal';
@ -24,7 +25,6 @@ import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import useTheme from '@/hooks/use-theme';
import AssignToRoleModal from '@/pages/Roles/components/AssignToRoleModal';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import type { UserDetailsOutletContext } from '../types';
@ -172,7 +172,7 @@ function UserRoles() {
</ConfirmModal>
)}
{isAssignRolesModalOpen && (
<AssignToRoleModal
<RoleAssignmentModal
entity={user}
type={RoleType.User}
onClose={(success) => {

View file

@ -252,6 +252,27 @@ describe('applications', () => {
await waitForToast(page, { text: 'Application created successfully.' });
// Expect to assign management API access role for the M2M app
if (app.type === ApplicationType.MachineToMachine) {
await expectModalWithTitle(
page,
'Authorize app with machine-to-machine role for permissions'
);
await expect(page).toClick(
'.ReactModalPortal div[class$=rolesTransfer] div[class$=item] div',
{
text: 'Logto Management API access',
}
);
await expectToClickModalAction(page, 'Assign roles');
await waitForToast(page, {
text: 'Successfully assigned role(s)',
});
}
await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', {
text: app.name,
});

View file

@ -55,6 +55,13 @@ const applications = {
placeholder_title: 'Wähle einen Anwendungstyp, um fortzufahren',
placeholder_description:
'Logto verwendet eine Anwendungs-Entität für OIDC, um Aufgaben wie die Identifizierung deiner Apps, das Management der Anmeldung und die Erstellung von Prüfprotokollen zu erleichtern.',
m2m_role_assignment: {
title: 'App autorisieren mit maschinenbasierten Rollen für Berechtigungen',
subtitle:
'Maschine-zu-Maschine-Anwendungen erfordern eine autorisierte Maschine-zu-Maschine-Rolle.',
role_creation_hint:
'Hat keine Maschine-zu-Maschine-Rolle? <a>Erstellen Sie zuerst eine Maschine-zu-Maschine</a>-Rolle',
},
};
export default Object.freeze(applications);

View file

@ -53,6 +53,12 @@ const applications = {
placeholder_title: 'Select an application type to continue',
placeholder_description:
'Logto uses an application entity for OIDC to help with tasks such as identifying your apps, managing sign-in, and creating audit logs.',
m2m_role_assignment: {
title: 'Authorize app with machine-to-machine role for permissions',
subtitle: 'Machine-to-machine applications require authorized machine-to-machine role.',
role_creation_hint:
'Doesnt have a machine-to-machine role? <a>Create a machine-to-machine</a> role first',
},
};
export default Object.freeze(applications);

View file

@ -55,6 +55,13 @@ const applications = {
placeholder_title: 'Selecciona un tipo de aplicación para continuar',
placeholder_description:
'Logto utiliza una entidad de aplicación para OIDC para ayudar con tareas como la identificación de tus aplicaciones, la gestión de inicio de sesión y la creación de registros de auditoría.',
m2m_role_assignment: {
title: 'Autorizar la aplicación con rol de máquina a máquina para permisos',
subtitle:
'Las aplicaciones de máquina a máquina requieren un rol de máquina a máquina autorizado.',
role_creation_hint:
'¿No tiene un rol de máquina a máquina? <a>Cree primero un rol de máquina a máquina</a>',
},
};
export default Object.freeze(applications);

View file

@ -56,6 +56,12 @@ const applications = {
placeholder_title: "Sélectionnez un type d'application pour continuer",
placeholder_description:
"Logto utilise une entité d'application pour OIDC pour aider aux tâches telles que l'identification de vos applications, la gestion de la connexion et la création de journaux d'audit",
m2m_role_assignment: {
title: "Autoriser l'application avec un rôle machine à machine pour les permissions",
subtitle: 'Les applications machine à machine nécessitent un rôle machine à machine autorisé.',
role_creation_hint:
'Na pas de rôle machine à machine ? <a>Créez dabord un rôle machine à machine</a>',
},
};
export default Object.freeze(applications);

View file

@ -55,6 +55,13 @@ const applications = {
placeholder_title: 'Seleziona un tipo di applicazione per continuare',
placeholder_description:
"Logto utilizza un'entità applicazione per OIDC per aiutarti in compiti come l'identificazione delle tue app, la gestione dell'accesso e la creazione di registri di audit.",
m2m_role_assignment: {
title: "Autorizza l'applicazione con ruolo da macchina a macchina per le autorizzazioni",
subtitle:
'Le applicazioni da macchina a macchina richiedono un ruolo da macchina a macchina autorizzato.',
role_creation_hint:
'Non ha un ruolo da macchina a macchina? <a>Crea prima un ruolo da macchina a macchina</a>',
},
};
export default Object.freeze(applications);

View file

@ -54,6 +54,12 @@ const applications = {
placeholder_title: '続行するにはアプリケーションタイプを選択してください',
placeholder_description:
'LogtoはOIDCのためにアプリケーションエンティティを使用して、アプリケーションの識別、サインインの管理、監査ログの作成などのタスクをサポートします。',
m2m_role_assignment: {
title: 'アプリを権限付きのマシン間ロールで認可する',
subtitle: 'マシン間アプリケーションには承認されたマシン間ロールが必要です。',
role_creation_hint:
'マシン間ロールがありませんか? <a>まずマシン間</a> ロールを作成してください',
},
};
export default Object.freeze(applications);

View file

@ -54,6 +54,11 @@ const applications = {
placeholder_title: '어플리케이션 유형을 선택하여 계속하세요',
placeholder_description:
'Logto는 OIDC용 앱 엔티티를 사용하여 앱 식별, 로그인 관리 및 감사 로그 생성과 같은 작업을 지원합니다.',
m2m_role_assignment: {
title: '권한을 위해 기계 간 역할로 앱을 인증합니다',
subtitle: '기계 간 애플리케이션은 인증된 기계 간 역할이 필요합니다.',
role_creation_hint: '기계 간 역할이 없습니까? <a>먼저 기계 간</a> 역할을 만드십시오',
},
};
export default Object.freeze(applications);

View file

@ -55,6 +55,12 @@ const applications = {
placeholder_title: 'Wybierz typ aplikacji, aby kontynuować',
placeholder_description:
'Logto używa jednostki aplikacji dla OIDC, aby pomóc w takich zadaniach jak identyfikowanie Twoich aplikacji, zarządzanie logowaniem i tworzenie dzienników audytu.',
m2m_role_assignment: {
title: 'Autoryzuj aplikację z rolą maszyny do maszyny dla uprawnień',
subtitle: 'Aplikacje maszyna-do-maszyna wymagają autoryzowanej roli maszyna-do-maszyna.',
role_creation_hint:
'Nie ma roli maszyna-do-maszyna? <a>Najpierw utwórz rolę maszyna-do-maszyna</a>',
},
};
export default Object.freeze(applications);

View file

@ -55,6 +55,13 @@ const applications = {
placeholder_title: 'Selecione um tipo de aplicativo para continuar',
placeholder_description:
'O Logto usa uma entidade de aplicativo para OIDC para ajudar nas tarefas, como identificar seus aplicativos, gerenciar o login e criar logs de auditoria.',
m2m_role_assignment: {
title: 'Autorizar aplicativo com função de máquina para máquina para permissões',
subtitle:
'Aplicativos de máquina para máquina requerem uma função de máquina para máquina autorizada.',
role_creation_hint:
'Não tem uma função de máquina para máquina? <a>Crie primeiro uma função de máquina para máquina</a>',
},
};
export default Object.freeze(applications);

View file

@ -54,6 +54,13 @@ const applications = {
placeholder_title: 'Selecione um tipo de aplicação para continuar',
placeholder_description:
'O Logto usa uma entidade de aplicativo para OIDC para ajudar em tarefas como identificar seus aplicativos, gerenciar o registro e criar registros de auditoria.',
m2m_role_assignment: {
title: 'Autorizar aplicação com papel de máquina para máquina para permissões',
subtitle:
'Aplicações de máquina para máquina requerem uma função de máquina para máquina autorizada.',
role_creation_hint:
'Não tem uma função de máquina para máquina? <a>Crie primeiro uma função de máquina para máquina</a>',
},
};
export default Object.freeze(applications);

View file

@ -55,6 +55,12 @@ const applications = {
placeholder_title: 'Выберите тип приложения, чтобы продолжить',
placeholder_description:
'Logto использует сущность приложения для OIDC для выполнения задач, таких как идентификация ваших приложений, управление входом в систему и создание журналов аудита.',
m2m_role_assignment: {
title: 'Авторизовать приложение с ролью от машины к машине для разрешений',
subtitle: 'Приложения от машины к машине требуют авторизованной роли от машины к машине.',
role_creation_hint:
'У вас нет роли от машины к машине? <a>Сначала создайте роль от машины к машине</a>',
},
};
export default Object.freeze(applications);

View file

@ -55,6 +55,12 @@ const applications = {
placeholder_title: 'Devam etmek için bir uygulama tipi seçin',
placeholder_description:
'Logto, uygulamanızı tanımlamaya, oturum açmayı yönetmeye ve denetim kayıtları oluşturmaya yardımcı olmak için OIDC için bir uygulama varlığı kullanır.',
m2m_role_assignment: {
title: 'İzinler için makineye özel rolle uygulamayı yetkilendir',
subtitle: 'Makine-makine uygulamaları yetkilendirilmiş bir makine-makine rolü gerektirir.',
role_creation_hint:
'Makine-makine rolünüz yok mu? <a>Önce bir makine-makine</a> rolü oluşturun',
},
};
export default Object.freeze(applications);

View file

@ -52,6 +52,11 @@ const applications = {
placeholder_title: '选择应用程序类型以继续',
placeholder_description:
'Logto 使用 OIDC 的应用程序实体来帮助识别你的应用程序、管理登录和创建审计日志等任务。',
m2m_role_assignment: {
title: '使用机器到机器角色授权应用程序以获取权限',
subtitle: '机器对机器应用程序需要经过授权的机器对机器角色。',
role_creation_hint: '没有机器对机器角色?先 <a>创建机器对机器</a> 角色',
},
};
export default Object.freeze(applications);

View file

@ -52,6 +52,11 @@ const applications = {
placeholder_title: '選擇應用程式類型以繼續',
placeholder_description:
'Logto 使用 OIDC 的應用程式實體來幫助識別您的應用程式、管理登錄和創建審核日誌等任務。',
m2m_role_assignment: {
title: '使用機器到機器角色為應用程式授權權限',
subtitle: '機器對機器應用程式需要經過授權的機器對機器角色。',
role_creation_hint: '沒有機器對機器角色?先 <a>創建機器對機器</a> 角色',
},
};
export default Object.freeze(applications);

View file

@ -52,6 +52,11 @@ const applications = {
placeholder_title: '選擇應用程式類型以繼續',
placeholder_description:
'Logto 使用 OIDC 的應用程式實體來幫助識別你的應用程式、管理登入和創建審計日誌等任務。',
m2m_role_assignment: {
title: '使用機器到機器角色為應用程式授權權限',
subtitle: '機器對機器應用程式需要經過授權的機器對機器角色。',
role_creation_hint: '沒有機器對機器角色? 先 <a>創建機器對機器</a> 角色',
},
};
export default Object.freeze(applications);