Skip to content

Commit 4f20c77

Browse files
committed
fix(credentials): match the last-admin guard to the server
1 parent 339ac91 commit 4f20c77

5 files changed

Lines changed: 20 additions & 5 deletions

File tree

‎apps/sim/app/api/credentials/[id]/members/route.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ describe('/api/credentials/[id]/members compatibility', () => {
7575
joinedAt: new Date('2026-08-02T00:00:00.000Z'),
7676
userName: 'Member',
7777
userEmail: 'member@example.com',
78+
explicitAdmin: false,
7879
},
7980
])
8081
})

‎apps/sim/app/api/v2/credentials/[credentialId]/members/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const GET = defineV2JsonRoute({
3939
}),
4040
useCase: listCredentialMembersUseCase,
4141
present: ({ members, nextCursorKeys }, { params, query }) => ({
42-
data: members.map((member) => ({
42+
data: members.map(({ explicitAdmin, ...member }) => ({
4343
...member,
4444
joinedAt: member.joinedAt?.toISOString() ?? null,
4545
})),

‎apps/sim/app/workspace/[workspaceId]/components/credential-detail/components/credential-members-section.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,7 @@ export function CredentialMembersSection({
5555

5656
const viewerId = session?.user?.id
5757
const activeMembers = members.filter((member) => member.status === 'active')
58-
const explicitAdminCount = activeMembers.filter(
59-
(member) => member.role === 'admin' && member.roleSource !== 'workspace-admin'
60-
).length
58+
const explicitAdminCount = activeMembers.filter((member) => member.explicitAdmin).length
6159
const showActions = isAdmin || activeMembers.some((member) => member.userId === viewerId)
6260

6361
const handleChangeMemberRole = async (userId: string, role: MemberRole) => {

‎apps/sim/lib/api/contracts/credentials.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ export const workspaceCredentialMemberSchema = z.object({
365365
userImage: z.string().nullable().optional(),
366366
/** `workspace-admin` roles are derived from workspace admin and cannot be changed. */
367367
roleSource: z.enum(['explicit', 'workspace-admin']).optional(),
368+
/** Holds an active explicit admin grant; the last-admin guards count these, not `role`. */
369+
explicitAdmin: z.boolean(),
368370
})
369371

370372
export type WorkspaceCredentialMember = z.output<typeof workspaceCredentialMemberSchema>

‎apps/sim/lib/credentials/members.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export interface CredentialMemberView {
3030
userEmail: string | null
3131
userImage: string | null
3232
roleSource: 'explicit' | 'workspace-admin'
33+
/**
34+
* Whether the user holds an active explicit admin grant, independent of any
35+
* inherited workspace-admin role. This is what the last-admin guards count.
36+
*/
37+
explicitAdmin: boolean
3338
}
3439

3540
export async function listCredentialMembers(
@@ -52,7 +57,14 @@ export async function listCredentialMembers(
5257
.where(eq(credentialMember.credentialId, credential.id))
5358

5459
const byUser = new Map<string, CredentialMemberView>(
55-
explicitMembers.map((member) => [member.userId, { ...member, roleSource: 'explicit' as const }])
60+
explicitMembers.map((member) => [
61+
member.userId,
62+
{
63+
...member,
64+
roleSource: 'explicit' as const,
65+
explicitAdmin: member.role === 'admin' && member.status === 'active',
66+
},
67+
])
5668
)
5769

5870
if (isSharedCredentialType(credential.type)) {
@@ -75,6 +87,7 @@ export async function listCredentialMembers(
7587
userEmail: workspaceMember.email,
7688
userImage: workspaceMember.image ?? null,
7789
roleSource: 'workspace-admin',
90+
explicitAdmin: false,
7891
})
7992
}
8093
}
@@ -148,6 +161,7 @@ export async function listCredentialMembersPage(
148161
roleSource: sql<
149162
'explicit' | 'workspace-admin'
150163
>`case when ${inheritedAdmin} then 'workspace-admin' else 'explicit' end`,
164+
explicitAdmin: sql<boolean>`coalesce(${credentialMember.role} = 'admin' and ${credentialMember.status} = 'active', false)`,
151165
})
152166
.from(user)
153167
.leftJoin(

0 commit comments

Comments
 (0)