diff --git a/server/mergin/sync/models.py b/server/mergin/sync/models.py index de81676a..cccd7b43 100644 --- a/server/mergin/sync/models.py +++ b/server/mergin/sync/models.py @@ -359,7 +359,8 @@ class ProjectAccessDetail: role: str username: str name: Optional[str] - project_permission: str + workspace_role: str + project_role: Optional[ProjectRole] type: str diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index 4160ed07..0116cd65 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -551,8 +551,7 @@ components: - id - type - email - - project_permission - - role + - workspace_role properties: id: description: User/Invitation (uu)id @@ -569,16 +568,9 @@ components: type: string format: email example: john.doe@example.com - role: + workspace_role: description: Workspace role - type: string - enum: - - owner - - admin - - writer - - editor - - reader - - guest + $ref: "#/components/schemas/WorkspaceRole" username: description: Present only for type `member` type: string @@ -587,13 +579,13 @@ components: description: Present only for type `member` type: string example: John Doe - project_permission: - type: string - enum: - - owner - - writer - - editor - - reader + role: + description: Project role defined as combination of project and workspace roles + $ref: "#/components/schemas/ProjectRole" + project_role: + nullable: true + description: Project role defined in database, not calculated version + $ref: "#/components/schemas/ProjectRole" invitation: description: Present only for type `invitation` type: object @@ -658,3 +650,19 @@ components: items: type: integer example: [1] + WorkspaceRole: + type: string + enum: + - owner + - admin + - writer + - editor + - reader + - guest + ProjectRole: + type: string + enum: + - owner + - writer + - editor + - reader diff --git a/server/mergin/sync/private_api_controller.py b/server/mergin/sync/private_api_controller.py index 616ac69d..31d3e2af 100644 --- a/server/mergin/sync/private_api_controller.py +++ b/server/mergin/sync/private_api_controller.py @@ -26,7 +26,6 @@ AdminProjectSchema, ProjectAccessSchema, ProjectAccessDetailSchema, - ProjectVersionListSchema, ) from .permissions import ( require_project_by_uuid, diff --git a/server/mergin/sync/schemas.py b/server/mergin/sync/schemas.py index c782c5ca..c282b56c 100644 --- a/server/mergin/sync/schemas.py +++ b/server/mergin/sync/schemas.py @@ -361,7 +361,8 @@ class ProjectAccessDetailSchema(Schema): role = fields.String() username = fields.String() name = fields.String() - project_permission = fields.String() + workspace_role = fields.String() + project_role = fields.String() type = fields.String() invitation = fields.Nested(ProjectInvitationAccessSchema()) diff --git a/server/mergin/sync/workspace.py b/server/mergin/sync/workspace.py index 8cc0a27a..25b8e6fe 100644 --- a/server/mergin/sync/workspace.py +++ b/server/mergin/sync/workspace.py @@ -323,17 +323,19 @@ def project_access(self, project: Project) -> List[ProjectAccessDetail]: direct_members_ids = [u.user_id for u in project.project_users] users = User.query.filter(User.active.is_(True)).order_by(User.email) - direct_members = users.filter(User.id.in_(direct_members_ids)).all() + direct_members: list[User] = users.filter(User.id.in_(direct_members_ids)).all() for dm in direct_members: - project_role = ProjectPermissions.get_user_project_role(project, dm) + project_permission = ProjectPermissions.get_user_project_role(project, dm) + project_role = project.get_role(dm.id) member = ProjectAccessDetail( id=dm.id, username=dm.username, - role=ws.get_user_role(dm).value, + workspace_role=ws.get_user_role(dm).value, name=dm.profile.name(), email=dm.email, - project_permission=project_role and project_role.value, + role=project_permission and project_permission.value, + project_role=project_role.value if project_role else None, type="member", ) result.append(member) @@ -345,8 +347,9 @@ def project_access(self, project: Project) -> List[ProjectAccessDetail]: username=gm.username, name=gm.profile.name(), email=gm.email, + workspace_role=global_role, role=global_role, - project_permission=global_role, + project_role=None, type="member", ) result.append(member) diff --git a/server/mergin/tests/test_private_project_api.py b/server/mergin/tests/test_private_project_api.py index c8aabd9e..f475255f 100644 --- a/server/mergin/tests/test_private_project_api.py +++ b/server/mergin/tests/test_private_project_api.py @@ -490,7 +490,7 @@ def test_get_project_access(client): resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 1 - assert resp.json[0]["project_permission"] == "owner" + assert resp.json[0]["role"] == "owner" project.set_role(users[0].id, ProjectRole.OWNER) project.set_role(users[1].id, ProjectRole.WRITER) project.set_role(users[2].id, ProjectRole.READER) @@ -498,9 +498,9 @@ def test_get_project_access(client): resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 4 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 2 - assert sum(map(lambda x: int(x["project_permission"] == "writer"), resp.json)) == 1 - assert sum(map(lambda x: int(x["project_permission"] == "reader"), resp.json)) == 1 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 2 + assert sum(map(lambda x: int(x["role"] == "writer"), resp.json)) == 1 + assert sum(map(lambda x: int(x["role"] == "reader"), resp.json)) == 1 # user3 does not have access to the project assert not any(users[3].email == access["email"] for access in resp.json) assert any(users[2].email == access["email"] for access in resp.json) @@ -508,27 +508,27 @@ def test_get_project_access(client): resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 6 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 2 - assert sum(map(lambda x: int(x["project_permission"] == "writer"), resp.json)) == 1 - assert sum(map(lambda x: int(x["project_permission"] == "reader"), resp.json)) == 3 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 2 + assert sum(map(lambda x: int(x["role"] == "writer"), resp.json)) == 1 + assert sum(map(lambda x: int(x["role"] == "reader"), resp.json)) == 3 Configuration.GLOBAL_WRITE = True resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 6 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 2 - assert sum(map(lambda x: int(x["project_permission"] == "writer"), resp.json)) == 4 - assert sum(map(lambda x: int(x["project_permission"] == "reader"), resp.json)) == 0 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 2 + assert sum(map(lambda x: int(x["role"] == "writer"), resp.json)) == 4 + assert sum(map(lambda x: int(x["role"] == "reader"), resp.json)) == 0 Configuration.GLOBAL_ADMIN = True resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 6 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 6 - assert sum(map(lambda x: int(x["project_permission"] == "writer"), resp.json)) == 0 - assert sum(map(lambda x: int(x["project_permission"] == "reader"), resp.json)) == 0 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 6 + assert sum(map(lambda x: int(x["role"] == "writer"), resp.json)) == 0 + assert sum(map(lambda x: int(x["role"] == "reader"), resp.json)) == 0 # pretend a user was deleted to test that api can handle it users[3].inactivate() users[3].anonymize() resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 5 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 5 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 5 diff --git a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue index 86b9cd2b..9032e85b 100644 --- a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue +++ b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue @@ -215,7 +215,8 @@ const changeStatusDialog = () => { await adminStore.updateUser({ username: user.value.username, data: { - active: !user.value.active + active: !user.value.active, + is_admin: user.value.is_admin } }) } diff --git a/web-app/packages/lib/src/common/permission_utils.ts b/web-app/packages/lib/src/common/permission_utils.ts index e810b2db..97179ffd 100644 --- a/web-app/packages/lib/src/common/permission_utils.ts +++ b/web-app/packages/lib/src/common/permission_utils.ts @@ -16,7 +16,6 @@ export enum WorkspaceRole { } export enum ProjectRole { - none, reader, editor, writer, @@ -37,9 +36,10 @@ export type WorkspaceRoleName = | 'admin' | 'owner' -export type ProjectRoleName = - | Extract - | 'none' +export type ProjectRoleName = Extract< + WorkspaceRoleName, + 'reader' | 'editor' | 'writer' | 'owner' +> export type ProjectPermissionName = 'owner' | 'write' | 'edit' | 'read' @@ -63,7 +63,6 @@ export const USER_ROLE_BY_NAME: Record = { } export const PROJECT_ROLE_NAME_BY_ROLE: Record = { - [ProjectRole.none]: 'none', [ProjectRole.reader]: 'reader', [ProjectRole.editor]: 'editor', [ProjectRole.writer]: 'writer', @@ -71,7 +70,6 @@ export const PROJECT_ROLE_NAME_BY_ROLE: Record = { } export const PROJECT_ROLE_BY_NAME: Record = { - none: ProjectRole.none, reader: ProjectRole.reader, editor: ProjectRole.editor, writer: ProjectRole.writer, @@ -191,8 +189,7 @@ export function getProjectAccessKeyByRoleName( owner: 'ownersnames', writer: 'writersnames', editor: 'editorsnames', - reader: 'readersnames', - none: undefined + reader: 'readersnames' } return mapper[roleName] } @@ -200,12 +197,11 @@ export function getProjectAccessKeyByRoleName( export function getProjectPermissionByRoleName( roleName: ProjectRoleName ): ProjectPermissionName { - const mapper: Record = { + const mapper: Record = { owner: 'owner', writer: 'write', editor: 'edit', - reader: 'read', - none: undefined + reader: 'read' } return mapper[roleName] } diff --git a/web-app/packages/lib/src/modules/project/components/AccessRequestTableTemplate.vue b/web-app/packages/lib/src/modules/project/components/AccessRequestTableTemplate.vue index 91eb6f22..e0e813c2 100644 --- a/web-app/packages/lib/src/modules/project/components/AccessRequestTableTemplate.vue +++ b/web-app/packages/lib/src/modules/project/components/AccessRequestTableTemplate.vue @@ -187,7 +187,7 @@ export default defineComponent({ await this.acceptProjectAccessRequest({ data, itemId: request.id, - namespace: this.namespace + workspace: this.namespace }) await this.updatePaginationOrFetch() } catch (err) { @@ -200,7 +200,7 @@ export default defineComponent({ async cancelRequest(request) { await this.cancelProjectAccessRequest({ itemId: request.id, - namespace: this.namespace + workspace: this.namespace }) await this.updatePaginationOrFetch() }, diff --git a/web-app/packages/lib/src/modules/project/components/ProjectAccessRequests.vue b/web-app/packages/lib/src/modules/project/components/ProjectAccessRequests.vue index 80d0eb3e..bc7af479 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectAccessRequests.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectAccessRequests.vue @@ -158,7 +158,7 @@ export default defineComponent({ await this.acceptProjectAccessRequest({ data, itemId: request.id, - namespace: this.project.namespace + workspace: this.project.namespace }) await this.updatePaginationOrFetch() } catch (err) { @@ -173,7 +173,7 @@ export default defineComponent({ async cancelRequest(request) { await this.cancelProjectAccessRequest({ itemId: request.id, - namespace: this.project.namespace + workspace: this.project.namespace }) await this.updatePaginationOrFetch() }, diff --git a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue index f96f9e6d..6f066f2b 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue @@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial