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

Merge pull request #6084 from logto-io/gao-org-role-types

feat: organization role types
This commit is contained in:
Gao Sun 2024-06-23 10:45:27 +08:00 committed by GitHub
commit 022b5fe6cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 187 additions and 71 deletions

View file

@ -1,4 +1,4 @@
import { type OrganizationRole } from '@logto/schemas';
import { roleTypeToKey, type OrganizationRole } from '@logto/schemas';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
@ -81,7 +81,7 @@ function OrganizationRoleDetails() {
<DetailsPageHeader
icon={<ThemedIcon for={OrgRoleIcon} size={60} />}
title={data.name}
primaryTag={t('organization_role_details.org_role')}
primaryTag={t(`roles.type_${roleTypeToKey[data.type]}`)}
identifier={{ name: 'ID', value: data.id }}
actionMenuItems={[
{

View file

@ -1,19 +1,32 @@
import { type OrganizationRole } from '@logto/schemas';
import { type AdminConsoleKey } from '@logto/phrases';
import { RoleType, type OrganizationRole } from '@logto/schemas';
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { isDevFeaturesEnabled } from '@/consts/env';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField';
import ModalLayout from '@/ds-components/ModalLayout';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { trySubmitSafe } from '@/utils/form';
type FormData = Pick<OrganizationRole, 'name' | 'description'>;
import * as styles from './index.module.scss';
type FormData = Pick<OrganizationRole, 'name' | 'description' | 'type'>;
type RadioOption = { key: AdminConsoleKey; value: RoleType };
const radioOptions: RadioOption[] = [
{ key: 'roles.type_user', value: RoleType.User },
{ key: 'roles.type_machine_to_machine', value: RoleType.MachineToMachine },
];
type Props = {
readonly isOpen: boolean;
@ -25,10 +38,11 @@ function CreateOrganizationRoleModal({ isOpen, onClose }: Props) {
const {
register,
control,
formState: { errors, isSubmitting },
handleSubmit,
reset,
} = useForm<FormData>();
} = useForm<FormData>({ defaultValues: { type: RoleType.User } });
const onCloseHandler = useCallback(
(createdData?: OrganizationRole) => {
@ -72,9 +86,10 @@ function CreateOrganizationRoleModal({ isOpen, onClose }: Props) {
onClick={submit}
/>
}
className={styles.createModal}
onClose={onCloseHandler}
>
<FormField isRequired title="organization_template.roles.create_modal.name_field">
<FormField isRequired title="organization_template.roles.create_modal.name">
<TextInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
@ -83,13 +98,36 @@ function CreateOrganizationRoleModal({ isOpen, onClose }: Props) {
{...register('name', { required: true })}
/>
</FormField>
<FormField title="organization_template.permissions.description_field_name">
<FormField title="organization_template.roles.create_modal.description">
<TextInput
placeholder={t('organization_role_details.general.description_field_placeholder')}
error={Boolean(errors.description)}
{...register('description')}
/>
</FormField>
{/* TODO: Remove */}
{isDevFeaturesEnabled && (
<FormField title="organization_template.roles.create_modal.type">
<Controller
name="type"
control={control}
render={({ field: { onChange, value, name } }) => (
<RadioGroup
name={name}
className={styles.roleTypes}
value={value}
onChange={(value) => {
onChange(value);
}}
>
{radioOptions.map(({ key, value }) => (
<Radio key={value} title={<DynamicT forKey={key} />} value={value} />
))}
</RadioGroup>
)}
/>
</FormField>
)}
</ModalLayout>
</ReactModal>
);

View file

@ -1,5 +1,11 @@
@use '@/scss/underscore' as _;
.createModal {
.roleTypes {
margin-top: _.unit(2);
}
}
.permissions {
display: flex;
flex-wrap: wrap;

View file

@ -1,4 +1,4 @@
import { type OrganizationRoleWithScopes } from '@logto/schemas';
import { roleTypeToKey, type OrganizationRoleWithScopes } from '@logto/schemas';
import { cond } from '@silverhand/essentials';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -88,6 +88,14 @@ function OrganizationRoles() {
);
},
},
{
title: <DynamicT forKey="organization_template.roles.type_column" />,
dataIndex: 'type',
colSpan: 4,
render: ({ type }) => {
return <DynamicT forKey={`roles.type_${roleTypeToKey[type]}`} />;
},
},
]}
rowClickHandler={({ id }) => {
navigate(id);

View file

@ -1,4 +1,4 @@
import { RoleType, type RoleResponse } from '@logto/schemas';
import { RoleType, roleTypeToKey, type RoleResponse } from '@logto/schemas';
import { Theme } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next';
@ -108,11 +108,7 @@ function Roles() {
title: t('roles.col_type'),
dataIndex: 'type',
colSpan: 4,
render: ({ type }) => (
<Breakable>
{type === RoleType.User ? t('roles.type_user') : t('roles.type_machine_to_machine')}
</Breakable>
),
render: ({ type }) => <Breakable>{t(`roles.type_${roleTypeToKey[type]}`)}</Breakable>,
},
{
title: t('roles.col_description'),

View file

@ -3,6 +3,7 @@ import {
type OrganizationRole,
type OrganizationRoleWithScopes,
type Scope,
type RoleType,
} from '@logto/schemas';
import { authedAdminApi } from './api.js';
@ -11,6 +12,7 @@ import { ApiFactory } from './factory.js';
export type CreateOrganizationRolePostData = {
name: string;
description?: string;
type?: RoleType;
organizationScopeIds?: string[];
resourceScopeIds?: string[];
};

View file

@ -4,6 +4,7 @@ import {
ApplicationType,
type ApplicationWithOrganizationRoles,
type Application,
RoleType,
} from '@logto/schemas';
import { HTTPError } from 'ky';
@ -70,8 +71,14 @@ devFeatureTest.describe('organization application APIs', () => {
const organizationId = organizationApi.organizations[0]!.id;
const app = applications[0]!;
const roles = await Promise.all([
organizationApi.roleApi.create({ name: generateTestName() }),
organizationApi.roleApi.create({ name: generateTestName() }),
organizationApi.roleApi.create({
name: generateTestName(),
type: RoleType.MachineToMachine,
}),
organizationApi.roleApi.create({
name: generateTestName(),
type: RoleType.MachineToMachine,
}),
]);
const roleIds = roles.map(({ id }) => id);
await organizationApi.addApplicationRoles(organizationId, app.id, roleIds);
@ -202,7 +209,10 @@ devFeatureTest.describe('organization application APIs', () => {
it('should be able to add and delete organization application role', async () => {
const organization = await organizationApi.create({ name: 'test' });
const role = await organizationApi.roleApi.create({ name: `test-${generateTestName()}` });
const role = await organizationApi.roleApi.create({
name: `test-${generateTestName()}`,
type: RoleType.MachineToMachine,
});
const application = await createApplication(
generateTestName(),
ApplicationType.MachineToMachine
@ -227,5 +237,27 @@ devFeatureTest.describe('organization application APIs', () => {
assert(response instanceof HTTPError);
expect(response.response.status).toBe(422);
});
it('should fail when try to add role that is not machine-to-machine type', async () => {
const organization = await organizationApi.create({ name: 'test' });
const role = await organizationApi.roleApi.create({
name: `test-${generateTestName()}`,
type: RoleType.User,
});
const application = await createApplication(
generateTestName(),
ApplicationType.MachineToMachine
);
await organizationApi.applications.add(organization.id, [application.id]);
const response = await organizationApi
.addApplicationRoles(organization.id, application.id, [role.id])
.catch((error: unknown) => error);
assert(response instanceof HTTPError);
expect(response.response.status).toBe(422);
expect(await response.response.json()).toMatchObject(
expect.objectContaining({ code: 'entity.db_constraint_violated' })
);
});
});
});

View file

@ -1,10 +1,11 @@
import assert from 'node:assert';
import { RoleType } from '@logto/schemas';
import { HTTPError } from 'ky';
import { OrganizationApiTest } from '#src/helpers/organization.js';
import { UserApiTest } from '#src/helpers/user.js';
import { generateTestName } from '#src/utils.js';
import { devFeatureTest, generateTestName } from '#src/utils.js';
describe('organization user APIs', () => {
describe('organization get users', () => {
@ -287,6 +288,25 @@ describe('organization user APIs', () => {
expect.objectContaining({ code: 'entity.not_found' })
);
});
devFeatureTest.it('should fail when try to add role that is not user type', async () => {
const organization = await organizationApi.create({ name: 'test' });
const user = await userApi.create({ username: generateTestName() });
const role = await roleApi.create({
name: generateTestName(),
type: RoleType.MachineToMachine,
});
await organizationApi.addUsers(organization.id, [user.id]);
const response = await organizationApi
.addUserRoles(organization.id, user.id, [role.id])
.catch((error: unknown) => error);
assert(response instanceof HTTPError);
expect(response.response.status).toBe(422);
expect(await response.response.json()).toMatchObject(
expect.objectContaining({ code: 'entity.db_constraint_violated' })
);
});
});
describe('organization - user - organization role - organization scopes relation', () => {

View file

@ -46,7 +46,7 @@ export const createM2mRoleAndAssignPermissions = async (
await expectModalWithTitle(page, 'Create role');
await expect(page).toClick('div[class*=radioGroup][class$=roleTypes] div[class$=content]', {
text: 'Machine-to-machine role',
text: 'Machine-to-machine',
});
await expect(page).toFillForm('.ReactModalPortal form', {

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: 'Organisationsrollendetails',
back_to_org_roles: 'Zurück zu den Organisationsrollen',
org_role: 'Org-Rolle',
delete_confirm:
'Dadurch werden die mit dieser Rolle verbundenen Berechtigungen von den betroffenen Benutzern entfernt und die Beziehungen zwischen Organisationsrollen, Mitgliedern in der Organisation und Organisationsberechtigungen gelöscht.',
deleted: 'Die Organisationsrolle {{name}} wurde erfolgreich gelöscht.',

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Rolle erstellen',
role_name: 'Rollenname',
role_type: 'Rollenart',
type_user: 'Benutzerrolle',
type_machine_to_machine: 'Rolle von Maschine zu Maschine',
role_description: 'Beschreibung',
role_name_placeholder: 'Geben Sie Ihren Rollennamen ein',
role_description_placeholder: 'Geben Sie Ihre Rollenbeschreibung ein',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: 'Organization role details',
back_to_org_roles: 'Back to organization roles',
org_role: 'Organization role',
delete_confirm:
'Doing so will remove the permissions associated with this role from the affected users and delete the relations among organization roles, members in the organization, and organization permissions.',
deleted: 'Organization role {{name}} was successfully deleted.',

View file

@ -8,14 +8,16 @@ const organization_template = {
create_title: 'Create organization role',
role_column: 'Organization role',
permissions_column: 'Permissions',
type_column: 'Role type',
placeholder_title: 'Organization role',
placeholder_description:
'Organization role is a grouping of permissions that can be assigned to users. The permissions must come from the predefined organization permissions.',
create_modal: {
title: 'Create organization role',
create: 'Create role',
name_field: 'Role name',
description_field: 'Description',
name: 'Role name',
description: 'Description',
type: 'Role type',
created: 'Organization role {{name}} has been successfully created.',
},
},

View file

@ -6,8 +6,8 @@ const roles = {
create: 'Create role',
role_name: 'Role name',
role_type: 'Role type',
type_user: 'User role',
type_machine_to_machine: 'Machine-to-machine role',
type_user: 'User',
type_machine_to_machine: 'Machine-to-machine',
role_description: 'Description',
role_name_placeholder: 'Enter your role name',
role_description_placeholder: 'Enter your role description',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: 'Detalles del rol de la organización',
back_to_org_roles: 'Volver a los roles de la organización',
org_role: 'Rol de la organización',
delete_confirm:
'Al hacerlo, se eliminarán los permisos asociados con este rol de los usuarios afectados y se borrarán las relaciones entre roles de organización, miembros en la organización y permisos de organización.',
deleted: 'El rol de organización {{name}} se eliminó con éxito.',

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Crear Rol',
role_name: 'Nombre de rol',
role_type: 'Tipo de rol',
type_user: 'Rol de usuario',
type_machine_to_machine: 'Rol de máquina a máquina',
role_description: 'Descripción',
role_name_placeholder: 'Ingrese el nombre de su rol',
role_description_placeholder: 'Ingrese la descripción de su rol',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: "Détails du rôle de l'organisation",
back_to_org_roles: "Retour aux rôles de l'organisation",
org_role: "Rôle de l'organisation",
delete_confirm:
"Cela supprimera les autorisations associées à ce rôle des utilisateurs concernés et supprimera les relations entre les rôles de l'organisation, les membres de l'organisation et les autorisations de l'organisation.",
deleted: "Le rôle de l'organisation {{name}} a été supprimé avec succès.",

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Créer un rôle',
role_name: 'Nom du rôle',
role_type: 'Type de rôle',
type_user: 'Rôle utilisateur',
type_machine_to_machine: 'Rôle de machine à machine',
role_description: 'Description',
role_name_placeholder: 'Entrez le nom de votre rôle',
role_description_placeholder: 'Entrez la description de votre rôle',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: "Dettagli del ruolo dell'organizzazione",
back_to_org_roles: "Torna ai ruoli dell'organizzazione",
org_role: "Ruolo dell'organizzazione",
delete_confirm:
"Facendo ciò, verranno rimossi i permessi associati a questo ruolo dagli utenti interessati e verranno eliminati i rapporti tra ruoli organizzativi, membri nell'organizzazione e permessi dell'organizzazione.",
deleted: "Il ruolo dell'organizzazione {{name}} è stato cancellato con successo.",

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Crea Ruolo',
role_name: 'Nome ruolo',
role_type: 'Tipo ruolo',
type_user: 'Ruolo utente',
type_machine_to_machine: 'Ruolo da macchina a macchina',
role_description: 'Descrizione',
role_name_placeholder: 'Inserisci il nome del tuo ruolo',
role_description_placeholder: 'Inserisci la descrizione del tuo ruolo',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: '組織の役割の詳細',
back_to_org_roles: '組織の役割に戻る',
org_role: '組織の役割',
delete_confirm:
'これにより、関連するユーザーからこのロールに関連付けられた権限が削除され、組織の役割、組織のメンバー、および組織の権限間の関係が削除されます。',
deleted: '組織の役割{{name}}が正常に削除されました。',

View file

@ -6,8 +6,6 @@ const roles = {
create: 'ロールを作成する',
role_name: '役割名',
role_type: '役割タイプ',
type_user: 'ユーザーの役割',
type_machine_to_machine: 'マシン間の役割',
role_description: '説明',
role_name_placeholder: 'ロールの名前を入力してください',
role_description_placeholder: 'ロールの説明を入力してください',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: '조직 역할 세부 정보',
back_to_org_roles: '조직 역할로 돌아가기',
org_role: '조직 역할',
delete_confirm:
'이렇게 하면 해당 역할과 관련된 사용자의 권한이 제거되고 조직 역할, 조직 구성원 및 조직 권한 간의 관계가 삭제됩니다.',
deleted: '조직 역할 {{name}} 이(가) 성공적으로 삭제되었습니다.',

View file

@ -6,8 +6,6 @@ const roles = {
create: '역할 생성',
role_name: '역할 이름',
role_type: '역할 유형',
type_user: '사용자 역할',
type_machine_to_machine: '기계 간 역할',
role_description: '설명',
role_name_placeholder: '역할 이름을 입력하세요',
role_description_placeholder: '역할 설명을 입력하세요',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: 'Szczegóły roli organizacji',
back_to_org_roles: 'Powrót do ról organizacyjnych',
org_role: 'Rola organizacji',
delete_confirm:
'W wyniku tego zostaną usunięte uprawnienia związane z tą rolą od dotkniętych użytkowników i zostaną usunięte związki między rolami organizacyjnymi, członkami organizacji a uprawnieniami organizacji.',
deleted: 'Rola organizacji {{name}} została pomyślnie usunięta.',

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Utwórz rolę',
role_name: 'Nazwa roli',
role_type: 'Typ roli',
type_user: 'Rola użytkownika',
type_machine_to_machine: 'Rola maszyny do maszyny',
role_description: 'Opis',
role_name_placeholder: 'Wprowadź nazwę swojej roli',
role_description_placeholder: 'Wprowadź opis swojej roli',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: 'Detalhes da função da organização',
back_to_org_roles: 'Voltar para os papéis da organização',
org_role: 'Função da organização',
delete_confirm:
'Ao fazer isso, os privilégios associados a esta função serão removidos dos usuários afetados e as relações entre funções da organização, membros na organização e permissões da organização serão excluídas.',
deleted: 'O papel da organização {{name}} foi excluído com sucesso.',

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Criar função',
role_name: 'Nome da função',
role_type: 'Tipo de função',
type_user: 'Função do usuário',
type_machine_to_machine: 'Função de máquina para máquina',
role_description: 'Descrição',
role_name_placeholder: 'Insira o nome da sua função',
role_description_placeholder: 'Insira a descrição da sua função',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: 'Detalhes da função da organização',
back_to_org_roles: 'Voltar aos papéis da organização',
org_role: 'Função da organização',
delete_confirm:
'Ao fazê-lo, serão removidas as permissões associadas a esta função dos utilizadores afetados e serão eliminadas as relações entre funções da organização, membros na organização e permissões da organização.',
deleted: 'O papel da organização {{name}} foi eliminado com sucesso.',

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Criar papel',
role_name: 'Nome do papel',
role_type: 'Tipo de papel',
type_user: 'Função de usuário',
type_machine_to_machine: 'Função de máquina para máquina',
role_description: 'Descrição',
role_name_placeholder: 'Digite o nome do papel',
role_description_placeholder: 'Digite a descrição do papel',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: 'Детали роли организации',
back_to_org_roles: 'Вернуться к ролям в организации',
org_role: 'Роль организации',
delete_confirm:
'При этом будут удалены разрешения, связанные с этой ролью, у затронутых пользователей, и будут удалены связи между ролями организации, членами организации и правами организации.',
deleted: 'Роль в организации {{name}} была успешно удалена.',

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Создать роль',
role_name: 'Имя роли',
role_type: 'Тип роли',
type_user: 'Роль пользователя',
type_machine_to_machine: 'Роль машина-машина',
role_description: 'Описание',
role_name_placeholder: 'Введите название роли',
role_description_placeholder: 'Введите описание роли',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: 'Kuruluş rolü ayrıntıları',
back_to_org_roles: 'Organizasyon rollerine geri dön',
org_role: 'Kuruluş rolü',
delete_confirm:
'Bunu yapmak, etkilenen kullanıcılardan bu role ilişkilendirilen izinleri kaldıracak ve organizasyon rolleri, organizasyon üyeleri ve organizasyon izinleri arasındaki ilişkileri silecektir.',
deleted: 'Kuruluş rolü {{name}} başarıyla silindi.',

View file

@ -6,8 +6,6 @@ const roles = {
create: 'Rol Oluştur',
role_name: 'Rol adı',
role_type: 'Rol tipi',
type_user: 'Kullanıcı rolü',
type_machine_to_machine: 'Makineden makineye rol',
role_description: 'Açıklama',
role_name_placeholder: 'Rol adınızı girin',
role_description_placeholder: 'Rol açıklamanızı girin',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: '组织角色详情',
back_to_org_roles: '返回组织角色',
org_role: '组织角色',
delete_confirm:
'这样做将从受影响的用户中删除与此角色关联的权限,并删除组织角色、组织成员和组织权限之间的关系。',
deleted: '组织角色 {{name}} 已成功删除。',

View file

@ -6,8 +6,6 @@ const roles = {
create: '创建角色',
role_name: '角色名称',
role_type: '角色类型',
type_user: '用户角色',
type_machine_to_machine: '机器对机器角色',
role_description: '描述',
role_name_placeholder: '输入你的角色名称',
role_description_placeholder: '输入你的角色描述',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: '組織角色詳情',
back_to_org_roles: '返回組織角色',
org_role: '組織角色',
delete_confirm:
'這樣做將會從受影響的使用者中移除與此角色相關聯的權限,並刪除組織角色、組織成員和組織權限之間的關係。',
deleted: '組織角色 {{name}} 已成功刪除。',

View file

@ -6,8 +6,6 @@ const roles = {
create: '創建角色',
role_name: '角色名稱',
role_type: '角色類型',
type_user: '用戶角色',
type_machine_to_machine: '機器對機器角色',
role_description: '描述',
role_name_placeholder: '輸入你的角色名稱',
role_description_placeholder: '輸入你的角色描述',

View file

@ -1,7 +1,6 @@
const organization_role_details = {
page_title: '組織角色詳情',
back_to_org_roles: '返回組織角色',
org_role: '組織角色',
delete_confirm:
'這樣做將會從受影響的使用者中移除與此角色相關聯的權限,並刪除組織角色、組織成員和組織權限之間的關係。',
deleted: '組織角色 {{name}} 已成功刪除。',

View file

@ -6,8 +6,6 @@ const roles = {
create: '建立角色',
role_name: '角色名稱',
role_type: '角色類型',
type_user: '使用者角色',
type_machine_to_machine: '機器對機器角色',
role_description: '描述',
role_name_placeholder: '輸入你的角色名稱',
role_description_placeholder: '輸入你的角色描述',

View file

@ -0,0 +1,35 @@
import { sql } from '@silverhand/slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
alter table organization_roles
add column type role_type not null default 'User';
create function check_organization_role_type(role_id varchar(21), target_type role_type) returns boolean as
$$ begin
return (select type from organization_roles where id = role_id) = target_type;
end; $$ language plpgsql;
alter table organization_role_user_relations
add constraint organization_role_user_relations__role_type
check (check_organization_role_type(organization_role_id, 'User'));
alter table organization_role_application_relations
add constraint organization_role_application_relations__role_type
check (check_organization_role_type(organization_role_id, 'MachineToMachine'));
`);
},
down: async (pool) => {
await pool.query(sql`
alter table organization_role_application_relations
drop constraint organization_role_application_relations__role_type;
alter table organization_role_user_relations
drop constraint organization_role_user_relations__role_type;
alter table organization_roles
drop column type;
drop function check_organization_role_type;
`);
},
};
export default alteration;

View file

@ -1,4 +1,4 @@
import type { Role } from '../db-entries/index.js';
import { RoleType, type Role } from '../db-entries/index.js';
import { type FeaturedApplication } from './application.js';
import { type FeaturedUser } from './user.js';
@ -9,3 +9,9 @@ export type RoleResponse = Role & {
applicationsCount: number;
featuredApplications: FeaturedApplication[];
};
/** The role type to i18n key mapping. */
export const roleTypeToKey = Object.freeze({
[RoleType.User]: 'user',
[RoleType.MachineToMachine]: 'machine_to_machine',
} as const satisfies Record<RoleType, string>);

View file

@ -8,6 +8,7 @@
*/
import {
RoleType,
type CreateOrganization,
type OrganizationRole,
type OrganizationScope,
@ -147,6 +148,7 @@ const tenantRoleDescriptions: Readonly<Record<TenantRole, string>> = Object.free
* id: 'collaborator',
* name: 'collaborator',
* description: 'Collaborator of the tenant, who has permissions to operate the tenant data, but not the tenant settings.',
* type: RoleType.User,
* });
* ```
*
@ -158,6 +160,7 @@ export const getTenantRole = (role: TenantRole): Readonly<OrganizationRole> =>
id: role,
name: role,
description: tenantRoleDescriptions[role],
type: RoleType.User,
});
/**

View file

@ -12,5 +12,7 @@ create table organization_role_application_relations (
/** Application's roles in an organization should be synchronized with the application's membership in the organization. */
foreign key (tenant_id, organization_id, application_id)
references organization_application_relations (tenant_id, organization_id, application_id)
on update cascade on delete cascade
on update cascade on delete cascade,
constraint organization_role_application_relations__role_type
check (check_organization_role_type(organization_role_id, 'MachineToMachine'))
);

View file

@ -12,5 +12,7 @@ create table organization_role_user_relations (
/** User's roles in an organization should be synchronized with the user's membership in the organization. */
foreign key (tenant_id, organization_id, user_id)
references organization_user_relations (tenant_id, organization_id, user_id)
on update cascade on delete cascade
on update cascade on delete cascade,
constraint organization_role_user_relations__role_type
check (check_organization_role_type(organization_role_id, 'User'))
);

View file

@ -1,4 +1,4 @@
/* init_order = 1 */
/* init_order = 1.1 */
/** The roles defined by the organization template. */
create table organization_roles (
@ -10,6 +10,8 @@ create table organization_roles (
name varchar(128) not null,
/** A brief description of the organization role. */
description varchar(256),
/** The type of the organization role. Same as the `type` field in the `roles` table. */
type role_type not null default 'User',
primary key (id),
constraint organization_roles__name
unique (tenant_id, name)
@ -17,3 +19,8 @@ create table organization_roles (
create index organization_roles__id
on organization_roles (tenant_id, id);
create function check_organization_role_type(role_id varchar(21), target_type role_type) returns boolean as
$$ begin
return (select type from organization_roles where id = role_id) = target_type;
end; $$ language plpgsql;