Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@
"WorkspaceName": "Název pracovního prostoru",
"Workspace": "Pracovní prostor",
"OwnerOrMaintainerRequired": "Musíte být vlastníkem nebo správcem pracovního prostoru",
"LastOwnerLeaveTitle": "Nelze opustit pracovní prostor",
"LastOwnerLeaveMessage": "Jste jediným vlastníkem tohoto pracovního prostoru. Abyste jej mohli opustit, nejprve předejte oprávnění vlastníka jinému členovi. Pokud tento pracovní prostor už nikdo nepotřebuje, zvažte jeho smazání.",
"Backup": "Záloha",
"BackupLast": "Poslední záloha",
"BackupTotalSnapshots": "Celkový počet snímků",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
"WorkspaceName": "Arbeitsbereich-Name",
"Workspace": "Arbeitsbereich",
"OwnerOrMaintainerRequired": "Sie müssen Eigentümer oder Betreuer des Arbeitsbereichs sein",
"LastOwnerLeaveTitle": "Arbeitsbereich kann nicht verlassen werden",
"LastOwnerLeaveMessage": "Sie sind der einzige Eigentümer dieses Arbeitsbereichs. Um ihn zu verlassen, übertragen Sie die Eigentümerrechte zunächst auf ein anderes Mitglied. Wenn niemand diesen Arbeitsbereich mehr benötigt, sollten Sie ihn stattdessen löschen.",
"Backup": "Sicherung",
"BackupLast": "Letzte Sicherung",
"BackupTotalSnapshots": "Gesamtanzahl der Schnappschüsse",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
"WorkspaceName": "Workspace name",
"Workspace": "Workspace",
"OwnerOrMaintainerRequired": "You need to be a workspace Owner or Maintainer",
"LastOwnerLeaveTitle": "Cannot leave workspace",
"LastOwnerLeaveMessage": "You are the only owner of this workspace. To leave, first grant owner permissions to another member. If nobody needs this workspace anymore, consider deleting this workspace instead.",
"Backup": "Backup",
"BackupLast": "Last backup",
"BackupTotalSnapshots": "Total Snapshots",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
"WorkspaceName": "Nombre del espacio de trabajo",
"Workspace": "Espacio de trabajo",
"OwnerOrMaintainerRequired": "Necesitas ser Propietario o Mantenedor del espacio de trabajo",
"LastOwnerLeaveTitle": "No puedes abandonar el espacio de trabajo",
"LastOwnerLeaveMessage": "Eres el único propietario de este espacio de trabajo. Para salir, primero asigna permisos de propietario a otro miembro. Si nadie más necesita este espacio de trabajo, considera eliminarlo.",
"Backup": "Copia de seguridad",
"BackupLast": "Última copia de seguridad",
"BackupTotalSnapshots": "Total instantáneas",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
"WorkspaceName": "Nom de l'espace de travail",
"Workspace": "Espace de travail",
"OwnerOrMaintainerRequired": "Vous devez être propriétaire ou responsable d'un espace de travail",
"LastOwnerLeaveTitle": "Impossible de quitter l'espace de travail",
"LastOwnerLeaveMessage": "Vous êtes le seul propriétaire de cet espace de travail. Pour le quitter, attribuez d'abord les droits de propriétaire à un autre membre. Si personne n'a plus besoin de cet espace de travail, envisagez de le supprimer.",
"Backup": "Sauvegarde",
"BackupLast": "Dernière sauvegarde",
"BackupTotalSnapshots": "Total des instantanés",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
"WorkspaceName": "Nome spazio di lavoro",
"Workspace": "Spazio di lavoro",
"OwnerOrMaintainerRequired": "Devi essere un proprietario o un manutentore dello spazio di lavoro",
"LastOwnerLeaveTitle": "Impossibile uscire dallo spazio di lavoro",
"LastOwnerLeaveMessage": "Sei l'unico proprietario di questo spazio di lavoro. Per uscire, assegna prima i permessi di proprietario a un altro membro. Se nessuno ha più bisogno di questo spazio di lavoro, valuta di eliminarlo.",
"Backup": "Backup",
"BackupLast": "Ultimo backup",
"BackupTotalSnapshots": "Totale istantanee",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
"WorkspaceName": "ワークスペース名",
"Workspace": "ワークスペース",
"OwnerOrMaintainerRequired": "ワークスペースの所有者またはメンテナーである必要があります",
"LastOwnerLeaveTitle": "ワークスペースから退出できません",
"LastOwnerLeaveMessage": "あなたはこのワークスペースの唯一の所有者です。退出するには、先に別のメンバーに所有者権限を付与してください。このワークスペースが誰にも必要ない場合は、削除することも検討してください。",
"Backup": "バックアップ",
"BackupLast": "最後のバックアップ",
"BackupTotalSnapshots": "合計スナップショット",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
"WorkspaceName": "Nome do espaço de trabalho",
"Workspace": "Espaço de trabalho",
"OwnerOrMaintainerRequired": "Precisa ser proprietário ou mantenedor do espaço de trabalho",
"LastOwnerLeaveTitle": "Não é possível sair do espaço de trabalho",
"LastOwnerLeaveMessage": "Você é o único proprietário deste espaço de trabalho. Para sair, primeiro conceda permissões de proprietário a outro membro. Se ninguém mais precisar deste espaço de trabalho, considere excluí-lo.",
"Backup": "Backup",
"BackupLast": "Último backup",
"BackupTotalSnapshots": "Total de Snapshots",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
"WorkspaceName": "Nome do espaço de trabalho",
"Workspace": "Espaço de trabalho",
"OwnerOrMaintainerRequired": "Precisa de ser proprietário ou mantenedor do espaço de trabalho",
"LastOwnerLeaveTitle": "Não é possível sair do espaço de trabalho",
"LastOwnerLeaveMessage": "É o único proprietário deste espaço de trabalho. Para sair, primeiro conceda permissões de proprietário a outro membro. Se ninguém mais precisar deste espaço de trabalho, considere eliminar o espaço de trabalho.",
"Backup": "Backup",
"BackupLast": "Último backup",
"BackupTotalSnapshots": "Total de Snapshots",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@
"WorkspaceName": "Название рабочего пространства",
"Workspace": "Рабочее пространство",
"OwnerOrMaintainerRequired": "Вы должны иметь роль Владельца/Сопровождаемого",
"LastOwnerLeaveTitle": "Невозможно выйти из рабочего пространства",
"LastOwnerLeaveMessage": "Вы являетесь единственным владельцем этого рабочего пространства. Чтобы выйти, сначала назначьте владельцем другого участника. Если это рабочее пространство больше никому не нужно, рассмотрите возможность его удаления.",
"Backup": "Резервное копирование",
"BackupLast": "Последнее резервное копирование",
"BackupTotalSnapshots": "Всего снимков",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
"WorkspaceName": "Çalışma alanı adı",
"Workspace": "Çalışma alanı",
"OwnerOrMaintainerRequired": "Çalışma alanı Sahibi veya Bakımcısı olmanız gerekiyor",
"LastOwnerLeaveTitle": "Çalışma alanından ayrılamazsınız",
"LastOwnerLeaveMessage": "Bu çalışma alanının tek sahibisiniz. Ayrılmak için önce başka bir üyeye sahiplik yetkisi verin. Bu çalışma alanına artık kimsenin ihtiyacı yoksa, silmeyi de düşünebilirsiniz.",
"Backup": "Yedek",
"BackupLast": "Son yedekleme",
"BackupTotalSnapshots": "Toplam Anlık Görüntü",
Expand Down
2 changes: 2 additions & 0 deletions plugins/setting-assets/lang/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
"WorkspaceName": "工作区名称",
"Workspace": "工作区",
"OwnerOrMaintainerRequired": "您需要成为工作空间所有者或维护者",
"LastOwnerLeaveTitle": "无法离开工作区",
"LastOwnerLeaveMessage": "您是此工作区的唯一所有者。要离开,请先将所有者权限授予其他成员。如果不再有人需要此工作区,请考虑删除该工作区。",
"Backup": "备份",
"BackupLast": "上次备份",
"BackupTotalSnapshots": "快照总数",
Expand Down
2 changes: 1 addition & 1 deletion plugins/setting-resources/src/components/Owners.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
{@const personUuid = employee.personUuid ?? undefined}
{@const role = personUuid !== undefined ? workspaceMembers[personUuid] : undefined}
{#if personUuid !== undefined && role !== undefined && employee.name?.includes(search)}
<div class="flex-row-center p-2 flex-no-shrink">
<div class="flex-row-center p-2 flex-no-shrink" data-id="owners-member-row">
<div class="p-1 min-w-80">
<EmployeePresenter value={employee} disabled={false} />
</div>
Expand Down
38 changes: 27 additions & 11 deletions plugins/setting-resources/src/components/Profile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<script lang="ts">
import contact, { combineName, getFirstName, getLastName } from '@hcengineering/contact'
import { ChannelsEditor, EditableAvatar, myEmployeeStore } from '@hcengineering/contact-resources'
import { getCurrentAccount, SocialIdType } from '@hcengineering/core'
import { AccountRole, getCurrentAccount, SocialIdType } from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login'
import { getResource } from '@hcengineering/platform'
import platform, { getResource, PlatformError } from '@hcengineering/platform'
import { AttributeEditor, createQuery, getClient, hasResource, MessageBox } from '@hcengineering/presentation'
import {
Breadcrumb,
Expand Down Expand Up @@ -64,7 +64,7 @@
}

let avatarEditor: EditableAvatar
async function onAvatarDone (e: any): Promise<void> {
async function onAvatarDone (): Promise<void> {
if ($myEmployeeStore === undefined) return

if ($myEmployeeStore.avatar != null) {
Expand All @@ -82,14 +82,30 @@
message: setting.string.LeaveDescr,
action: async () => {
const leaveWorkspace = await getResource(login.function.LeaveWorkspace)
const loginInfo = await leaveWorkspace(account.uuid)

if (loginInfo?.token != null) {
await logIn(loginInfo)
navigate({ path: [loginId, 'selectWorkspace'] })
} else {
await logOut()
navigate({ path: [loginId] })
try {
const loginInfo = await leaveWorkspace(account.uuid)

if (loginInfo?.token != null) {
await logIn(loginInfo)
navigate({ path: [loginId, 'selectWorkspace'] })
} else {
await logOut()
navigate({ path: [loginId] })
}
} catch (err: any) {
if (
err instanceof PlatformError &&
err.status?.code === platform.status.Forbidden &&
account.role === AccountRole.Owner
) {
showPopup(MessageBox, {
label: setting.string.LastOwnerLeaveTitle,
message: setting.string.LastOwnerLeaveMessage,
canSubmit: false
})
} else {
throw err
}
}
}
})
Expand Down
4 changes: 3 additions & 1 deletion plugins/setting-resources/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ export default mergeIds(settingId, setting, {
PasswordAgingRule: '' as IntlString,
PasswordAgingRuleDescription: '' as IntlString,
ShowInTitle: '' as IntlString,
SpaceMembersOnly: '' as IntlString
SpaceMembersOnly: '' as IntlString,
LastOwnerLeaveTitle: '' as IntlString,
LastOwnerLeaveMessage: '' as IntlString
}
})
81 changes: 80 additions & 1 deletion server/account/src/__tests__/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ import {
getPerson,
getSocialIds,
createAccessLink,
getSubscriptions
getSubscriptions,
leaveWorkspace
} from '../operations'
import { accountPlugin } from '../plugin'

Expand Down Expand Up @@ -105,6 +106,8 @@ describe('account operations', () => {
update: jest.fn()
},
getWorkspaceRole: jest.fn(),
getWorkspaceMembers: jest.fn(),
unassignWorkspace: jest.fn(),
person: {
findOne: jest.fn()
},
Expand Down Expand Up @@ -1553,6 +1556,82 @@ describe('account operations', () => {
})
})

describe('workspace membership operations', () => {
const ownerAccount = 'owner-account-uuid' as AccountUuid
const maintainerAccount = 'maintainer-account-uuid' as AccountUuid
const otherOwnerAccount = 'other-owner-account-uuid' as AccountUuid

beforeEach(() => {
jest.clearAllMocks()
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
account: maintainerAccount,
workspace: mockWorkspace.uuid,
extra: {}
})
})

describe('leaveWorkspace', () => {
test('should forbid maintainer from removing owner from workspace', async () => {
;(mockDb.getWorkspaceRole as jest.Mock).mockImplementation(async (accountId: AccountUuid) => {
if (accountId === maintainerAccount) return AccountRole.Maintainer
if (accountId === ownerAccount) return AccountRole.Owner
return null
})

await expect(
leaveWorkspace(mockCtx, mockDb, mockBranding, mockToken, { account: ownerAccount })
).rejects.toThrow(new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})))

expect(mockDb.unassignWorkspace).not.toHaveBeenCalled()
})

test('should forbid last owner from leaving workspace', async () => {
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
account: ownerAccount,
workspace: mockWorkspace.uuid,
extra: {}
})
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.Owner)
;(mockDb.getWorkspaceMembers as jest.Mock).mockResolvedValue([
{ accountUuid: ownerAccount, role: AccountRole.Owner }
])

await expect(
leaveWorkspace(mockCtx, mockDb, mockBranding, mockToken, { account: ownerAccount })
).rejects.toThrow(new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})))

expect(mockDb.unassignWorkspace).not.toHaveBeenCalled()
})

test('should allow owner to leave when there are multiple owners', async () => {
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
account: ownerAccount,
workspace: mockWorkspace.uuid,
extra: {}
})
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.Owner)
;(mockDb.getWorkspaceMembers as jest.Mock).mockResolvedValue([
{ accountUuid: ownerAccount, role: AccountRole.Owner },
{ accountUuid: otherOwnerAccount, role: AccountRole.Owner }
])
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({
uuid: ownerAccount,
firstName: 'Owner',
lastName: 'User'
})

const result = await leaveWorkspace(mockCtx, mockDb, mockBranding, mockToken, { account: ownerAccount })

expect(mockDb.unassignWorkspace).toHaveBeenCalledWith(ownerAccount, mockWorkspace.uuid)
expect(result).toEqual({
account: ownerAccount,
name: 'Owner User',
token: expect.any(String)
})
})
})
})

describe('registration operations', () => {
const mockCtx = {
error: jest.fn(),
Expand Down
25 changes: 25 additions & 0 deletions server/account/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,7 @@ export async function leaveWorkspace (
}

const initiatorRole = await db.getWorkspaceRole(account, workspace)
const targetRole = await db.getWorkspaceRole(targetAccount, workspace)

if (account !== targetAccount) {
if (initiatorRole == null || getRolePower(initiatorRole) < getRolePower(AccountRole.Maintainer)) {
Expand All @@ -1489,6 +1490,30 @@ export async function leaveWorkspace (
})
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}

if (targetRole === AccountRole.Owner && initiatorRole === AccountRole.Maintainer) {
ctx.warn('Maintainer cannot remove owner from workspace', {
account,
targetAccount,
workspace,
initiatorRole,
targetRole
})
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
}

if (account === targetAccount && initiatorRole === AccountRole.Owner) {
const members = await db.getWorkspaceMembers(workspace)
const owners = members.filter((m) => m.role === AccountRole.Owner)

if (owners.length === 1) {
ctx.warn('Owner cannot remove themselves as the last owner of the workspace', {
account,
workspace
})
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
}

await db.unassignWorkspace(targetAccount, workspace)
Expand Down
Loading
Loading