-
Notifications
You must be signed in to change notification settings - Fork 1
Add updateWorkspaceMember action to change workspace roles #20
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| 'use strict'; | ||
|
|
||
| const extrovert = require('extrovert'); | ||
|
|
||
| module.exports = extrovert.toNetlifyFunction(require('../../src/actions/updateWorkspaceMember')); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| 'use strict'; | ||
|
|
||
| const Archetype = require('archetype'); | ||
| const connect = require('../../src/db'); | ||
| const mongoose = require('mongoose'); | ||
| const stripe = require('../integrations/stripe'); | ||
|
|
||
| const UpdateWorkspaceMemberParams = new Archetype({ | ||
| authorization: { | ||
| $type: 'string', | ||
| $required: true | ||
| }, | ||
| workspaceId: { | ||
| $type: mongoose.Types.ObjectId, | ||
| $required: true | ||
| }, | ||
| userId: { | ||
| $type: mongoose.Types.ObjectId, | ||
| $required: true | ||
| }, | ||
| role: { | ||
| $type: 'string', | ||
| $required: true, | ||
| $enum: ['admin', 'member', 'readonly', 'dashboards'] | ||
| } | ||
| }).compile('UpdateWorkspaceMemberParams'); | ||
|
|
||
| module.exports = async function updateWorkspaceMember(params) { | ||
| const db = await connect(); | ||
| const { AccessToken, User, Workspace } = db.models; | ||
|
|
||
| const { authorization, workspaceId, userId, role } = new UpdateWorkspaceMemberParams(params); | ||
|
|
||
| const accessToken = await AccessToken.findById(authorization).orFail(new Error('Invalid or expired access token')); | ||
| if (accessToken.expiresAt < new Date()) { | ||
| throw new Error('Access token has expired'); | ||
| } | ||
| const initiatedByUserId = accessToken.userId; | ||
|
|
||
| const workspace = await Workspace.findById(workspaceId).orFail(new Error('Workspace not found')); | ||
| const initiatedByUserRoles = workspace.members.find(member => member.userId.toString() === initiatedByUserId.toString())?.roles; | ||
| if (initiatedByUserRoles == null || (!initiatedByUserRoles.includes('admin') && !initiatedByUserRoles.includes('owner'))) { | ||
| throw new Error('Forbidden'); | ||
| } | ||
|
|
||
| const member = workspace.members.find(currentMember => currentMember.userId.toString() === userId.toString()); | ||
| if (member == null) { | ||
| throw new Error('Member not found in the workspace'); | ||
| } | ||
|
|
||
| member.roles = [role]; | ||
| await workspace.save(); | ||
|
|
||
| const users = await User.find({ _id: { $in: workspace.members.map(currentMember => currentMember.userId) } }); | ||
| if (workspace.stripeSubscriptionId) { | ||
| const seats = users.filter(user => !user.isFreeUser).length; | ||
| await stripe.updateSubscriptionSeats(workspace.stripeSubscriptionId, seats); | ||
| } | ||
|
|
||
| return { workspace, users }; | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| 'use strict'; | ||
|
|
||
| const { afterEach, beforeEach, describe, it } = require('mocha'); | ||
| const assert = require('assert'); | ||
| const connect = require('../src/db'); | ||
| const updateWorkspaceMember = require('../src/actions/updateWorkspaceMember'); | ||
|
|
||
| describe('updateWorkspaceMember', function() { | ||
| let db, AccessToken, User, Workspace; | ||
| let user, workspace, accessToken, memberUser; | ||
|
|
||
| beforeEach(async function() { | ||
| db = await connect(); | ||
| ({ AccessToken, User, Workspace } = db.models); | ||
|
|
||
| await AccessToken.deleteMany({}); | ||
| await User.deleteMany({}); | ||
| await Workspace.deleteMany({}); | ||
|
|
||
| user = await User.create({ | ||
| name: 'John Doe', | ||
| email: 'johndoe@example.com', | ||
| githubUsername: 'johndoe', | ||
| githubUserId: '1234' | ||
| }); | ||
|
|
||
| memberUser = await User.create({ | ||
| name: 'Jane Smith', | ||
| email: 'janesmith@example.com', | ||
| githubUsername: 'janesmith', | ||
| githubUserId: '5678' | ||
| }); | ||
|
|
||
| accessToken = await AccessToken.create({ | ||
| userId: user._id, | ||
| expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30) | ||
| }); | ||
|
|
||
| workspace = await Workspace.create({ | ||
| name: 'Test Workspace', | ||
| ownerId: user._id, | ||
| apiKey: 'test-api-key', | ||
| baseUrl: 'https://example.com', | ||
| members: [ | ||
| { userId: user._id, roles: ['owner'] }, | ||
| { userId: memberUser._id, roles: ['member'] } | ||
| ], | ||
| subscriptionTier: 'pro' | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(async function() { | ||
| await AccessToken.deleteMany({}); | ||
| await User.deleteMany({}); | ||
| await Workspace.deleteMany({}); | ||
| }); | ||
|
|
||
| it('updates the role of a workspace member', async function() { | ||
| const result = await updateWorkspaceMember({ | ||
| authorization: accessToken._id.toString(), | ||
| workspaceId: workspace._id, | ||
| userId: memberUser._id, | ||
| role: 'admin' | ||
| }); | ||
|
|
||
| assert.ok(result.workspace); | ||
|
|
||
| const updatedMember = result.workspace.members.find(member => member.userId.toString() === memberUser._id.toString()); | ||
| assert.deepStrictEqual(updatedMember.roles, ['admin']); | ||
|
|
||
| const workspaceInDb = await Workspace.findById(workspace._id); | ||
| const updatedMemberInDb = workspaceInDb.members.find(member => member.userId.toString() === memberUser._id.toString()); | ||
| assert.deepStrictEqual(updatedMemberInDb.roles, ['admin']); | ||
| }); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new action sets
member.rolesand saves the workspace without checking whether the workspace has a paid subscription or available seats when elevating someone from a freedashboardsrole toadmin/member.inviteToWorkspaceexplicitly blocks adding paid members on non‑pro plans (subscriptionTier !== 'pro'andnumPaidUsers > 0), but this update path skips that validation, so an admin can invite a user as a free dashboards member and immediately promote them to a paid role without triggering any billing enforcement or Stripe provisioning.Useful? React with 👍 / 👎.