Adding promoting to owner
This commit is contained in:
+7
-1
@@ -3210,7 +3210,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) => {
|
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) {
|
if (!parsed.success) {
|
||||||
res.status(400).json({ error: 'Invalid flock member role payload', details: parsed.error.flatten() });
|
res.status(400).json({ error: 'Invalid flock member role payload', details: parsed.error.flatten() });
|
||||||
@@ -3220,6 +3220,12 @@ app.put('/api/workspace/members/:memberId', requireAuth, requireWriteAccess, req
|
|||||||
try {
|
try {
|
||||||
const billingEmail = req.auth!.workspace.billing_email ? normalizeEmail(req.auth!.workspace.billing_email) : '';
|
const billingEmail = req.auth!.workspace.billing_email ? normalizeEmail(req.auth!.workspace.billing_email) : '';
|
||||||
const requesterIsBillingOwner = Boolean(billingEmail && billingEmail === normalizeEmail(req.auth!.user.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({
|
const member = await updateWorkspaceMemberRole({
|
||||||
memberId: req.params.memberId,
|
memberId: req.params.memberId,
|
||||||
workspaceId: req.auth!.workspace.id,
|
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);
|
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 () => {
|
test('deleteWorkspaceMember removes non-owner members without billing owner access', async () => {
|
||||||
const { calls } = mockDb({
|
const { calls } = mockDb({
|
||||||
rowCount: 1,
|
rowCount: 1,
|
||||||
|
|||||||
@@ -415,6 +415,10 @@ export const updateWorkspaceMemberRole = async ({
|
|||||||
SET role = $3
|
SET role = $3
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND workspace_id = $2
|
AND workspace_id = $2
|
||||||
|
AND (
|
||||||
|
$3 <> 'owner'
|
||||||
|
OR $4 = TRUE
|
||||||
|
)
|
||||||
AND (
|
AND (
|
||||||
role <> 'owner'
|
role <> 'owner'
|
||||||
OR (
|
OR (
|
||||||
|
|||||||
@@ -7400,6 +7400,7 @@ function App() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{isBillingOwner ? <option value="owner">Owner</option> : null}
|
||||||
<option value="assistant">Assistant</option>
|
<option value="assistant">Assistant</option>
|
||||||
<option value="caregiver">Caregiver</option>
|
<option value="caregiver">Caregiver</option>
|
||||||
<option value="viewer">Viewer</option>
|
<option value="viewer">Viewer</option>
|
||||||
@@ -7424,6 +7425,7 @@ function App() {
|
|||||||
activeMembership?.role === 'owner' &&
|
activeMembership?.role === 'owner' &&
|
||||||
member.id !== activeMembership.id &&
|
member.id !== activeMembership.id &&
|
||||||
(isBillingOwner || !memberIsBillingOwner);
|
(isBillingOwner || !memberIsBillingOwner);
|
||||||
|
const canPromoteToOwner = member.role !== 'owner' && isBillingOwner && member.id !== activeMembership?.id;
|
||||||
const canRemoveMember = member.role !== 'owner' || canRemoveOwner;
|
const canRemoveMember = member.role !== 'owner' || canRemoveOwner;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -7444,7 +7446,7 @@ function App() {
|
|||||||
removingWorkspaceMemberId === member.id
|
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="assistant">Assistant</option>
|
||||||
<option value="caregiver">Caregiver</option>
|
<option value="caregiver">Caregiver</option>
|
||||||
<option value="viewer">Viewer</option>
|
<option value="viewer">Viewer</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user