Conversation
packages/twenty-server/src/engine/core-modules/user/services/user.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Greptile Overview
Greptile Summary
This PR fixes user deletion flows by properly cleaning up workspaceMember, userWorkspace, and roleTarget records. It adds the ability for users to leave individual workspaces while remaining in others, and implements soft-deletion with a 30-day retention period.
Key Changes:
- Added
deleteUserWorkspaceAndPotentiallyDeleteUserto allow users to leave workspaces individually - Introduced soft-delete for users with hard-delete after 30 days via cron job
- Created cleanup commands for orphaned
roleTargetanduserWorkspacerecords - Updated frontend to show "Leave workspace" button when user has multiple workspaces
Critical Issues Found:
- Missing
awaiton async call inuser.service.ts:144will cause incomplete cleanup - Frontend mutation missing required
workspaceMemberIdToDeleteparameter - feature will fail at runtime - Cron job has infinite loop bug -
hasMoreflag never updated - Role targets cleanup command only processes one workspace due to early return
Confidence Score: 1/5
- This PR has critical bugs that will cause runtime failures and data inconsistency
- Score reflects multiple critical logic errors: missing await causing race conditions, missing mutation parameter causing frontend failures, infinite loop in cron job, and broken cleanup command. These issues will prevent the feature from working correctly and could lead to orphaned records.
- Critical attention needed:
user.service.ts(missing await),DeleteAccount.tsx(missing mutation parameter),user-hard-delete.cron.job.ts(infinite loop), and1-11-clean-orphaned-role-targets.command.ts(only processes one workspace)
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| packages/twenty-server/src/engine/core-modules/user/services/user.service.ts | 1/5 | Added user deletion logic with workspace cleanup, but missing await on async call (line 144) which could cause incomplete cleanup |
| packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx | 0/5 | UI for leave workspace feature, but mutation call missing required workspaceMemberIdToDelete parameter |
| packages/twenty-server/src/engine/core-modules/user/crons/user-hard-delete.cron.job.ts | 2/5 | Cron job for hard-deleting soft-deleted users after 30 days, but hasMore flag never updated causing infinite loop |
| packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command.ts | 2/5 | Cleanup command for orphaned role targets, but hasRunOnce flag prevents processing multiple workspaces |
Sequence Diagram
sequenceDiagram
participant User
participant Frontend
participant UserResolver
participant UserService
participant UserWorkspaceService
participant WorkspaceMemberRepo
participant UserWorkspaceRepo
participant RoleTargetsRepo
participant WorkspaceService
alt User has multiple workspaces - Leave Workspace
User->>Frontend: Click "Leave workspace"
Frontend->>UserResolver: deleteUserFromWorkspace(workspaceMemberIdToDelete)
UserResolver->>UserResolver: Validate permissions
UserResolver->>UserService: deleteUserWorkspaceAndPotentiallyDeleteUser()
UserService->>UserService: Load user with userWorkspaces
UserService->>UserService: Find userWorkspace for workspaceId
UserService->>UserService: removeUserFromWorkspaceAndPotentiallyDeleteWorkspace()
UserService->>WorkspaceMemberRepo: delete(userId)
UserService->>UserWorkspaceService: deleteUserWorkspace(userWorkspaceId)
UserWorkspaceService->>RoleTargetsRepo: delete(userWorkspaceId)
UserWorkspaceService->>UserWorkspaceRepo: delete(id)
alt User has only 1 workspace
UserService->>UserService: softDelete(userId)
end
else User deletes account entirely
User->>Frontend: Click "Delete account"
Frontend->>UserResolver: deleteUser()
UserResolver->>UserService: deleteUser(userId)
UserService->>UserService: Load user with userWorkspaces
loop For each userWorkspace
UserService->>UserService: removeUserFromWorkspaceAndPotentiallyDeleteWorkspace()
UserService->>WorkspaceMemberRepo: delete(userId)
UserService->>UserWorkspaceService: deleteUserWorkspace(userWorkspaceId)
UserWorkspaceService->>RoleTargetsRepo: delete(userWorkspaceId)
UserWorkspaceService->>UserWorkspaceRepo: delete(id)
alt Last member in workspace
UserService->>WorkspaceService: deleteWorkspace(workspaceId)
end
end
UserService->>UserService: softDelete(userId)
end
Note over UserService: After 30 days
UserHardDeleteCronJob->>UserWorkspaceRepo: Find soft-deleted users older than 30 days
UserHardDeleteCronJob->>UserWorkspaceRepo: Hard delete users
27 files reviewed, 5 comments
|
|
||
| if (workspaceMembers.length === 1) { | ||
| await this.workspaceService.deleteWorkspace(workspaceId); | ||
| this.removeUserFromWorkspaceAndPotentiallyDeleteWorkspace(userWorkspace); |
There was a problem hiding this comment.
logic: missing await - removeUserFromWorkspaceAndPotentiallyDeleteWorkspace is async but not awaited
| this.removeUserFromWorkspaceAndPotentiallyDeleteWorkspace(userWorkspace); | |
| await this.removeUserFromWorkspaceAndPotentiallyDeleteWorkspace(userWorkspace); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-server/src/engine/core-modules/user/services/user.service.ts
Line: 144:144
Comment:
**logic:** missing `await` - `removeUserFromWorkspaceAndPotentiallyDeleteWorkspace` is async but not awaited
```suggestion
await this.removeUserFromWorkspaceAndPotentiallyDeleteWorkspace(userWorkspace);
```
How can I resolve this? If you propose a fix, please make it concise.| let totalDeleted = 0; | ||
|
|
||
| let hasMore = true; | ||
|
|
There was a problem hiding this comment.
logic: hasMore variable is set but never used - the while loop will run indefinitely
| while (hasMore) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-server/src/engine/core-modules/user/crons/user-hard-delete.cron.job.ts
Line: 36:36
Comment:
**logic:** `hasMore` variable is set but never used - the while loop will run indefinitely
```suggestion
while (hasMore) {
```
How can I resolve this? If you propose a fix, please make it concise.| if (usersToDelete.length === 0) break; | ||
|
|
||
| await this.userRepository.delete({ | ||
| id: In(usersToDelete.map((user) => user.id)), | ||
| }); | ||
| totalDeleted += usersToDelete.length; |
There was a problem hiding this comment.
logic: hasMore should be updated after determining batch size
| if (usersToDelete.length === 0) break; | |
| await this.userRepository.delete({ | |
| id: In(usersToDelete.map((user) => user.id)), | |
| }); | |
| totalDeleted += usersToDelete.length; | |
| if (usersToDelete.length === 0) { | |
| hasMore = false; | |
| break; | |
| } | |
| await this.userRepository.delete({ | |
| id: In(usersToDelete.map((user) => user.id)), | |
| }); | |
| totalDeleted += usersToDelete.length; | |
| hasMore = usersToDelete.length === BATCH_SIZE; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-server/src/engine/core-modules/user/crons/user-hard-delete.cron.job.ts
Line: 47:52
Comment:
**logic:** `hasMore` should be updated after determining batch size
```suggestion
if (usersToDelete.length === 0) {
hasMore = false;
break;
}
await this.userRepository.delete({
id: In(usersToDelete.map((user) => user.id)),
});
totalDeleted += usersToDelete.length;
hasMore = usersToDelete.length === BATCH_SIZE;
```
How can I resolve this? If you propose a fix, please make it concise.| if (this.hasRunOnce) { | ||
| this.logger.log('Skipping kanban field metadata id foreign key creation'); | ||
|
|
||
| return; | ||
| } |
There was a problem hiding this comment.
logic: early return prevents command from processing multiple workspaces - only first workspace will be processed
| if (this.hasRunOnce) { | |
| this.logger.log('Skipping kanban field metadata id foreign key creation'); | |
| return; | |
| } | |
| const isDryRun = options.dryRun || false; | |
| if (isDryRun && this.hasRunOnce) { | |
| this.logger.log('Dry run already executed, skipping'); | |
| return; | |
| } | |
| if (!isDryRun && this.hasRunOnce) { | |
| this.logger.log('Command already executed, skipping'); | |
| return; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command.ts
Line: 35:39
Comment:
**logic:** early return prevents command from processing multiple workspaces - only first workspace will be processed
```suggestion
const isDryRun = options.dryRun || false;
if (isDryRun && this.hasRunOnce) {
this.logger.log('Dry run already executed, skipping');
return;
}
if (!isDryRun && this.hasRunOnce) {
this.logger.log('Command already executed, skipping');
return;
}
```
How can I resolve this? If you propose a fix, please make it concise.| const leaveWorkspace = async () => { | ||
| await deleteUserFromWorkspace(); | ||
| await signOut(); | ||
| }; |
There was a problem hiding this comment.
logic: missing required argument workspaceMemberIdToDelete for mutation - backend expects this parameter but frontend doesn't provide it
Check backend mutation signature:
deleteUserFromWorkspace(
@Args('workspaceMemberIdToDelete') workspaceMemberIdToDelete: string,
...
)Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx
Line: 45:48
Comment:
**logic:** missing required argument `workspaceMemberIdToDelete` for mutation - backend expects this parameter but frontend doesn't provide it
Check backend mutation signature:
```typescript
deleteUserFromWorkspace(
@Args('workspaceMemberIdToDelete') workspaceMemberIdToDelete: string,
...
)
```
How can I resolve this? If you propose a fix, please make it concise.|
🚀 Preview Environment Ready! Your preview environment is available at: http://bore.pub:37301 This environment will automatically shut down when the PR is closed or after 5 hours. |
| fieldName: string, | ||
| fieldMetadataSettings: FieldMetadataRelationSettings, | ||
| ): string => { | ||
| if (fieldMetadataSettings.relationType === RelationType.ONE_TO_MANY) { |
There was a problem hiding this comment.
forgotten from another pr
Weiko
left a comment
There was a problem hiding this comment.
Left some comments otherwise LGTM
...c/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command.ts
Outdated
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command.ts
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-modules/user/services/user.service.ts
Show resolved
Hide resolved
| const hasReadPermission = objectPermissions.canReadObjectRecords; | ||
|
|
||
| const { data, loading, error, fetchMore } = | ||
| const { data, loading, error, fetchMore, refetch } = |
There was a problem hiding this comment.
I'm not sure about exposing the refetch for useFindMany for this new use case only 🤔 Could we use optimistic update / apollo cache mutation instead?
There was a problem hiding this comment.
why not? by fear it would be use in the wrong context?
There was a problem hiding this comment.
There's dedicated tooling for fetchMore and doing a refetch like that has not been QAed, so I would not expose this method.
|
@Bonapara @StephanieJoly4 @FelixMalfait |
Before
So the problems are
Now
Next
Fixes #14608