Adding promoting to owner
This commit is contained in:
+7
-1
@@ -3390,7 +3390,7 @@ app.post('/api/workspace/members', requireAuth, requireWriteAccess, requireWorks
|
||||
});
|
||||
|
||||
app.put('/api/workspace/members/:memberId', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant']), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const parsed = z.object({ role: workspaceRoleSchema.exclude(['owner']) }).safeParse(req.body);
|
||||
const parsed = z.object({ role: workspaceRoleSchema }).safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: 'Invalid flock member role payload', details: parsed.error.flatten() });
|
||||
@@ -3400,6 +3400,12 @@ app.put('/api/workspace/members/:memberId', requireAuth, requireWriteAccess, req
|
||||
try {
|
||||
const billingEmail = req.auth!.workspace.billing_email ? normalizeEmail(req.auth!.workspace.billing_email) : '';
|
||||
const requesterIsBillingOwner = Boolean(billingEmail && billingEmail === normalizeEmail(req.auth!.user.email));
|
||||
|
||||
if (parsed.data.role === 'owner' && !requesterIsBillingOwner) {
|
||||
res.status(403).json({ error: 'Only the billing owner can promote collaborators to owner.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await updateWorkspaceMemberRole({
|
||||
memberId: req.params.memberId,
|
||||
workspaceId: req.auth!.workspace.id,
|
||||
|
||||
@@ -415,6 +415,57 @@ test('updateWorkspaceMemberRole does not let a non-billing owner change the bill
|
||||
assert.equal(member, null);
|
||||
});
|
||||
|
||||
test('updateWorkspaceMemberRole lets the billing owner promote a non-owner to owner', async () => {
|
||||
const { calls } = mockDb({
|
||||
rowCount: 1,
|
||||
rows: [
|
||||
{
|
||||
id: 'member-1',
|
||||
workspace_id: 42,
|
||||
user_id: 'user-2',
|
||||
invite_email: 'helper@example.com',
|
||||
name: 'Helper',
|
||||
role: 'owner',
|
||||
accepted_at: '2026-04-14T00:00:00.000Z',
|
||||
created_at: '2026-04-14T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const member = await updateWorkspaceMemberRole({
|
||||
memberId: 'member-1',
|
||||
workspaceId: 42,
|
||||
role: 'owner',
|
||||
requesterMemberId: 'billing-owner',
|
||||
requesterIsBillingOwner: true,
|
||||
requesterRole: 'owner',
|
||||
billingEmail: 'billing@example.com',
|
||||
});
|
||||
|
||||
assert.equal(member?.role, 'owner');
|
||||
assert.deepEqual(calls[0].params, ['member-1', 42, 'owner', true, 'billing-owner', 'billing@example.com', 'owner']);
|
||||
assert.match(calls[0].text, /\$3 <> 'owner'/);
|
||||
});
|
||||
|
||||
test('updateWorkspaceMemberRole does not let a non-billing owner promote a member to owner', async () => {
|
||||
mockDb({
|
||||
rowCount: 0,
|
||||
rows: [],
|
||||
});
|
||||
|
||||
const member = await updateWorkspaceMemberRole({
|
||||
memberId: 'member-1',
|
||||
workspaceId: 42,
|
||||
role: 'owner',
|
||||
requesterMemberId: 'non-billing-owner',
|
||||
requesterIsBillingOwner: false,
|
||||
requesterRole: 'owner',
|
||||
billingEmail: 'billing@example.com',
|
||||
});
|
||||
|
||||
assert.equal(member, null);
|
||||
});
|
||||
|
||||
test('deleteWorkspaceMember removes non-owner members without billing owner access', async () => {
|
||||
const { calls } = mockDb({
|
||||
rowCount: 1,
|
||||
|
||||
@@ -415,6 +415,10 @@ export const updateWorkspaceMemberRole = async ({
|
||||
SET role = $3
|
||||
WHERE id = $1
|
||||
AND workspace_id = $2
|
||||
AND (
|
||||
$3 <> 'owner'
|
||||
OR $4 = TRUE
|
||||
)
|
||||
AND (
|
||||
role <> 'owner'
|
||||
OR (
|
||||
|
||||
@@ -7925,6 +7925,7 @@ function App() {
|
||||
})
|
||||
}
|
||||
>
|
||||
{isBillingOwner ? <option value="owner">Owner</option> : null}
|
||||
<option value="assistant">Assistant</option>
|
||||
<option value="caregiver">Caregiver</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
@@ -7949,6 +7950,7 @@ function App() {
|
||||
activeMembership?.role === 'owner' &&
|
||||
member.id !== activeMembership.id &&
|
||||
(isBillingOwner || !memberIsBillingOwner);
|
||||
const canPromoteToOwner = member.role !== 'owner' && isBillingOwner && member.id !== activeMembership?.id;
|
||||
const canRemoveMember = member.role !== 'owner' || canRemoveOwner;
|
||||
|
||||
return (
|
||||
@@ -7969,7 +7971,7 @@ function App() {
|
||||
removingWorkspaceMemberId === member.id
|
||||
}
|
||||
>
|
||||
{member.role === 'owner' ? <option value="owner">Owner</option> : null}
|
||||
{member.role === 'owner' || canPromoteToOwner ? <option value="owner">Owner</option> : null}
|
||||
<option value="assistant">Assistant</option>
|
||||
<option value="caregiver">Caregiver</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
|
||||
Reference in New Issue
Block a user