@@ -14,7 +14,6 @@ import type { NextRequest } from 'next/server'
1414import { getUserOrganization } from '@/lib/billing/organizations/membership'
1515import { PlatformEvents } from '@/lib/core/telemetry'
1616import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment'
17- import { PERMISSION_RANK , type PermissionLevel } from '@/lib/invitations/core'
1817import { cancelPendingInvitation , sendWorkspaceAddedEmail } from '@/lib/invitations/send'
1918import { captureServerEvent } from '@/lib/posthog/server'
2019import type { PermissionType } from '@/lib/workspaces/permissions/utils'
@@ -23,7 +22,6 @@ const logger = createLogger('InvitationDirectGrant')
2322
2423export type DirectGrantOutcome =
2524 | { outcome : 'added' ; permission : PermissionType }
26- | { outcome : 'upgraded' ; from : PermissionType ; to : PermissionType }
2725 | { outcome : 'unchanged' ; permission : PermissionType }
2826
2927export interface GrantWorkspaceAccessDirectlyInput {
@@ -89,15 +87,14 @@ async function supersedePendingWorkspaceInvites(
8987/**
9088 * Grants a user workspace access immediately, without an invitation or
9189 * acceptance step. Intended for users who already belong to the workspace's
92- * organization. Idempotent: no-ops when the user already has equal or higher
93- * access, upgrades when the new permission is higher.
90+ * organization and are not yet members of the workspace. Idempotent: when a
91+ * permission already exists it is left untouched (no-op) — invites never modify
92+ * or upgrade an existing member's permission.
9493 */
9594export async function grantWorkspaceAccessDirectly (
9695 input : GrantWorkspaceAccessDirectlyInput
9796) : Promise < DirectGrantOutcome > {
9897 const normalizedEmail = normalizeEmail ( input . email )
99- const newPermission = input . permission as PermissionLevel
100- const newRank = PERMISSION_RANK [ newPermission ] ?? 0
10198
10299 const result = await db . transaction ( async ( tx ) : Promise < DirectGrantOutcome > => {
103100 const [ existing ] = await tx
@@ -112,33 +109,29 @@ export async function grantWorkspaceAccessDirectly(
112109 )
113110 . limit ( 1 )
114111
115- if ( ! existing ) {
116- await tx
117- . insert ( permissions )
118- . values ( {
119- id : generateId ( ) ,
120- entityType : 'workspace' ,
121- entityId : input . workspaceId ,
122- userId : input . userId ,
123- permissionType : newPermission ,
124- createdAt : new Date ( ) ,
125- updatedAt : new Date ( ) ,
126- } )
127- . onConflictDoNothing ( )
128- return { outcome : 'added' , permission : input . permission }
112+ if ( existing ) {
113+ return { outcome : 'unchanged' , permission : existing . permissionType as PermissionType }
129114 }
130115
131- const existingPermission = existing . permissionType as PermissionType
132- const existingRank = PERMISSION_RANK [ existingPermission as PermissionLevel ] ?? 0
133- if ( newRank > existingRank ) {
134- await tx
135- . update ( permissions )
136- . set ( { permissionType : newPermission , updatedAt : new Date ( ) } )
137- . where ( eq ( permissions . id , existing . id ) )
138- return { outcome : 'upgraded' , from : existingPermission , to : input . permission }
116+ const inserted = await tx
117+ . insert ( permissions )
118+ . values ( {
119+ id : generateId ( ) ,
120+ entityType : 'workspace' ,
121+ entityId : input . workspaceId ,
122+ userId : input . userId ,
123+ permissionType : input . permission ,
124+ createdAt : new Date ( ) ,
125+ updatedAt : new Date ( ) ,
126+ } )
127+ . onConflictDoNothing ( )
128+ . returning ( { id : permissions . id } )
129+
130+ if ( inserted . length === 0 ) {
131+ return { outcome : 'unchanged' , permission : input . permission }
139132 }
140133
141- return { outcome : 'unchanged ' , permission : existingPermission }
134+ return { outcome : 'added ' , permission : input . permission }
142135 } )
143136
144137 if ( result . outcome === 'unchanged' ) {
@@ -182,7 +175,6 @@ export async function grantWorkspaceAccessDirectly(
182175 addedBy : input . actorId ,
183176 addedUserId : input . userId ,
184177 role : input . permission ,
185- outcome : result . outcome ,
186178 } )
187179 } catch {
188180 /**
@@ -196,7 +188,6 @@ export async function grantWorkspaceAccessDirectly(
196188 {
197189 workspace_id : input . workspaceId ,
198190 member_role : input . permission ,
199- outcome : result . outcome ,
200191 } ,
201192 {
202193 groups : { workspace : input . workspaceId } ,
@@ -212,17 +203,13 @@ export async function grantWorkspaceAccessDirectly(
212203 resourceType : AuditResourceType . WORKSPACE ,
213204 resourceId : input . workspaceId ,
214205 resourceName : normalizedEmail ,
215- description :
216- result . outcome === 'upgraded'
217- ? `Added existing organization member ${ normalizedEmail } (upgraded to ${ input . permission } )`
218- : `Added existing organization member ${ normalizedEmail } as ${ input . permission } ` ,
206+ description : `Added existing organization member ${ normalizedEmail } as ${ input . permission } ` ,
219207 metadata : {
220208 targetEmail : normalizedEmail ,
221209 targetRole : input . permission ,
222210 organizationId : input . organizationId ,
223211 workspaceName : input . workspaceName ,
224212 addedUserId : input . userId ,
225- outcome : result . outcome ,
226213 } ,
227214 request : input . request ,
228215 } )
0 commit comments