From 48a28a0b81360363da93835cd165deb5cef3b682 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Wed, 25 Feb 2026 19:42:11 +0700 Subject: [PATCH] Configure who should be able to send invitation link (#10555) * Configure who should be able to send invitation link Signed-off-by: Artem Savchenko * Add user role select Signed-off-by: Artem Savchenko * Limit default user roles Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- common/config/rush/pnpm-lock.yaml | 5 +- models/setting/src/index.ts | 13 +- plugins/login-assets/lang/en.json | 6 +- plugins/login-resources/package.json | 1 + .../src/components/InviteLink.svelte | 49 +++- plugins/setting-assets/lang/cs.json | 3 + plugins/setting-assets/lang/de.json | 3 + plugins/setting-assets/lang/en.json | 3 + plugins/setting-assets/lang/es.json | 3 + plugins/setting-assets/lang/fr.json | 3 + plugins/setting-assets/lang/it.json | 3 + plugins/setting-assets/lang/ja.json | 3 + plugins/setting-assets/lang/pt-br.json | 3 + plugins/setting-assets/lang/pt.json | 3 + plugins/setting-assets/lang/ru.json | 3 + plugins/setting-assets/lang/tr.json | 3 + plugins/setting-assets/lang/zh.json | 3 + .../src/components/ClassAttributesList.svelte | 10 +- .../src/components/EnumValuesList.svelte | 8 +- .../src/components/General.svelte | 1 - .../src/components/InviteSetting.svelte | 269 ++++++++++++++++-- .../src/components/OfficeSettings.svelte | 12 +- .../src/components/UserRoleSelect.svelte | 51 ++++ .../src/hasRoleCapabilityAsync.ts | 43 +++ plugins/setting-resources/src/index.ts | 8 +- .../setting-resources/src/roleCapability.ts | 60 ++++ plugins/setting/src/index.ts | 38 ++- .../src/components/AccountPopup.svelte | 18 +- 28 files changed, 567 insertions(+), 61 deletions(-) create mode 100644 plugins/setting-resources/src/components/UserRoleSelect.svelte create mode 100644 plugins/setting-resources/src/hasRoleCapabilityAsync.ts create mode 100644 plugins/setting-resources/src/roleCapability.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 22d3fffd38..b6b4d32982 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22895,6 +22895,9 @@ importers: '@hcengineering/setting': specifier: workspace:^0.7.0 version: link:../setting + '@hcengineering/setting-resources': + specifier: workspace:^0.7.0 + version: link:../setting-resources '@hcengineering/theme': specifier: workspace:^0.7.0 version: link:../../packages/theme @@ -61374,7 +61377,7 @@ snapshots: node-loader@2.0.0(webpack@5.102.1): dependencies: loader-utils: 2.0.4 - webpack: 5.102.1(esbuild@0.25.12)(webpack-cli@5.1.4) + webpack: 5.102.1 node-localstorage@2.2.1: dependencies: diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index 982c66c418..3f6e1a7d86 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -25,7 +25,7 @@ import { type IntegrationKind } from '@hcengineering/core' import exportPlugin from '@hcengineering/export' -import { Mixin, Model, UX, type Builder } from '@hcengineering/model' +import { Mixin, Model, Prop, TypeRecord, UX, type Builder } from '@hcengineering/model' import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core' import view, { createAction } from '@hcengineering/model-view' import notification from '@hcengineering/notification' @@ -39,6 +39,7 @@ import { type IntegrationType, type InviteSettings, type OfficeSettings, + type RoleCapabilitySettings, type SettingsCategory, type SpaceTypeCreator, type SpaceTypeEditor, @@ -112,6 +113,15 @@ export class TInviteSettings extends TConfiguration implements InviteSettings { expirationTime!: number emailMask!: string limit!: number + defaultInviteRole!: AccountRole + inviteLinkGeneratorRoles!: AccountRole[] +} + +@Model(setting.class.RoleCapabilitySettings, core.class.Configuration, DOMAIN_SETTING) +@UX(setting.string.RoleCapabilitySettings) +export class TRoleCapabilitySettings extends TConfiguration implements RoleCapabilitySettings { + @Prop(TypeRecord(), setting.string.RoleCapabilitySettings) + roleByCapability!: Record } @Model(setting.class.OfficeSettings, core.class.Configuration, DOMAIN_SETTING) @@ -146,6 +156,7 @@ export function createModel (builder: Builder): void { TEditable, TUserMixin, TInviteSettings, + TRoleCapabilitySettings, TOfficeSettings, TWorkspaceSetting, TSpaceTypeEditor, diff --git a/plugins/login-assets/lang/en.json b/plugins/login-assets/lang/en.json index 4b62fb8738..8d6e126d50 100644 --- a/plugins/login-assets/lang/en.json +++ b/plugins/login-assets/lang/en.json @@ -42,11 +42,11 @@ "PasswordRecovery": "Password recovery", "RecoveryLinkSent": "Password recovery link sent to email", "UseWorkspaceInviteSettings": "Use workspace invite settings", - "LinkValidHours": "Link valid (hours):", + "LinkValidHours": "Invite link expiration (hours):", "EmailMask": "Email mask:", - "InviteLimit": "Invite limit:", + "InviteLimit": "Maximum uses per invite link:", "GetLink": "Get invite link", - "NoLimit": "No limit", + "NoLimit": "Unlimited uses:", "AlreadyJoined": "Already joined?", "ConfirmationSent": "A message has been sent to your email containing a link to confirm your address.", "ConfirmationSent2": "Please follow the link to complete your sign up.", diff --git a/plugins/login-resources/package.json b/plugins/login-resources/package.json index 854745f3c4..b220bb0768 100644 --- a/plugins/login-resources/package.json +++ b/plugins/login-resources/package.json @@ -47,6 +47,7 @@ "@hcengineering/core": "workspace:^0.7.24", "@hcengineering/presentation": "workspace:^0.7.0", "@hcengineering/setting": "workspace:^0.7.0", + "@hcengineering/setting-resources": "workspace:^0.7.0", "@hcengineering/theme": "workspace:^0.7.0", "@hcengineering/analytics": "workspace:^0.7.17", "@hcengineering/account-client": "workspace:^0.7.21", diff --git a/plugins/login-resources/src/components/InviteLink.svelte b/plugins/login-resources/src/components/InviteLink.svelte index e7bcf30397..d5e5614ea0 100644 --- a/plugins/login-resources/src/components/InviteLink.svelte +++ b/plugins/login-resources/src/components/InviteLink.svelte @@ -15,15 +15,16 @@ diff --git a/plugins/setting-assets/lang/cs.json b/plugins/setting-assets/lang/cs.json index 1c12c2648a..23d6a5deb7 100644 --- a/plugins/setting-assets/lang/cs.json +++ b/plugins/setting-assets/lang/cs.json @@ -84,6 +84,9 @@ "Hidden": "Skryté", "Configure": "Konfigurovat", "InviteSettings": "Nastavení pozvánek", + "RoleCapabilitySettings": "Oprávnění rolí", + "DefaultInviteRoleForJoin": "Výchozí role při připojení přes odkaz s pozvánkou:", + "InviteLinkGeneratorRoles": "Vyberte uživatelské role, které mohou vytvářet odkazy s pozvánkou:", "DefaultValue": "Výchozí hodnota", "SelectAValue": "Vyberte hodnotu", "DateOnly": "Pouze datum", diff --git a/plugins/setting-assets/lang/de.json b/plugins/setting-assets/lang/de.json index cd128e8fb7..40607524d8 100644 --- a/plugins/setting-assets/lang/de.json +++ b/plugins/setting-assets/lang/de.json @@ -90,6 +90,9 @@ "Hidden": "Ausgeblendet", "Configure": "Konfigurieren", "InviteSettings": "Einladungseinstellungen", + "RoleCapabilitySettings": "Rollenberechtigungen", + "DefaultInviteRoleForJoin": "Standardrolle nach dem Beitritt über Einladungslink:", + "InviteLinkGeneratorRoles": "Benutzerrollen auswählen, die Einladungslinks erstellen dürfen:", "DefaultValue": "Standardwert", "SelectAValue": "Wert auswählen", "DateOnly": "Nur Datum", diff --git a/plugins/setting-assets/lang/en.json b/plugins/setting-assets/lang/en.json index 9db23d77ab..a36482387c 100644 --- a/plugins/setting-assets/lang/en.json +++ b/plugins/setting-assets/lang/en.json @@ -90,6 +90,9 @@ "Hidden": "Hidden", "Configure": "Configure", "InviteSettings": "Invite settings", + "RoleCapabilitySettings": "Role permissions", + "DefaultInviteRoleForJoin": "Default role assigned after joining via invite link:", + "InviteLinkGeneratorRoles": "Select user roles who can generate invite links:", "DefaultValue": "Default value", "SelectAValue": "Select a value", "DateOnly": "Date only", diff --git a/plugins/setting-assets/lang/es.json b/plugins/setting-assets/lang/es.json index fa50b9b905..a086b03a79 100644 --- a/plugins/setting-assets/lang/es.json +++ b/plugins/setting-assets/lang/es.json @@ -90,6 +90,9 @@ "Hidden": "Oculto", "Configure": "Configurar", "InviteSettings": "Ajustes de invitación", + "RoleCapabilitySettings": "Permisos de rol", + "DefaultInviteRoleForJoin": "Rol predeterminado asignado al unirse mediante enlace de invitación:", + "InviteLinkGeneratorRoles": "Selecciona los roles de usuario que pueden generar enlaces de invitación:", "DefaultValue": "Valor predeterminado", "SelectAValue": "Seleccionar un valor", "DateOnly": "Solo fecha", diff --git a/plugins/setting-assets/lang/fr.json b/plugins/setting-assets/lang/fr.json index 4815544365..6e276f3bf6 100644 --- a/plugins/setting-assets/lang/fr.json +++ b/plugins/setting-assets/lang/fr.json @@ -90,6 +90,9 @@ "Hidden": "Caché", "Configure": "Configurer", "InviteSettings": "Paramètres d'invitation", + "RoleCapabilitySettings": "Autorisations de rôle", + "DefaultInviteRoleForJoin": "Rôle attribué par défaut après avoir rejoint via un lien d'invitation :", + "InviteLinkGeneratorRoles": "Sélectionnez les rôles utilisateurs pouvant générer des liens d'invitation :", "DefaultValue": "Valeur par défaut", "SelectAValue": "Sélectionnez une valeur", "DateOnly": "Date uniquement", diff --git a/plugins/setting-assets/lang/it.json b/plugins/setting-assets/lang/it.json index f8a431d8a7..d6accbf420 100644 --- a/plugins/setting-assets/lang/it.json +++ b/plugins/setting-assets/lang/it.json @@ -90,6 +90,9 @@ "Hidden": "Nascosto", "Configure": "Configura", "InviteSettings": "Impostazioni invito", + "RoleCapabilitySettings": "Autorizzazioni ruolo", + "DefaultInviteRoleForJoin": "Ruolo predefinito assegnato dopo l'accesso tramite link di invito:", + "InviteLinkGeneratorRoles": "Seleziona i ruoli utente che possono generare link di invito:", "DefaultValue": "Valore predefinito", "SelectAValue": "Seleziona un valore", "DateOnly": "Solo data", diff --git a/plugins/setting-assets/lang/ja.json b/plugins/setting-assets/lang/ja.json index c99d7e6874..3af5a48d4b 100644 --- a/plugins/setting-assets/lang/ja.json +++ b/plugins/setting-assets/lang/ja.json @@ -90,6 +90,9 @@ "Hidden": "非表示", "Configure": "構成", "InviteSettings": "招待設定", + "RoleCapabilitySettings": "ロール権限", + "DefaultInviteRoleForJoin": "招待リンクから参加した後に割り当てる既定のロール:", + "InviteLinkGeneratorRoles": "招待リンクを生成できるユーザーロールを選択:", "DefaultValue": "既定値", "SelectAValue": "値を選択", "DateOnly": "日付のみ", diff --git a/plugins/setting-assets/lang/pt-br.json b/plugins/setting-assets/lang/pt-br.json index 857fa63bf2..02ee1a0dba 100644 --- a/plugins/setting-assets/lang/pt-br.json +++ b/plugins/setting-assets/lang/pt-br.json @@ -90,6 +90,9 @@ "Hidden": "Oculto", "Configure": "Configurar", "InviteSettings": "Configurações de convite", + "RoleCapabilitySettings": "Permissões de função", + "DefaultInviteRoleForJoin": "Função padrão atribuída após entrar por link de convite:", + "InviteLinkGeneratorRoles": "Selecione as funções de usuário que podem gerar links de convite:", "DefaultValue": "Valor padrão", "SelectAValue": "Selecionar um valor", "DateOnly": "Somente data", diff --git a/plugins/setting-assets/lang/pt.json b/plugins/setting-assets/lang/pt.json index 325fec6bb7..299ca56960 100644 --- a/plugins/setting-assets/lang/pt.json +++ b/plugins/setting-assets/lang/pt.json @@ -90,6 +90,9 @@ "Hidden": "Oculto", "Configure": "Configurar", "InviteSettings": "Configurações de convite", + "RoleCapabilitySettings": "Permissões de função", + "DefaultInviteRoleForJoin": "Função padrão atribuída após entrar por link de convite:", + "InviteLinkGeneratorRoles": "Selecione as funções de usuário que podem gerar links de convite:", "DefaultValue": "Valor padrão", "SelectAValue": "Selecionar um valor", "DateOnly": "Somente data", diff --git a/plugins/setting-assets/lang/ru.json b/plugins/setting-assets/lang/ru.json index 567b221523..d3dc62745b 100644 --- a/plugins/setting-assets/lang/ru.json +++ b/plugins/setting-assets/lang/ru.json @@ -94,6 +94,9 @@ "Hidden": "Спрятанный", "Configure": "Настроить", "InviteSettings": "Настройки приглашений", + "RoleCapabilitySettings": "Настройки доступа", + "DefaultInviteRoleForJoin": "Роль по умолчанию после присоединения по ссылке-приглашению:", + "InviteLinkGeneratorRoles": "Выберите роли пользователей, которые могут создавать ссылки-приглашения:", "DefaultValue": "Значение по умолчанию", "SelectAValue": "Выбрать значение", "DateOnly": "Только дата", diff --git a/plugins/setting-assets/lang/tr.json b/plugins/setting-assets/lang/tr.json index 3897e30c0b..2cc5609af8 100644 --- a/plugins/setting-assets/lang/tr.json +++ b/plugins/setting-assets/lang/tr.json @@ -90,6 +90,9 @@ "Hidden": "Gizli", "Configure": "Yapılandır", "InviteSettings": "Davet ayarları", + "RoleCapabilitySettings": "Rol izinleri", + "DefaultInviteRoleForJoin": "Davet bağlantısı ile katıldıktan sonra atanacak varsayılan rol:", + "InviteLinkGeneratorRoles": "Davet bağlantısı oluşturabilecek kullanıcı rollarını seçin:", "DefaultValue": "Varsayılan değer", "SelectAValue": "Bir değer seç", "DateOnly": "Sadece tarih", diff --git a/plugins/setting-assets/lang/zh.json b/plugins/setting-assets/lang/zh.json index 6ff29f8825..fbfe8bd281 100644 --- a/plugins/setting-assets/lang/zh.json +++ b/plugins/setting-assets/lang/zh.json @@ -90,6 +90,9 @@ "Hidden": "隐藏", "Configure": "配置", "InviteSettings": "邀请设置", + "RoleCapabilitySettings": "角色权限", + "DefaultInviteRoleForJoin": "通过邀请链接加入后分配的默认角色:", + "InviteLinkGeneratorRoles": "选择可以生成邀请链接的用户角色:", "DefaultValue": "默认值", "SelectAValue": "选择一个值", "DateOnly": "仅日期", diff --git a/plugins/setting-resources/src/components/ClassAttributesList.svelte b/plugins/setting-resources/src/components/ClassAttributesList.svelte index 2405d21960..8ab7299d4d 100644 --- a/plugins/setting-resources/src/components/ClassAttributesList.svelte +++ b/plugins/setting-resources/src/components/ClassAttributesList.svelte @@ -138,13 +138,13 @@ ...extra.map((it) => ({ label: it.label, icon: it.icon, - action: async (_: any, evt: Event) => { + action: async (evtArg: unknown, evt: Event): Promise => { const r = await getResource(it.action) await r(attribute, evt, it.actionProps) } })) ) - showPopup(Menu, { actions }, getEventPositionElement(ev), () => { + void showPopup(Menu, { actions }, getEventPositionElement(ev), () => { hovered = null }) } @@ -155,8 +155,10 @@ return client.getHierarchy().getClass((type as RefTo).to)?.label case core.class.Collection: return client.getHierarchy().getClass((type as Collection).of)?.label - case core.class.ArrOf: - return (type as ArrOf).of?.label + case core.class.ArrOf: { + const arrOf = type as ArrOf + return arrOf.of !== undefined && arrOf.of !== null ? arrOf.of.label : undefined + } default: return undefined } diff --git a/plugins/setting-resources/src/components/EnumValuesList.svelte b/plugins/setting-resources/src/components/EnumValuesList.svelte index e9ce23531a..8133048fd3 100644 --- a/plugins/setting-resources/src/components/EnumValuesList.svelte +++ b/plugins/setting-resources/src/components/EnumValuesList.svelte @@ -45,7 +45,7 @@ return false } - function dragover (ev: MouseEvent, item: string) { + function dragover (ev: MouseEvent, item: string): void { const s = values.findIndex((p) => p === selected) const i = values.findIndex((p) => p === item) if (dragswap(ev, item)) { @@ -55,11 +55,11 @@ const dispatch = createEventDispatcher() - async function remove (target: string) { + async function remove (target: string): Promise { dispatch('remove', target) } - async function onDrop () { + async function onDrop (): Promise { dispatch('drop') } @@ -70,7 +70,7 @@ label: setting.string.Delete, action: () => { if (opened !== undefined) { - remove(values[opened]) + void remove(values[opened]) opened = undefined } } diff --git a/plugins/setting-resources/src/components/General.svelte b/plugins/setting-resources/src/components/General.svelte index 86ae9cb898..b88a6118e1 100644 --- a/plugins/setting-resources/src/components/General.svelte +++ b/plugins/setting-resources/src/components/General.svelte @@ -59,7 +59,6 @@ import settingsRes from '../plugin' import ApiTokenPopup from './ApiTokenPopup.svelte' import WorkspacePermissionEditor from './WorkspacePermissionEditor.svelte' - import de from 'date-fns/locale/de' let loading = true let isEditingName = false diff --git a/plugins/setting-resources/src/components/InviteSetting.svelte b/plugins/setting-resources/src/components/InviteSetting.svelte index afa94801e5..f635d6589d 100644 --- a/plugins/setting-resources/src/components/InviteSetting.svelte +++ b/plugins/setting-resources/src/components/InviteSetting.svelte @@ -13,34 +13,112 @@ // limitations under the License. -->
- +
-
-
- -
-
- noLimit && (limit = -1)} /> -
- {#if !noLimit} -
- +
+ {#if loading} +
+
+ {:else} + +
+
+ +
+
+
+ +
+
+ + {#if !noLimit} +
+
+ {/if} + +
+
+ + {#if canManagePermissions} +
+
+
+ +
+
+ {/if} +
+
+
{/if} -
-
diff --git a/plugins/setting-resources/src/components/OfficeSettings.svelte b/plugins/setting-resources/src/components/OfficeSettings.svelte index b43eeeca9b..b9d721784d 100644 --- a/plugins/setting-resources/src/components/OfficeSettings.svelte +++ b/plugins/setting-resources/src/components/OfficeSettings.svelte @@ -26,8 +26,8 @@ let existingOfficeSettings: OfficeSettings[] = [] const query = createQuery() - $: query.query((setting.class as any).OfficeSettings, {}, (set) => { - existingOfficeSettings = set as unknown as OfficeSettings[] + $: query.query(setting.class.OfficeSettings, {}, (set) => { + existingOfficeSettings = set as OfficeSettings[] if (existingOfficeSettings !== undefined && existingOfficeSettings.length > 0) { defaultStartWithTranscription = existingOfficeSettings[0].defaultStartWithTranscription ?? false defaultStartWithRecording = existingOfficeSettings[0].defaultStartWithRecording ?? false @@ -44,10 +44,10 @@ enabled: true } if (existingOfficeSettings.length === 0) { - await client.createDoc((setting.class as any).OfficeSettings, core.space.Workspace, newSettings) + await client.createDoc(setting.class.OfficeSettings, core.space.Workspace, newSettings) } else { await client.updateDoc( - (setting.class as any).OfficeSettings, + setting.class.OfficeSettings, core.space.Workspace, existingOfficeSettings[0]._id, newSettings @@ -64,10 +64,10 @@ enabled: true } if (existingOfficeSettings.length === 0) { - await client.createDoc((setting.class as any).OfficeSettings, core.space.Workspace, newSettings) + await client.createDoc(setting.class.OfficeSettings, core.space.Workspace, newSettings) } else { await client.updateDoc( - (setting.class as any).OfficeSettings, + setting.class.OfficeSettings, core.space.Workspace, existingOfficeSettings[0]._id, newSettings diff --git a/plugins/setting-resources/src/components/UserRoleSelect.svelte b/plugins/setting-resources/src/components/UserRoleSelect.svelte new file mode 100644 index 0000000000..7294db9da9 --- /dev/null +++ b/plugins/setting-resources/src/components/UserRoleSelect.svelte @@ -0,0 +1,51 @@ + + + + diff --git a/plugins/setting-resources/src/hasRoleCapabilityAsync.ts b/plugins/setting-resources/src/hasRoleCapabilityAsync.ts new file mode 100644 index 0000000000..7835649093 --- /dev/null +++ b/plugins/setting-resources/src/hasRoleCapabilityAsync.ts @@ -0,0 +1,43 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { InviteSettings, RoleCapabilityId, RoleCapabilitySettings } from '@hcengineering/setting' +import { getCurrentAccount } from '@hcengineering/core' +import { getClient } from '@hcengineering/presentation' +import setting, { RoleCapability } from '@hcengineering/setting' +import { hasRoleCapability } from './roleCapability' + +/** + * Returns whether the current account has the given role capability. + * Fetches InviteSettings and RoleCapabilitySettings from the client. + * @public + */ +export async function hasRoleCapabilityAsync (capabilityId: RoleCapabilityId | string): Promise { + const client = getClient() + const [inviteSettings, roleCapabilitySettings] = await Promise.all([ + client.findAll(setting.class.InviteSettings, {}, { limit: 1 }), + client.findAll(setting.class.RoleCapabilitySettings, {}, { limit: 1 }) + ]) + const firstInvite = inviteSettings[0] as InviteSettings | undefined + const firstRoleCap = roleCapabilitySettings[0] as RoleCapabilitySettings | undefined + const inviteLinkGeneratorRoles = + capabilityId === RoleCapability.GenerateInviteLink && + firstInvite?.inviteLinkGeneratorRoles != null && + firstInvite.inviteLinkGeneratorRoles.length > 0 + ? firstInvite.inviteLinkGeneratorRoles + : undefined + const roleByCapability = firstRoleCap?.roleByCapability + const account = getCurrentAccount() + return hasRoleCapability(account, capabilityId, roleByCapability, inviteLinkGeneratorRoles) +} diff --git a/plugins/setting-resources/src/index.ts b/plugins/setting-resources/src/index.ts index 3f9a69386a..5248ce157a 100644 --- a/plugins/setting-resources/src/index.ts +++ b/plugins/setting-resources/src/index.ts @@ -36,6 +36,7 @@ import ClassAttributes from './components/ClassAttributes.svelte' import ClassAttributesList from './components/ClassAttributesList.svelte' import Configure from './components/Configure.svelte' import InviteSetting from './components/InviteSetting.svelte' +import UserRoleSelect from './components/UserRoleSelect.svelte' import PermissionPresenter from './components/presenters/PermissionPresenter.svelte' import AttributePermissionPresenter from './components/presenters/AttributePermissionPresenter.svelte' import ClassPermissionPresenter from './components/presenters/ClassPermissionPresenter.svelte' @@ -76,8 +77,11 @@ import IntegrationStateRow from './components/integrations/IntegrationStateRow.s import EmployeeRefEditor from './components/typeEditors/EmployeeRefEditor.svelte' import setting from './plugin' import { filterDescendants, getOwnerFirstName, getOwnerLastName, getOwnerPosition, getValue } from './utils' +import { hasRoleCapabilityAsync } from './hasRoleCapabilityAsync' export * from './store' +export { hasRoleCapability, getRolesForCapability } from './roleCapability' +export { hasRoleCapabilityAsync } from './hasRoleCapabilityAsync' export { ClassAttributes, ClassAttributesList, @@ -161,12 +165,14 @@ export default async (): Promise => ({ OfficeSettings, AddSocialId, AddEmailSocialId, - EmployeeRefEditor + EmployeeRefEditor, + UserRoleSelect }, actionImpl: { DeleteMixin }, function: { + HasRoleCapability: hasRoleCapabilityAsync, GetOwnerFirstName: getOwnerFirstName, GetOwnerLastName: getOwnerLastName, GetOwnerPosition: getOwnerPosition, diff --git a/plugins/setting-resources/src/roleCapability.ts b/plugins/setting-resources/src/roleCapability.ts new file mode 100644 index 0000000000..0157f21640 --- /dev/null +++ b/plugins/setting-resources/src/roleCapability.ts @@ -0,0 +1,60 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Account } from '@hcengineering/core' +import { AccountRole, hasAccountRole } from '@hcengineering/core' +import { RoleCapability, type RoleCapabilityId } from '@hcengineering/setting' + +/** Default roles that have each capability when no RoleCapabilitySettings is set */ +const DEFAULT_ROLES_BY_CAPABILITY: Record = { + [RoleCapability.GenerateInviteLink]: [AccountRole.User, AccountRole.Maintainer, AccountRole.Owner], + [RoleCapability.ManageInviteSettings]: [AccountRole.Maintainer, AccountRole.Owner] +} + +/** + * Returns whether the given account has the specified capability. + * Uses roleByCapability if provided; otherwise for GenerateInviteLink falls back to inviteLinkGeneratorRoles; otherwise uses built-in defaults. + * @public + */ +export function hasRoleCapability ( + account: Account, + capabilityId: RoleCapabilityId | string, + roleByCapability: Record | undefined | null, + inviteLinkGeneratorRoles?: AccountRole[] | undefined | null +): boolean { + const roles = getRolesForCapability(capabilityId, roleByCapability, inviteLinkGeneratorRoles) + return roles.some((role) => hasAccountRole(account, role)) +} + +/** + * Returns the list of AccountRoles that have the given capability (for the current config/fallbacks). + * @public + */ +export function getRolesForCapability ( + capabilityId: RoleCapabilityId | string, + roleByCapability: Record | undefined | null, + inviteLinkGeneratorRoles?: AccountRole[] | undefined | null +): AccountRole[] { + if (roleByCapability?.[capabilityId] != null && roleByCapability[capabilityId].length > 0) { + return roleByCapability[capabilityId] + } + if ( + capabilityId === RoleCapability.GenerateInviteLink && + inviteLinkGeneratorRoles != null && + inviteLinkGeneratorRoles.length > 0 + ) { + return inviteLinkGeneratorRoles + } + return DEFAULT_ROLES_BY_CAPABILITY[capabilityId] ?? [] +} diff --git a/plugins/setting/src/index.ts b/plugins/setting/src/index.ts index 77435db6b1..ce3af0b930 100644 --- a/plugins/setting/src/index.ts +++ b/plugins/setting/src/index.ts @@ -123,6 +123,33 @@ export interface InviteSettings extends Configuration { expirationTime: number emailMask: string limit: number + defaultInviteRole: AccountRole + inviteLinkGeneratorRoles: AccountRole[] +} + +/** + * Stable capability IDs for role-based permissions. + * Used with RoleCapabilitySettings to define which AccountRoles can perform which actions. + * @public + */ +export const RoleCapability = { + GenerateInviteLink: 'setting:capability:GenerateInviteLink', + ManageInviteSettings: 'setting:capability:ManageInviteSettings' +} as const + +/** + * @public + */ +export type RoleCapabilityId = (typeof RoleCapability)[keyof typeof RoleCapability] + +/** + * Workspace-level config: which AccountRoles have which capabilities. + * Maps capability ID -> list of roles that have it (account has capability if hasAccountRole(account, role) for any role in the list). + * @public + */ +export interface RoleCapabilitySettings extends Configuration { + /** capabilityId -> roles that are allowed (e.g. [User, Maintainer, Owner]) */ + roleByCapability: Record } /** @@ -164,6 +191,7 @@ export default plugin(settingId, { General: '' as Ref, Owners: '' as Ref, InviteSettings: '' as Ref, + RoleCapabilitySettings: '' as Ref, WorkspaceSetting: '' as Ref, ManageSpaces: '' as Ref, Spaces: '' as Ref, @@ -185,6 +213,7 @@ export default plugin(settingId, { Integration: '' as Ref>, IntegrationType: '' as Ref>, InviteSettings: '' as Ref>, + RoleCapabilitySettings: '' as Ref>, OfficeSettings: '' as Ref>, WorkspaceSetting: '' as Ref> }, @@ -214,7 +243,8 @@ export default plugin(settingId, { EditRelation: '' as AnyComponent, Mailboxes: '' as AnyComponent, AddEmailSocialId: '' as AnyComponent, - OfficeSettings: '' as AnyComponent + OfficeSettings: '' as AnyComponent, + UserRoleSelect: '' as AnyComponent }, string: { Settings: '' as IntlString, @@ -246,6 +276,9 @@ export default plugin(settingId, { Owners: '' as IntlString, Configure: '' as IntlString, InviteSettings: '' as IntlString, + RoleCapabilitySettings: '' as IntlString, + DefaultInviteRoleForJoin: '' as IntlString, + InviteLinkGeneratorRoles: '' as IntlString, General: '' as IntlString, Properties: '' as IntlString, TaskTypes: '' as IntlString, @@ -336,5 +369,8 @@ export default plugin(settingId, { }, metadata: { BackupUrl: '' as Metadata + }, + function: { + HasRoleCapability: '' as Resource<(capabilityId: RoleCapabilityId | string) => Promise> } }) diff --git a/plugins/workbench-resources/src/components/AccountPopup.svelte b/plugins/workbench-resources/src/components/AccountPopup.svelte index 0f240673b8..eca320f5dd 100644 --- a/plugins/workbench-resources/src/components/AccountPopup.svelte +++ b/plugins/workbench-resources/src/components/AccountPopup.svelte @@ -24,7 +24,7 @@ hasResource, isDisabled } from '@hcengineering/presentation' - import setting, { settingId, SettingsCategory } from '@hcengineering/setting' + import setting, { RoleCapability, settingId, SettingsCategory } from '@hcengineering/setting' import { Action, closePopup, @@ -42,9 +42,11 @@ import HelpAndSupport from './HelpAndSupport.svelte' import { Analytics } from '@hcengineering/analytics' import { allowGuestSignUpStore } from '@hcengineering/view-resources' - import { getMetadata } from '@hcengineering/platform' + import { getMetadata, getResource } from '@hcengineering/platform' + import { onMount } from 'svelte' let items: SettingsCategory[] = [] + let canGenerateInviteLink = false const account = getCurrentAccount() const settingsQuery = createQuery() @@ -57,6 +59,16 @@ { sort: { order: 1 } } ) + onMount(() => { + void getResource(setting.function.HasRoleCapability).then((checkCapability) => { + if (checkCapability != null) { + void checkCapability(RoleCapability.GenerateInviteLink).then((v: boolean) => { + canGenerateInviteLink = v + }) + } + }) + }) + $: person = $myEmployeeStore function selectCategory (sp?: SettingsCategory): void { @@ -131,7 +143,7 @@ }) } actions.push(...getMenu(items, ['main'])) - if (hasAccountRole(account, AccountRole.User) && !isDisabled('invites')) { + if (hasAccountRole(account, AccountRole.User) && !isDisabled('invites') && canGenerateInviteLink) { actions.push({ icon: setting.icon.InviteWorkspace, label: setting.string.InviteWorkspace,