-
-
Notifications
You must be signed in to change notification settings - Fork 50
#202 - Step 2, 3, 4: Implement Add/Remove/Edit permissions
#2895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 76 commits
9d5c3d9
dee87eb
c330801
fa3c77e
8233f2b
bac2f6b
56a1cad
adca63b
e4297b9
9b84fdb
b8bf379
06414fe
767007e
628fb9b
531382f
e2d1e03
e4823b0
d2da1be
bb41eff
bd40c51
46a28d5
0d6a3cb
49a25c5
3a49516
c144e42
06d1114
06ab362
a8aa8a6
2388741
18f6aac
e3abe1c
8a1bd2d
889a9ff
0e13361
4a3fdde
8886b67
5b111eb
bf7f9e8
e072a23
eb1bd11
4f0058d
49d8045
b141c88
665dfa4
4f61911
55c0baa
5814d03
2bf3471
3a0a5f5
57da992
ae8cf6b
c37d01d
5a74d6f
f0440cf
848d931
ff8d493
e2a03d4
98c1cf1
c1c346d
2b50f8e
d0adad0
cc50ca7
9fa58d4
e18a060
0656413
e8e278f
39ab757
ab36b2f
554b540
3074ebf
fa54b56
c858ae0
780bdc5
a872e16
45cb45c
a97ee68
dff35fe
2d2508f
82b9c0d
6f9c624
62b57f1
8ac1bba
ab26b3f
76a8f31
3ed61c2
44ffa2c
77fe0ec
346472f
18eb94b
64d691b
9ef8b08
f412b42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,11 @@ import { | |
| PROPOSAL_REASON_MAX_CHAR, | ||
| PROPOSAL_PROPOSAL_SETTING_CHANGE, | ||
| PROPOSAL_REMOVE_MEMBER, | ||
| STATUS_CANCELLED, STATUS_EXPIRED, STATUS_OPEN | ||
| STATUS_CANCELLED, STATUS_EXPIRED, STATUS_OPEN, | ||
| GROUP_ROLES, | ||
| GROUP_PERMISSIONS_PRESET, | ||
| GROUP_PERMISSIONS, | ||
| GROUP_PERMISSION_UPDATE_ACTIONS | ||
| } from './shared/constants.js' | ||
| import { adjustedDistribution, unadjustedDistribution } from './shared/distribution/distribution.js' | ||
| import { paymentHashesFromPaymentPeriod, referenceTally } from './shared/functions.js' | ||
|
|
@@ -56,7 +60,7 @@ function fetchInitKV (obj: Object, key: string, initialValue: any): any { | |
| return value | ||
| } | ||
|
|
||
| function initGroupProfile (joinedDate: string, joinedHeight: number, reference: string) { | ||
| function initGroupProfile (joinedDate: string, joinedHeight: number, reference: string, isGroupCreator: boolean) { | ||
| return { | ||
| globalUsername: '', // TODO: this? e.g. groupincome:greg / namecoin:bob / ens:alice | ||
| joinedDate, | ||
|
|
@@ -65,7 +69,10 @@ function initGroupProfile (joinedDate: string, joinedHeight: number, reference: | |
| nonMonetaryContributions: [], | ||
| status: PROFILE_STATUS.ACTIVE, | ||
| departedDate: null, | ||
| incomeDetailsLastUpdatedDate: null | ||
| incomeDetailsLastUpdatedDate: null, | ||
| role: isGroupCreator | ||
| ? { name: GROUP_ROLES.ADMIN, permissions: GROUP_PERMISSIONS_PRESET.ADMIN } | ||
| : null | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -368,6 +375,11 @@ const leaveAllChatRoomsUponLeaving = (groupID, state, memberID, actorID) => { | |
| ) | ||
| } | ||
|
|
||
| const getMemberPermissions = ({ getters, memberID }) => { | ||
| const profile = getters.groupProfile(memberID) | ||
| return profile?.role?.permissions || [] | ||
| } | ||
|
|
||
| export const actionRequireActiveMember = (next: Function): Function => (data, props) => { | ||
| const innerSigningContractID = props.message.innerSigningContractID | ||
| if (!innerSigningContractID || innerSigningContractID === props.contractID) { | ||
|
|
@@ -809,6 +821,7 @@ sbp('chelonia/defineContract', { | |
| const memberToRemove = data.memberID || innerSigningContractID | ||
| const membersCount = getters.groupMembersCount | ||
| const isGroupCreator = innerSigningContractID === getters.currentGroupOwnerID | ||
| const myPermissions = getMemberPermissions({ getters, memberID: innerSigningContractID }) | ||
|
|
||
| if (!state.profiles[memberToRemove]) { | ||
| throw new GIGroupNotJoinedError(L('Not part of the group.')) | ||
|
|
@@ -821,7 +834,7 @@ sbp('chelonia/defineContract', { | |
| return true | ||
| } | ||
|
|
||
| if (isGroupCreator) { | ||
| if (isGroupCreator || myPermissions.includes(GROUP_PERMISSIONS.REMOVE_MEMBER)) { | ||
| return true | ||
| } else if (membersCount < 3) { | ||
| // In a small group only the creator can remove someone | ||
|
|
@@ -883,7 +896,9 @@ sbp('chelonia/defineContract', { | |
| if (state.profiles[innerSigningContractID]?.status === PROFILE_STATUS.ACTIVE) { | ||
| throw new Error(`[gi.contracts/group/inviteAccept] Existing members can't accept invites: ${innerSigningContractID}`) | ||
| } | ||
| state.profiles[innerSigningContractID] = initGroupProfile(meta.createdDate, height, data.reference) | ||
|
|
||
| const isGroupCreator = state.groupOwnerID === innerSigningContractID | ||
| state.profiles[innerSigningContractID] = initGroupProfile(meta.createdDate, height, data.reference, isGroupCreator) | ||
| // If we're triggered by handleEvent in state.js (and not latestContractState) | ||
| // then the asynchronous sideEffect function will get called next | ||
| // and we will subscribe to this new user's identity contract | ||
|
|
@@ -953,14 +968,24 @@ sbp('chelonia/defineContract', { | |
| } | ||
| }, | ||
| 'gi.contracts/group/inviteRevoke': { | ||
| validate: actionRequireActiveMember((data, { state }) => { | ||
| validate: actionRequireActiveMember((data, { state, getters, message: { innerSigningContractID } }) => { | ||
| objectOf({ | ||
| inviteKeyId: stringMax(MAX_HASH_LEN, 'inviteKeyId') | ||
| })(data) | ||
|
|
||
| if (!state._vm.invites[data.inviteKeyId]) { | ||
| const invite = state._vm.invites[data.inviteKeyId] | ||
|
|
||
| if (!invite) { | ||
| throw new TypeError(L('The link does not exist.')) | ||
| } | ||
|
|
||
| const myPermissions = getMemberPermissions({ getters, memberID: innerSigningContractID }) | ||
| const inviteCreatorID = state.invites?.[data.inviteKeyId]?.creatorID | ||
|
|
||
| // Only the creator and members with the REVOKE_INVITE permission can revoke an invite | ||
| if (inviteCreatorID !== innerSigningContractID && !myPermissions.includes(GROUP_PERMISSIONS.REVOKE_INVITE)) { | ||
| throw new TypeError(L('You do not have permission to revoke this invite.')) | ||
corrideat marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| }), | ||
| process () { | ||
| // Handled by Chelonia | ||
|
|
@@ -1106,6 +1131,72 @@ sbp('chelonia/defineContract', { | |
| } | ||
| } | ||
| }, | ||
| 'gi.contracts/group/updatePermissions': { | ||
| validate: actionRequireActiveMember((data, { state, getters, message: { innerSigningContractID } }) => { | ||
| arrayOf(objectMaybeOf({ | ||
| memberID: stringMax(MAX_HASH_LEN, 'memberID'), | ||
| action: validatorFrom(x => Object.values(GROUP_PERMISSION_UPDATE_ACTIONS).includes(x)), | ||
| roleName: validatorFrom(x => Object.values(GROUP_ROLES).includes(x)), | ||
| permissions: arrayOf(validatorFrom(x => Object.values(GROUP_PERMISSIONS).includes(x))) | ||
| }))(data) | ||
|
|
||
| const myProfile = getters.groupProfile(innerSigningContractID) | ||
| const myPermissions = myProfile?.role?.permissions || [] | ||
|
|
||
| if (data.some(item => item?.roleName === GROUP_ROLES.ADMIN)) { | ||
| throw new TypeError(L('Cannot assign admin role.')) | ||
| } | ||
|
|
||
| if (data.some(item => item.permissions?.includes(GROUP_PERMISSIONS.ASSIGN_DELEGATOR) && | ||
| myProfile?.role?.name !== GROUP_ROLES.ADMIN)) { | ||
| // Only the group admin can assign the delegator role | ||
| throw new TypeError(L('You do not have permission to assign delegator role.')) | ||
| } | ||
|
|
||
| if (!myPermissions.includes(GROUP_PERMISSIONS.DELEGATE_PERMISSIONS)) { | ||
|
||
| throw new TypeError(L('You do not have permission to delegate permissions.')) | ||
| } | ||
|
|
||
| if (data.some(item => { | ||
| return item.action === GROUP_PERMISSION_UPDATE_ACTIONS.EDIT && !state.profiles[item.memberID]?.role | ||
| })) { | ||
| throw new TypeError(L('Cannot edit permissions for a member who does not have a role.')) | ||
| } | ||
SebinSong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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.')) | ||
corrideat marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
SebinSong marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| }), | ||
| process ({ data }, { state }) { | ||
| for (const item of data) { | ||
| const groupProfile = state.profiles[item.memberID] | ||
|
|
||
| switch (item.action) { | ||
| case GROUP_PERMISSION_UPDATE_ACTIONS.ADD: | ||
| groupProfile.role = { | ||
| name: item.roleName, | ||
| permissions: item.permissions | ||
|
||
| } | ||
| break | ||
| case GROUP_PERMISSION_UPDATE_ACTIONS.EDIT: | ||
| groupProfile.role = { | ||
| name: item.roleName || groupProfile.role.name, | ||
| permissions: item.permissions || groupProfile.role.permissions | ||
| } | ||
| break | ||
| case GROUP_PERMISSION_UPDATE_ACTIONS.REMOVE: | ||
| groupProfile.role = null | ||
| break | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| 'gi.contracts/group/updateAllVotingRules': { | ||
| validate: actionRequireActiveMember(objectMaybeOf({ | ||
| ruleName: x => [RULE_PERCENTAGE, RULE_DISAGREEMENT].includes(x), | ||
|
|
@@ -1200,8 +1291,10 @@ sbp('chelonia/defineContract', { | |
| validate: actionRequireActiveMember((data, { getters, message: { innerSigningContractID } }) => { | ||
| objectOf({ chatRoomID: stringMax(MAX_HASH_LEN, 'chatRoomID') })(data) | ||
|
|
||
| if (getters.groupChatRooms[data.chatRoomID].creatorID !== innerSigningContractID) { | ||
| throw new TypeError(L('Only the channel creator can delete channel.')) | ||
| const myPermissions = getMemberPermissions({ getters, memberID: innerSigningContractID }) | ||
|
|
||
| if (getters.groupChatRooms[data.chatRoomID].creatorID !== innerSigningContractID && !myPermissions.includes(GROUP_PERMISSIONS.DELETE_CHANNEL)) { | ||
| throw new TypeError(L('You do not have permission to delete this channel.')) | ||
| } | ||
| }), | ||
| process ({ contractID, data }, { state }) { | ||
|
|
@@ -1213,7 +1306,7 @@ sbp('chelonia/defineContract', { | |
| } | ||
| delete state.chatRooms[data.chatRoomID] | ||
| }, | ||
| sideEffect ({ data, contractID, innerSigningContractID }) { | ||
| sideEffect ({ data, contractID, innerSigningContractID }, { getters }) { | ||
| // identityContractID intentionally left out because deleted chatrooms | ||
| // affect all users. If it's set, it should be set to | ||
| // sbp('state/vuex/state').loggedIn, _not_ innerSigningContractID, for | ||
|
|
@@ -1222,8 +1315,10 @@ sbp('chelonia/defineContract', { | |
| sbp('okTurtles.events/emit', DELETED_CHATROOM, { groupContractID: contractID, chatRoomID: data.chatRoomID }) | ||
| const { identityContractID } = sbp('state/vuex/state').loggedIn | ||
| if (identityContractID === innerSigningContractID) { | ||
| sbp('gi.actions/chatroom/delete', { contractID: data.chatRoomID, data: {} }).catch(e => { | ||
| console.log(`Error sending chatroom removal action for ${data.chatRoomID}`, e) | ||
| sbp('gi.actions/chatroom/delete', { | ||
| contractID: data.chatRoomID | ||
| }).catch(e => { | ||
| console.error(`Error sending chatroom removal action for ${data.chatRoomID}`, e) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.