Skip to content

#202 - Step 2, 3, 4: Implement Add/Remove/Edit permissions#2895

Open
SebinSong wants to merge 92 commits intomasterfrom
sebin/task/#202-roles-and-permission-step2
Open

#202 - Step 2, 3, 4: Implement Add/Remove/Edit permissions#2895
SebinSong wants to merge 92 commits intomasterfrom
sebin/task/#202-roles-and-permission-step2

Conversation

@SebinSong
Copy link
Collaborator

@SebinSong SebinSong commented Aug 18, 2025

Work on step 2 items in this TODO list

This PR is about building this 'Add Permissions' modal and implement various features & updates associated with it.



Permission details set up via this modal will be displayed in the Roles and permissions section in the Group Settings page below.
add permissions button in orange box is only exposed to members who have delegate-permissions permission granted, which are admin role and moderator(delegator) role.

Note: Implementing hide/enable other functionalities depending on granted permissions will be worked in Step 4 here, FYI

image

For those who don't have view-permissions permission granted, This table will only show their own permission details like below:

image

@taoeffect
These two UIs below I built in this PR have no Figma reference actually. (built by combining existing ingredients in the app) Once the project has a designer back, they could be reviewed again and improved. Hope it looks good to you anyways.


EDIT: As part of Step 3 I built below two modals and implement functionalities associated with them

['Remove role' modal]

image

['Edit permissions' modal]

image

These are accessed via below menu:

image
Open with Devin

…Modal.vue and implement some front-end logics
…d getters / change the contract logic for 'gi.contracts/group/updatePermissions' action
@SebinSong SebinSong self-assigned this Aug 18, 2025
@cypress
Copy link

cypress bot commented Aug 18, 2025

group-income    Run #5352

Run Properties:  status check passed Passed #5352  •  git commit 657f75822b ℹ️: Merge f412b422bd14a04e20b33c91feb47fbca9b7bde6 into c83b58d442e8100902883b830060...
Project group-income
Branch Review sebin/task/#202-roles-and-permission-step2
Run status status check passed Passed #5352
Run duration 14m 31s
Commit git commit 657f75822b ℹ️: Merge f412b422bd14a04e20b33c91feb47fbca9b7bde6 into c83b58d442e8100902883b830060...
Committer Sebin Song
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 10
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 117
View all changes introduced in this branch ↗︎

@SebinSong
Copy link
Collaborator Author

The PR is ready for review again.

@taoeffect
Copy link
Member

taoeffect commented Feb 20, 2026

/review_fast


AI review started.

@github-actions
Copy link

github-actions bot commented Feb 20, 2026

Advanced AI Review

  • Type: Agentic (opencode)
  • Model: glm-5
Click to expand review

I'll analyze the changes for bugs, security issues, and potential improvements. Let me first examine some key files to understand the patterns and context better.
Let me check more files to understand the patterns and find additional issues:
Let me check more files to complete the review:
Let me check a few more files to complete the review:
Based on my thorough review of the changes, here are the issues I've identified:


Issue 1: 🔴 Incorrect HTML nesting in RemoveRoleModal.vue

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/RemoveRoleModal.vue:13-22

The "Role" list item is incorrectly nested inside the "Member" list item, creating malformed HTML:

ul.c-details-list(v-if='data')
  li.c-list-item
    i18n.c-label(tag='span') Member:
    member-name.c-list-item-content.c-member-details-container(:memberID='data.memberID')

    li.c-list-item  // <-- INCORRECTLY NESTED inside the previous li
      i18n.c-label(tag='span') Role:

Suggested fix:

ul.c-details-list(v-if='data')
  li.c-list-item
    i18n.c-label(tag='span') Member:
    member-name.c-list-item-content.c-member-details-container(:memberID='data.memberID')

  li.c-list-item  // <-- Should be a sibling, not nested
    i18n.c-label(tag='span') Role:

Issue 2: 🔴 Missing validation: roleName required for UPSERT action in contract

  • Addressed
  • Dismissed

Location: frontend/model/contracts/group.js:1153-1166

The validation for updatePermissions doesn't ensure roleName is provided when action is UPSERT. The objectMaybeOf validator makes all fields optional, so roleName could be undefined:

if (item.action === GROUP_PERMISSION_CHANGE_ACTIONS.UPSERT) {
  if (item.roleName === GROUP_ROLES.ADMIN) {  // if roleName is undefined, this is false
    throw new TypeError(L('Cannot assign admin role.'))
  }
  // ... no check that roleName is actually provided
}

This allows storing { name: undefined } in the role.

Suggested fix:

if (item.action === GROUP_PERMISSION_CHANGE_ACTIONS.UPSERT) {
  if (!item.roleName) {
    throw new TypeError(L('Role name is required.'))
  }
  if (item.roleName === GROUP_ROLES.ADMIN) {
    throw new TypeError(L('Cannot assign admin role.'))
  }
  // ... rest of validation
}

Issue 3: 🟡 Potential undefined array access in EditPermissionsModal

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/EditPermissionsModal.vue:114-116

permissionsToDisplay () {
  return this.config.permissionPresets[this.ephemeral.role]
}

If this.ephemeral.role is null or an empty string, this returns undefined, which could cause issues in the template iteration.

Suggested fix:

permissionsToDisplay () {
  return this.config.permissionPresets[this.ephemeral.role] || []
}

Issue 4: 🟡 Potential null reference when user has no role

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/RolesAndPermissions.vue:75-82

groupPermissionsToDisplay () {
  const myEntry = this.allGroupMemberRolesAndPermissions.find(entry => entry.memberID === this.ourIdentityContractId)
  // ...
  return this.canViewOtherMembersPermissions
    ? sortedEntries
    : [myEntry]  // myEntry could be undefined
}

If the current user doesn't have a role and canViewOtherMembersPermissions is false, this returns [undefined], which will cause issues when rendering.

Suggested fix:

return this.canViewOtherMembersPermissions
  ? sortedEntries
  : (myEntry ? [myEntry] : [])

Issue 5: 🟡 Missing null check in globalProfile call

  • Addressed
  • Dismissed

Location: frontend/views/components/GroupMembersDropdown.vue:98-100

allActiveMembers () {
  return Object.keys(this.profilesByGroup(this.groupID || this.currentGroupId))
    .map(memberId => this.globalProfile(memberId))
}

If globalProfile(memberId) returns null for any member, the dropdown options would include null values, causing issues when accessing option.contractID.

Suggested fix:

allActiveMembers () {
  return Object.keys(this.profilesByGroup(this.groupID || this.currentGroupId))
    .map(memberId => this.globalProfile(memberId))
    .filter(Boolean)
}

Issue 6: 🟡 Potential undefined permissions causing runtime error

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/AddPermissionsListItem.vue:133-144

updateRole (e) {
  const value = e.target.value
  this.ephemeral.selectedRole = value

  if (value === GROUP_ROLES.CUSTOM) {
    // ...
  } else {
    this.ephemeral.selectedPermissions = this.config.permissionPresets[value]  // could be undefined
  }

If value is an empty string (which shouldn't happen but could with keyboard navigation), this.config.permissionPresets[''] is undefined.

Then in AddPermissionsModal.vue:96-98:

enableUpdateBtn () {
  return this.ephemeral.roleEntries.every(entry => entry.permissions.length)  // throws if permissions is undefined
}

Suggested fix (AddPermissionsListItem.vue):

} else {
  this.ephemeral.selectedPermissions = this.config.permissionPresets[value] || []
}

Issue 7: 🟡 enableSubmitBtn allows submission with null role in EditPermissionsModal

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/EditPermissionsModal.vue:117-131

enableSubmitBtn () {
  const isCurrentRoleCustom = this.data.roleName === GROUP_ROLES.CUSTOM
  const { permissions, role } = this.ephemeral

  if (isCurrentRoleCustom) {
    return role !== GROUP_ROLES.CUSTOM || (
      permissions.length !== this.data.permissions.length ||
      permissions.some(p => !this.data.permissions.includes(p))
    )
  } else {
    return role === GROUP_ROLES.CUSTOM
      ? permissions.length > 0
      : role !== this.data.roleName  // true if role is null/empty and original wasn't
  }
}

If this.ephemeral.role is null (initial state before selection) and this.data.roleName is 'moderator', then role !== this.data.roleName evaluates to true, enabling the submit button incorrectly.

Suggested fix:

enableSubmitBtn () {
  const isCurrentRoleCustom = this.data.roleName === GROUP_ROLES.CUSTOM
  const { permissions, role } = this.ephemeral

  if (!role) return false  // Add this guard

  if (isCurrentRoleCustom) {
    // ...
  }
  // ...
}

Issue 8: 🟡 Modal close in created hook without preventing further rendering

  • Addressed
  • Dismissed

Location:

  • frontend/views/containers/group-settings/roles-and-permissions/EditPermissionsModal.vue:208-213
  • frontend/views/containers/group-settings/roles-and-permissions/RemoveRoleModal.vue:109-113
created () {
  if (!this.data?.memberID) {
    this.close()
  }
}

After calling this.close(), the component lifecycle continues and the template may attempt to render with missing data, potentially causing errors.

Suggested fix:

created () {
  if (!this.data?.memberID) {
    this.close()
    return  // Add explicit return
  }
  this.initComponent()
}

Issue 9: ⚪ Unused variable in PermissionTableRow.vue

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/PermissionTableRow.vue:59-64

data () {
  return {
    ephemeral: {
      isSubmitting: false  // Never used
    }
  }
}

This variable is declared but never referenced anywhere in the component.


Issue 10: ⚪ Typo in CSS class name (multiple files)

  • Addressed
  • Dismissed

Locations:

  • frontend/views/containers/group-settings/roles-and-permissions/AddPermissionsListItem.vue:273
  • frontend/views/containers/group-settings/roles-and-permissions/EditPermissionsModal.vue:296
.c-perimission-items-container  // Should be "permission" not "perimission"

Issue 11: ⚪ Unused import in RemoveRoleModal.vue

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/RemoveRoleModal.vue:43

import AvatarUser from '@components/AvatarUser.vue'

This import is not used directly in the component (it's used within MemberName).


Issue 12: ⚪ Unused import in AddPermissionsListItem.vue

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/AddPermissionsListItem.vue:47

import AvatarUser from '@components/AvatarUser.vue'

This import is not used directly in the component (it's used within MemberName).


Review generated using glm-5 via Z.AI. Comment /review to re-run.

@taoeffect
Copy link
Member

taoeffect commented Feb 20, 2026

/crush_fast


AI review started.

@github-actions
Copy link

Advanced AI Review

  • Type: Agentic (crush)
  • Model: glm-5
Click to expand review

Code Review: Permissions/Roles System Implementation

Issue 1: Null Pointer Exception in Validation 🔴

  • Addressed
  • Dismissed

Location: frontend/model/contracts/group.js:1162-1163

Problem: In the updatePermissions validation, line 1158 correctly uses optional chaining (item.permissions?.includes(...)), but lines 1162-1163 access item.permissions.some(...) without the optional chaining operator. Since the schema uses objectMaybeOf, the permissions field is optional and can be undefined, causing a TypeError.

if (myRoleName === GROUP_ROLES.MODERATOR_DELEGATOR &&
  item.permissions.some(permission => !myPermissions.includes(permission))) {

Suggested Fix:

if (myRoleName === GROUP_ROLES.MODERATOR_DELEGATOR &&
  item.permissions?.some(permission => !myPermissions.includes(permission))) {

Issue 2: Getter Returns Undefined for Custom Roles Without Permissions 🟡

  • Addressed
  • Dismissed

Location: frontend/model/contracts/shared/getters/group.js:154-156

Problem: The getGroupMemberPermissionsById getter returns profile.role.permissions for CUSTOM roles without checking if it's defined. The schema allows permissions to be optional. This could return undefined instead of an array, causing issues for callers that expect an array.

return profile.role.name === GROUP_ROLES.CUSTOM
  ? profile.role.permissions
  : GROUP_PERMISSIONS_PRESET[profile.role.name]

Suggested Fix:

return profile.role.name === GROUP_ROLES.CUSTOM
  ? (profile.role.permissions || [])
  : (GROUP_PERMISSIONS_PRESET[profile.role.name] || [])

Issue 3: allGroupMemberRolesAndPermissions Returns Undefined Permissions 🟡

  • Addressed
  • Dismissed

Location: frontend/model/contracts/shared/getters/group.js:135-137

Problem: Similar to Issue 2, the allGroupMemberRolesAndPermissions getter can return objects with permissions: undefined, which will break getAllMemberIdsWithPermission at line 145 when it calls permissions.includes(permission).

permissions: profile.role.name === GROUP_ROLES.CUSTOM
  ? profile.role.permissions
  : GROUP_PERMISSIONS_PRESET[profile.role.name],

Suggested Fix:

permissions: profile.role.name === GROUP_ROLES.CUSTOM
  ? (profile.role.permissions || [])
  : (GROUP_PERMISSIONS_PRESET[profile.role.name] || []),

Issue 4: Computed Property Returns Undefined in EditPermissionsModal 🟡

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/EditPermissionsModal.vue:114-115

Problem: permissionsToDisplay can return undefined when this.ephemeral.role is null or doesn't match any key in permissionPresets. This is used in a v-for which will silently render nothing, but could cause confusion and inconsistent UI state.

permissionsToDisplay () {
  return this.config.permissionPresets[this.ephemeral.role]
},

Suggested Fix:

permissionsToDisplay () {
  return this.config.permissionPresets[this.ephemeral.role] || []
},

Issue 5: Similar Undefined Access in AddPermissionsListItem 🟡

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/AddPermissionsListItem.vue:130

Problem: GROUP_PERMISSIONS_PRESET[role] can return undefined if the role doesn't exist in the preset (e.g., if role is GROUP_ROLES.ADMIN which is intentionally excluded from the config).

this.ephemeral.permissionOptionsToDisplay = GROUP_PERMISSIONS_PRESET[role]

Suggested Fix:

this.ephemeral.permissionOptionsToDisplay = GROUP_PERMISSIONS_PRESET[role] || []

Issue 6: Missing Optional Chaining in updateRole Method 🟡

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/AddPermissionsListItem.vue:143

Problem: Similar to Issue 5, accessing this.config.permissionPresets[value] without a fallback.

this.ephemeral.selectedPermissions = this.config.permissionPresets[value]

Suggested Fix:

this.ephemeral.selectedPermissions = this.config.permissionPresets[value] || []

Issue 7: Duplicate Permission Preset Definition ⚪️

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/EditPermissionsModal.vue:90-94 and AddPermissionsListItem.vue:64-70

Problem: Both components define config.permissionPresets with the same structure. This duplication could lead to inconsistencies if one is updated without the other.

Suggested Improvement: Consider extracting this configuration to a shared utility or constants file.


Issue 8: Unused Import ⚪️

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/PermissionTableRow.vue

Problem: The getPermissionDisplayName function is imported but not used in the template (only getRoleDisplayName is used in methods).

import {
  getRoleDisplayName,
  getPermissionDisplayName  // unused
} from './permissions-utils.js'

Suggested Fix: Remove the unused import.


Summary

Rating Count Issues
🔴 1 Null pointer exception in validation
🟡 5 Undefined access issues in getters and UI components
⚪️ 2 Code quality (duplication, unused imports)

Review generated using glm-5 via Z.AI. Comment /review to re-run.

@taoeffect
Copy link
Member

taoeffect commented Feb 20, 2026

@SebinSong Above are two AI reviews using the same model but different environments (opencode vs crush), please let me know which one did better, or if they were about the same.

@taoeffect
Copy link
Member

taoeffect commented Feb 20, 2026

/crush_fast (Sorry @SebinSong, I remembered that there's a bug in crush's default max context size for GLM-5, it's about half of what it should be. That's why I'm re-running the crush review as I've added a fix for that.)


AI review started.

@github-actions
Copy link

github-actions bot commented Feb 20, 2026

Advanced AI Review

  • Type: Agentic (crush)
  • Model: glm-5
Click to expand review

Code Review: Add Permissions Feature

🔴 Issue 1: Runtime Error in Validation - item.permissions May Be Undefined

  • Addressed
  • Dismissed

Location: frontend/model/contracts/group.js:1162-1164

Problem: When assigning a predefined role (MODERATOR_DELEGATOR or MODERATOR), the AddPermissionsModal.vue only includes permissions in the payload when role === GROUP_ROLES.CUSTOM (line 142-144). This means for predefined roles, item.permissions is undefined. The validation then attempts to call .some() on undefined:

if (myRoleName === GROUP_ROLES.MODERATOR_DELEGATOR &&
  item.permissions.some(permission => !myPermissions.includes(permission))) {

This will throw a TypeError: Cannot read property 'some' of undefined when a moderator-delegator tries to assign another moderator-delegator or moderator role.

Suggested Fix:

// frontend/model/contracts/group.js:1162-1165
if (myRoleName === GROUP_ROLES.MODERATOR_DELEGATOR &&
  item.permissions?.some(permission => !myPermissions.includes(permission))) {
  throw new TypeError(L("You(moderator-delegator) cannot assign permissions that you don't have to others."))
}

Or, alternatively, always pass permissions in the payload from AddPermissionsModal.vue:

// frontend/views/containers/group-settings/roles-and-permissions/AddPermissionsModal.vue:135-146
data: this.ephemeral.roleEntries.map(entry => {
  return {
    memberID: entry.userId,
    action: GROUP_PERMISSION_CHANGE_ACTIONS.UPSERT,
    roleName: entry.role,
    permissions: entry.permissions // Always include, even if empty for predefined roles
  }
})

🟡 Issue 2: Potential undefined Rendered in Vue Template

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/RolesAndPermissions.vue:75-82

Problem: The groupPermissionsToDisplay computed property can return [undefined] in edge cases:

groupPermissionsToDisplay () {
  const myEntry = this.allGroupMemberRolesAndPermissions.find(entry => entry.memberID === this.ourIdentityContractId)
  // ...
  return this.canViewOtherMembersPermissions
    ? sortedEntries
    : [myEntry]  // Could be [undefined] if myEntry is not found
}

While this is mitigated by the displayRolesAndPermissions check that requires ourGroupProfile.role to be truthy, there could be race conditions during state updates where the profile has a role but it hasn't yet appeared in allGroupMemberRolesAndPermissions.

Suggested Fix:

// frontend/views/containers/group-settings/roles-and-permissions/RolesAndPermissions.vue:75-82
groupPermissionsToDisplay () {
  const myEntry = this.allGroupMemberRolesAndPermissions.find(entry => entry.memberID === this.ourIdentityContractId)
  const otherEntries = this.allGroupMemberRolesAndPermissions.filter(entry => entry.memberID !== this.ourIdentityContractId)
  const sortedEntries = myEntry ? [myEntry, ...otherEntries] : this.allGroupMemberRolesAndPermissions
  return this.canViewOtherMembersPermissions
    ? sortedEntries
    : myEntry ? [myEntry] : []  // Return empty array if no entry found
}

🟡 Issue 3: Missing Null Check for groupChatRooms in deleteChatRoom Validation

  • Addressed
  • Dismissed

Location: frontend/model/contracts/group.js:1285

Problem: The validation accesses getters.groupChatRooms[data.chatRoomID].creatorID without checking if the chatroom exists:

if (getters.groupChatRooms[data.chatRoomID].creatorID !== innerSigningContractID) {

If data.chatRoomID doesn't exist in groupChatRooms, this will throw a TypeError: Cannot read property 'creatorID' of undefined.

Suggested Fix:

// frontend/model/contracts/group.js:1282-1288
validate: actionRequireActiveMember((data, { getters, message: { innerSigningContractID } }) => {
  objectOf({ chatRoomID: stringMax(MAX_HASH_LEN, 'chatRoomID') })(data)

  const chatRoom = getters.groupChatRooms[data.chatRoomID]
  if (!chatRoom) {
    throw new TypeError(L('Chatroom does not exist.'))
  }
  if (chatRoom.creatorID !== innerSigningContractID) {
    // TODO: add DELETE_CHANNEL permission check when it's implemented
    throw new TypeError(L('You do not have permission to delete this channel.'))
  }
}),

🟡 Issue 4: EditPermissionsModal - permissionsToDisplay Can Return undefined

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/EditPermissionsModal.vue:114-115

Problem: The permissionsToDisplay computed property returns this.config.permissionPresets[this.ephemeral.role], but if ephemeral.role is null or doesn't match a key in the presets (e.g., if data.roleName is GROUP_ROLES.ADMIN), it returns undefined. The template then iterates over this with v-for, which could cause issues.

permissionsToDisplay () {
  return this.config.permissionPresets[this.ephemeral.role]
}

Note that GROUP_ROLES.ADMIN is not in config.permissionPresets:

permissionPresets: {
  [GROUP_ROLES.MODERATOR]: GROUP_PERMISSIONS_PRESET[GROUP_ROLES.MODERATOR],
  [GROUP_ROLES.MODERATOR_DELEGATOR]: GROUP_PERMISSIONS_PRESET[GROUP_ROLES.MODERATOR_DELEGATOR],
  [GROUP_ROLES.CUSTOM]: GROUP_PERMISSIONS_CUSTOM_OPTIONS
}

But admins can still edit permissions (they have DELEGATE_PERMISSIONS). If an admin tries to edit another user's permissions who has the admin role, the modal would fail.

Suggested Fix:

// frontend/views/containers/group-settings/roles-and-permissions/EditPermissionsModal.vue:114-116
permissionsToDisplay () {
  return this.config.permissionPresets[this.ephemeral.role] || []
}

Also consider whether admins should be able to edit other admins' permissions, or if this should be restricted. The validation in group.js already prevents assigning the admin role:

if (item.roleName === GROUP_ROLES.ADMIN) {
  throw new TypeError(L('Cannot assign admin role.'))
}

But it doesn't prevent editing an existing admin's permissions. Consider adding a check in PermissionTableRow.vue to not show the action menu for admins.


🟡 Issue 5: Inconsistent Error Message Update

  • Addressed
  • Dismissed

Location: frontend/model/contracts/group.js:1308-1312 and frontend/model/contracts/chatroom.js:281-284

Problem: The error message for deleting a channel was changed from "Only the channel creator can delete channel." to "You do not have permission to delete this channel." This is good for the new permissions system, but the permission check hasn't been implemented yet (as indicated by the TODO comment). The error message is now misleading since it still only checks for the creator.

However, looking at the diff more carefully, the change from console.log to console.error in group.js:1311 is a good improvement.

Suggested Fix: Either implement the DELETE_CHANNEL permission check, or revert to the more specific error message:

// frontend/model/contracts/group.js:1285-1288
if (getters.groupChatRooms[data.chatRoomID].creatorID !== innerSigningContractID) {
  // TODO: add DELETE_CHANNEL permission check when it's implemented
  throw new TypeError(L('Only the channel creator can delete this channel.'))
}

⚪️ Issue 6: Unused profile Computed Property

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/AddPermissionsListItem.vue:91-93

Problem: The profile computed property is defined but only used in the updateRole method. It could be removed and the lookup done inline.

profile () {
  return this.globalProfile(this.data.userId)
}

Suggested Fix: This is a minor cleanup - either remove the computed property if not needed elsewhere, or keep it for clarity. No action required.


⚪️ Issue 7: Comment Typo in Validation Error Message

  • Addressed
  • Dismissed

Location: frontend/model/contracts/group.js:1164

Problem: The error message has a minor grammatical issue: "You(moderator-delegator)" lacks a space.

throw new TypeError(L("You(moderator-delegator) cannot assign permissions that you don't have to others."))

Suggested Fix:

throw new TypeError(L("You (moderator-delegator) cannot assign permissions that you don't have to others."))

⚪️ Issue 8: PermissionsToDisplay Typo in CSS Class

  • Addressed
  • Dismissed

Location: frontend/views/containers/group-settings/roles-and-permissions/AddPermissionsListItem.vue:29 and EditPermissionsModal.vue:46

Problem: The CSS class name has a typo: c-perimission-items-container should be c-permission-items-container.

.c-perimission-items-container {

Suggested Fix: Rename to c-permission-items-container in both the template and style sections.


Summary

Issue Severity Confidence Status
1. Runtime error with item.permissions.some() 🔴 High High Needs fix
2. Potential undefined in Vue template 🟡 Medium Medium Consider fix
3. Missing null check for chatroom 🟡 Medium High Should fix
4. permissionsToDisplay can return undefined 🟡 Medium High Should fix
5. Misleading error message 🟡 Medium High Consider fix
6. Unused computed property ⚪️ Low High Optional
7. Typo in error message ⚪️ Low High Optional
8. CSS class name typo ⚪️ Low High Optional

Review generated using glm-5 via Z.AI. Comment /review to re-run.

devin-ai-integration[bot]

This comment was marked as resolved.

@SebinSong
Copy link
Collaborator Author

SebinSong commented Feb 24, 2026

@taoeffect PR is ready for review again.

Personally It actually didn't feel like one was better than the other. They performance seem equal and also have several overlapping items.

But it feels like glm-5 doesn't perform reviews with broad/deep investigation in comparison to Devin or other LLM models you've used. For example:

it had multiple items(in both opencode/crush reviews) where it says about possible UI runtime error caused by profile.role potentially being null.
But I've dismissed all these items because profile.role being null is properly guarded in the parent component of all .vue files it raised this concern about. (doesn't seem to check something with considering the context it is used as well)

Also, there was one item(issue 8 in this review) where it says something that is not true (there is no other Vue lifecycle hooks other than created() in both files), and the suggested fix has an anti Vue pattern(return is not supposed to be used in created() hook).

But the rest items were helpful in finding several unused codes and typos.

Thanks,

@taoeffect
Copy link
Member

taoeffect commented Feb 24, 2026

/crush_fast {{openrouter, google/gemini-3.1-pro-preview, google/gemini-3-flash-preview}}

Thanks @SebinSong! I'm gonna try one more model that's supposed to be better than GLM-5 just in case it's able to find some things those missed. I'll also double-check with @corrideat to see if there's anything he thinks we should wait for before merging this.

EDIT: (I will also test these changes manually before merging as well)


AI review started.

@github-actions
Copy link

github-actions bot commented Feb 24, 2026

Advanced AI Review

  • Type: Agentic (crush)
  • Model: google/gemini-3.1-pro-preview
Click to expand review

Here is my review of the PR. I have checked for bugs, security vulnerabilities, edge cases, and areas of improvement, evaluating and prioritizing them by importance.

1. 🔴 Vulnerability in updatePermissions validates user input instead of the target's current role

  • Addressed
  • Dismissed

In the updatePermissions contract validation, the logic incorrectly checks against item.roleName (the new role name requested) instead of examining the target member's actual current role, to decide if someone is illegally modifying an admin's privileges. Because of this, a user with DELEGATE_PERMISSIONS (e.g. moderator-delegator) can maliciously bypass this validation. They could simply omit admin from item.roleName while doing a REMOVE request, successfully stripping admin privileges, or overwrite the admin with a lesser role.

Additionally, the way the REMOVE action processes deletes the role unconditionally, which allows for admin role removal. Modifying the validation block handles this cleanly for both UPSERT and REMOVE:

frontend/model/contracts/group.js:1143-1172

        for (const item of data) {
          if (!state.profiles[item.memberID]) {
            throw new TypeError(L(`${item.memberID} is not a member of the group.`))
          }

          if (item.memberID === innerSigningContractID) {
            throw new TypeError(L('You cannot modify your own permissions.'))
          }

          const targetRoleName = getters.getGroupMemberRoleNameById(item.memberID)
          
          if (targetRoleName === GROUP_ROLES.ADMIN) {
            throw new TypeError(L('Cannot modify the permissions of an admin.'))
          }

          if (item.action === GROUP_PERMISSION_CHANGE_ACTIONS.UPSERT) {
            const isMyRoleAdmin = myRoleName === GROUP_ROLES.ADMIN
            if (item.roleName === GROUP_ROLES.ADMIN) {
              throw new TypeError(L('Cannot assign or update admin role.'))
            }

            if (item.permissions?.includes(GROUP_PERMISSIONS.ASSIGN_DELEGATOR)) {
              throw new TypeError(L('Only the group admin can have assign-delegator permission and it cannot be granted to other roles.'))
            }

            if (item.roleName === GROUP_ROLES.MODERATOR_DELEGATOR && !isMyRoleAdmin) {
              throw new TypeError(L('You do not have permission to assign moderator-delegator role.'))
            }

            if (myRoleName === GROUP_ROLES.MODERATOR_DELEGATOR &&
              item.permissions?.some(permission => !myPermissions.includes(permission))) {
              throw new TypeError(L("You(moderator-delegator) cannot assign permissions that you don't have to others."))
            }
          }
        }

2. 🔴 Legacy groups will be permanently locked out of the permissions system

  • Addressed
  • Dismissed

Older groups created before this PR will not have a role explicit attribute assigned to their group creators inside their profiles. Since validations and the frontend rely on parsing profile.role.name, creators of existing groups will be evaluated as having no role and empty [] permissions. They won't be able to access the UI safely or assign roles.

To prevent backwards-compatibility breakage without requiring a data-migration, seamlessly catch this case in your getters by dynamically mapping GROUP_ROLES.ADMIN when checking the currentGroupOwnerID.

frontend/model/contracts/shared/getters/group.js:132-154

  allGroupMemberRolesAndPermissions (state, getters) {
    // Get the list of { roleName: string, permissions: string[], memberID: string } for all members in the group.
    return Object.entries(getters.groupProfiles)
      .filter(([memberID, profile]: [string, any]) => !!profile.role || memberID === getters.currentGroupOwnerID)
      .map(([memberID, profile]: [string, any]) => {
        const roleName = memberID === getters.currentGroupOwnerID ? GROUP_ROLES.ADMIN : profile.role?.name
        return {
          roleName,
          permissions: roleName === GROUP_ROLES.CUSTOM
            ? profile.role.permissions
            : GROUP_PERMISSIONS_PRESET[roleName],
          memberID
        }
      })
  },
  getAllMemberIdsWithPermission (state, getters) {
    // Get all members with a specific permission as a list of { roleName: string, permissions: string[], memberID: string }.
    // Maybe make this getter takes an array of permissions(instead of a single permission) in the future if there is a need for it.
    return (permission) => {
      return getters.allGroupMemberRolesAndPermissions.filter(({ permissions }) => permissions.includes(permission))
        .map(({ memberID }) => memberID)
    }
  },
  getGroupMemberPermissionsById (state, getters) {
    // Pass the group memberID and get all the permissions that the member has if any.
    return (memberID) => {
      if (memberID === getters.currentGroupOwnerID) return GROUP_PERMISSIONS_PRESET[GROUP_ROLES.ADMIN]
      const profile = getters.groupProfiles[memberID]
      if (!profile?.role) return []
      return profile.role.name === GROUP_ROLES.CUSTOM
        ? profile.role.permissions
        : GROUP_PERMISSIONS_PRESET[profile.role.name]
    }
  },
  getGroupMemberRoleNameById (state, getters) {
    // Pass the group memberID and get the role name of the member if any.
    return (memberID) => {
      if (memberID === getters.currentGroupOwnerID) return GROUP_ROLES.ADMIN
      const profile = getters.groupProfiles[memberID]
      return profile?.role?.name || ''
    }
  },

Then leverage this getter in GroupSettingsMain rather than accessing the raw Vuex state directly, restoring access to the "Roles And Permissions" UI tab:

frontend/views/containers/group-settings/GroupSettingsMain.vue:104-110

    ...mapGetters([
      'groupSettings',
      'currentGroupOwnerID',
      'ourIdentityContractId',
      'groupProposalSettings',
      'ourRoleName'
    ]),

frontend/views/containers/group-settings/GroupSettingsMain.vue:117-120

    displayRolesAndPermissions () {
      // TODO: Remove this once the development is complete and the feature is ready for release.
      return process.env.NODE_ENV === 'development' && this.ourRoleName
    },

3. 🔴 The removeMember validation allows lower-ranked users to kick admins

  • Addressed
  • Dismissed

Inside gi.contracts/group/removeMember, a member possessing GROUP_PERMISSIONS.REMOVE_MEMBER skips downstream proposal validations. However, your logic neglects to verify hierarchy, thereby enabling any moderator to forcefully expel an admin or even the group creator. Ensure lower roles cannot kick admin ranked profiles unless they are the group creator.

frontend/model/contracts/group.js:824-826

        if (isGroupCreator || myPermissions.includes(GROUP_PERMISSIONS.REMOVE_MEMBER)) {
          const targetRoleName = getters.getGroupMemberRoleNameById(memberToRemove)
          if (targetRoleName === GROUP_ROLES.ADMIN && !isGroupCreator) {
            throw new TypeError(L('Cannot remove an admin.'))
          }
          return true
        } else if (membersCount < 3) {

4. 🟡 inviteRevoke async crash on invalid key ownership

  • Addressed
  • Dismissed

In Chelonia, deleting a key usually verifies you are the original owner of it. In this PR, since users can be granted overarching permission to revoke items created by other people, keyDel risks throwing an unauthorized invocation or key error that aborts execution. Wrapping it in a try-catch ensures the contract inviteRevoke event continues to propagate appropriately in cases where the request is legitimate per group permission matrices.

frontend/controller/actions/group.js:1019-1029

  ...encryptedAction('gi.actions/group/inviteRevoke', L('Failed to revoke invite.'), async function (sendMessage, params, signingKeyId) {
    try {
      await sbp('chelonia/out/keyDel', {
        contractID: params.contractID,
        contractName: 'gi.contracts/group',
        data: [params.data.inviteKeyId],
        signingKeyId
      })
    } catch (e) {
      console.warn('[gi.actions/group/inviteRevoke] Failed to delete invite key:', e)
    }

    return sendMessage(params)
  }),

5. 🟡 Conditional rendering removes the <td> tag in PermissionTableRow.vue resulting in misaligned layouts

  • Addressed
  • Dismissed

In RolesAndPermissions.vue, the th.th-action acts globally. However in the rows, you are rendering out td.td-action conditionally depending on !isAdmin && !isMe. This means that if it encounters the Admin's or your own row, the row is rendered entirely without the trailing <td class="td-action"> tag in HTML, which will cause misaligned borders or visual layout problems for <table>. Leave the <td> tag present but conditionally mask its contents.

frontend/views/containers/group-settings/roles-and-permissions/PermissionTableRow.vue:24-29

    td.td-action(v-if='canDelegatePermissions')
      .c-action-wrapper(v-if='!isAdmin && !isMe')
        permission-action-menu(
          @remove='onActionMenuSelect("remove")'
          @edit='onActionMenuSelect("edit")'
        )

Review generated using google/gemini-3.1-pro-preview via OpenRouter. Comment /review to re-run.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Member

@corrideat corrideat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks much better. Thank you for addressing the feedback.

One thing I've noticed now is that both in naming and errors there's a lot of personal pronouns (you, my, etc.) This form of address doesn't seem very accurate for checks that everyone is running. Concrete example: someone builds a custom client and attempts to change permissions in a way that's not allowed (say, giving themselves the admin permission). While this should be rejected, it'd be confusing for other people to get a message having 'you' in it. Of course, it's fine to say 'you' in the UI, so long as only the user performing the action receives it.

Copy link
Member

@corrideat corrideat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants