From 5db7e004c3fdd4bd3555036a441bfc13c04e6c00 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:34:07 +0000 Subject: [PATCH 01/11] feat(dashboard-api): add admin DELETE /admin/users/{userId} route Adds an internal admin endpoint to delete a user through the user profile provider. The handler: 1. Deletes the identity provider record (Ory identity via SDK). 2. Deletes from public.users (cascades to user_identities via FK). The Provider interface gains a DeleteUser method implemented by both the Ory and Supabase backends (Supabase returns unsupported). Co-Authored-By: ben@e2b.dev --- .../dashboard-api/internal/api/api.gen.go | 170 +++++++++++------- .../internal/handlers/admin_users_delete.go | 33 ++++ .../dashboard-api/internal/userprofile/ory.go | 25 +++ .../internal/userprofile/provider.go | 1 + .../internal/userprofile/supabase.go | 4 + spec/openapi-dashboard.yml | 21 +++ 6 files changed, 185 insertions(+), 69 deletions(-) create mode 100644 packages/dashboard-api/internal/handlers/admin_users_delete.go diff --git a/packages/dashboard-api/internal/api/api.gen.go b/packages/dashboard-api/internal/api/api.gen.go index dfb028466a..0f0ed70ea7 100644 --- a/packages/dashboard-api/internal/api/api.gen.go +++ b/packages/dashboard-api/internal/api/api.gen.go @@ -615,6 +615,9 @@ type ServerInterface interface { // Bootstrap auth provider user // (POST /admin/users/bootstrap) PostAdminUsersBootstrap(c *gin.Context) + // Delete user + // (DELETE /admin/users/{userId}) + DeleteAdminUsersUserId(c *gin.Context, userId UserId) // Bootstrap user // (POST /admin/users/{userId}/bootstrap) PostAdminUsersUserIdBootstrap(c *gin.Context, userId UserId) @@ -758,6 +761,33 @@ func (siw *ServerInterfaceWrapper) PostAdminUsersBootstrap(c *gin.Context) { siw.Handler.PostAdminUsersBootstrap(c) } +// DeleteAdminUsersUserId operation middleware +func (siw *ServerInterfaceWrapper) DeleteAdminUsersUserId(c *gin.Context) { + + var err error + _ = err + + // ------------- Path parameter "userId" ------------- + var userId UserId + + err = runtime.BindStyledParameterWithOptions("simple", "userId", c.Param("userId"), &userId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter userId: %w", err), http.StatusBadRequest) + return + } + + c.Set(string(AdminApiKeyAuthScopes), []string{}) + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.DeleteAdminUsersUserId(c, userId) +} + // PostAdminUsersUserIdBootstrap operation middleware func (siw *ServerInterfaceWrapper) PostAdminUsersUserIdBootstrap(c *gin.Context) { @@ -1296,6 +1326,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.POST(options.BaseURL+"/admin/user-profiles/resolve", wrapper.PostAdminUserProfilesResolve) router.GET(options.BaseURL+"/admin/user-profiles/:userId", wrapper.GetAdminUserProfilesUserId) router.POST(options.BaseURL+"/admin/users/bootstrap", wrapper.PostAdminUsersBootstrap) + router.DELETE(options.BaseURL+"/admin/users/:userId", wrapper.DeleteAdminUsersUserId) router.POST(options.BaseURL+"/admin/users/:userId/bootstrap", wrapper.PostAdminUsersUserIdBootstrap) router.GET(options.BaseURL+"/builds", wrapper.GetBuilds) router.GET(options.BaseURL+"/builds/statuses", wrapper.GetBuildsStatuses) @@ -1318,75 +1349,76 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/packages/dashboard-api/internal/handlers/admin_users_delete.go b/packages/dashboard-api/internal/handlers/admin_users_delete.go new file mode 100644 index 0000000000..f2a5c2def2 --- /dev/null +++ b/packages/dashboard-api/internal/handlers/admin_users_delete.go @@ -0,0 +1,33 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "github.com/e2b-dev/infra/packages/dashboard-api/internal/api" + "github.com/e2b-dev/infra/packages/shared/pkg/logger" +) + +func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { + ctx := c.Request.Context() + + // Delete the identity provider record (e.g. Ory identity). + if err := s.userProfiles.DeleteUser(ctx, userId); err != nil { + logger.L().Error(ctx, "failed to delete user identity provider record", zap.String("user_id", userId.String()), zap.Error(err)) + s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete user identity provider record") + + return + } + + // Delete from public.users (cascades to user_identities via FK). + if err := s.authDB.Write.DeletePublicUser(ctx, userId); err != nil { + logger.L().Error(ctx, "failed to delete public user", zap.String("user_id", userId.String()), zap.Error(err)) + s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete public user record") + + return + } + + c.Status(http.StatusNoContent) +} diff --git a/packages/dashboard-api/internal/userprofile/ory.go b/packages/dashboard-api/internal/userprofile/ory.go index b39fbcfab0..cb87620fc6 100644 --- a/packages/dashboard-api/internal/userprofile/ory.go +++ b/packages/dashboard-api/internal/userprofile/ory.go @@ -179,6 +179,31 @@ func (p *oryProvider) GetTeamCreatorContext(ctx context.Context, userID uuid.UUI return creatorContextFromOryIdentity(identities[0]), nil } +func (p *oryProvider) DeleteUser(ctx context.Context, userID uuid.UUID) error { + if userID == uuid.Nil { + return fmt.Errorf("user id is required") + } + + userIDBySubject, err := p.subjectsForUserIDs(ctx, []uuid.UUID{userID}) + if err != nil { + return fmt.Errorf("lookup ory subject for user: %w", err) + } + + for subject := range userIDBySubject { + resp, err := p.identities.DeleteIdentityExecute( + p.identities.DeleteIdentity(p.authCtx(ctx), subject), + ) + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + if err != nil { + return fmt.Errorf("delete ory identity %s: %w", subject, err) + } + } + + return nil +} + func (p *oryProvider) subjectsForUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[string]uuid.UUID, error) { rows, err := p.resolver.GetUserIdentitiesByUserIDs(ctx, authqueries.GetUserIdentitiesByUserIDsParams{ OidcIss: p.issuer, diff --git a/packages/dashboard-api/internal/userprofile/provider.go b/packages/dashboard-api/internal/userprofile/provider.go index a27465d856..77b23b44af 100644 --- a/packages/dashboard-api/internal/userprofile/provider.go +++ b/packages/dashboard-api/internal/userprofile/provider.go @@ -21,6 +21,7 @@ type Provider interface { GetProfilesByUserID(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID]Profile, error) FindProfilesByEmail(ctx context.Context, email string) ([]Profile, error) GetTeamCreatorContext(ctx context.Context, userID uuid.UUID) (*sharedteamprovision.CreatorContextV1, error) + DeleteUser(ctx context.Context, userID uuid.UUID) error } func NewProvider(mode Mode, supa Provider, ory Provider) (Provider, error) { diff --git a/packages/dashboard-api/internal/userprofile/supabase.go b/packages/dashboard-api/internal/userprofile/supabase.go index fb6805f5e1..adae7b8f02 100644 --- a/packages/dashboard-api/internal/userprofile/supabase.go +++ b/packages/dashboard-api/internal/userprofile/supabase.go @@ -65,6 +65,10 @@ func (p *supabaseProvider) FindProfilesByEmail(ctx context.Context, email string return profiles, nil } +func (p *supabaseProvider) DeleteUser(_ context.Context, _ uuid.UUID) error { + return fmt.Errorf("user deletion is not supported in supabase mode") +} + func (p *supabaseProvider) GetTeamCreatorContext(ctx context.Context, userID uuid.UUID) (*sharedteamprovision.CreatorContextV1, error) { if userID == uuid.Nil { return nil, nil diff --git a/spec/openapi-dashboard.yml b/spec/openapi-dashboard.yml index 94594e0bb4..ce20a18515 100644 --- a/spec/openapi-dashboard.yml +++ b/spec/openapi-dashboard.yml @@ -1202,6 +1202,27 @@ paths: "500": $ref: "#/components/responses/500" + /admin/users/{userId}: + delete: + summary: Delete user + description: Deletes a user by removing the identity provider record, user_identities mapping, and public.users row. + tags: [admin] + security: + - AdminApiKeyAuth: [] + parameters: + - $ref: "#/components/parameters/userId" + responses: + "204": + description: Successfully deleted user. + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + /teams/resolve: get: summary: Resolve team identity From c39d9a3f59e9150d2b57fd831678949500604639 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:38:10 +0000 Subject: [PATCH 02/11] fix: add DeleteUser to fakeProvider in test Co-Authored-By: ben@e2b.dev --- packages/dashboard-api/internal/userprofile/provider_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/dashboard-api/internal/userprofile/provider_test.go b/packages/dashboard-api/internal/userprofile/provider_test.go index 0c696b5135..8c3552f2e1 100644 --- a/packages/dashboard-api/internal/userprofile/provider_test.go +++ b/packages/dashboard-api/internal/userprofile/provider_test.go @@ -23,6 +23,10 @@ func (f *fakeProvider) GetTeamCreatorContext(_ context.Context, _ uuid.UUID) (*s return nil, nil } +func (f *fakeProvider) DeleteUser(_ context.Context, _ uuid.UUID) error { + return nil +} + func TestNewProvider_FactorySelection(t *testing.T) { t.Parallel() From 9001703b52384f53890626e5007a46426241f2a4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:42:18 +0000 Subject: [PATCH 03/11] fix: use errors.New instead of fmt.Errorf for static strings (perfsprint) Co-Authored-By: ben@e2b.dev --- packages/dashboard-api/internal/userprofile/ory.go | 2 +- packages/dashboard-api/internal/userprofile/supabase.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/dashboard-api/internal/userprofile/ory.go b/packages/dashboard-api/internal/userprofile/ory.go index cb87620fc6..f76bef5372 100644 --- a/packages/dashboard-api/internal/userprofile/ory.go +++ b/packages/dashboard-api/internal/userprofile/ory.go @@ -181,7 +181,7 @@ func (p *oryProvider) GetTeamCreatorContext(ctx context.Context, userID uuid.UUI func (p *oryProvider) DeleteUser(ctx context.Context, userID uuid.UUID) error { if userID == uuid.Nil { - return fmt.Errorf("user id is required") + return errors.New("user id is required") } userIDBySubject, err := p.subjectsForUserIDs(ctx, []uuid.UUID{userID}) diff --git a/packages/dashboard-api/internal/userprofile/supabase.go b/packages/dashboard-api/internal/userprofile/supabase.go index adae7b8f02..68c6eb9d28 100644 --- a/packages/dashboard-api/internal/userprofile/supabase.go +++ b/packages/dashboard-api/internal/userprofile/supabase.go @@ -3,6 +3,7 @@ package userprofile import ( "context" "encoding/json" + "errors" "fmt" "strings" @@ -66,7 +67,7 @@ func (p *supabaseProvider) FindProfilesByEmail(ctx context.Context, email string } func (p *supabaseProvider) DeleteUser(_ context.Context, _ uuid.UUID) error { - return fmt.Errorf("user deletion is not supported in supabase mode") + return errors.New("user deletion is not supported in supabase mode") } func (p *supabaseProvider) GetTeamCreatorContext(ctx context.Context, userID uuid.UUID) (*sharedteamprovision.CreatorContextV1, error) { From 5234fc93e105718d4bc00011afd392bdbc3508ae Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:39:05 +0000 Subject: [PATCH 04/11] =?UTF-8?q?fix:=20two-phase=20user=20deletion=20?= =?UTF-8?q?=E2=80=94=20resolve=20identity=20before=20DB=20delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PrepareDeleteUser looks up the Ory subject while user_identities rows still exist, then Execute removes the Ory identity after the DB rows are gone. Prevents orphaned state if DeletePublicUser fails on FK constraints. Co-Authored-By: ben@e2b.dev --- .../internal/handlers/admin_users_delete.go | 18 +++++++++--- .../dashboard-api/internal/userprofile/ory.go | 28 ++++++++++++++----- .../internal/userprofile/provider.go | 12 +++++++- .../internal/userprofile/provider_test.go | 4 +-- .../internal/userprofile/supabase.go | 8 ++++-- 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/packages/dashboard-api/internal/handlers/admin_users_delete.go b/packages/dashboard-api/internal/handlers/admin_users_delete.go index f2a5c2def2..c11fd6aa4e 100644 --- a/packages/dashboard-api/internal/handlers/admin_users_delete.go +++ b/packages/dashboard-api/internal/handlers/admin_users_delete.go @@ -13,15 +13,17 @@ import ( func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { ctx := c.Request.Context() - // Delete the identity provider record (e.g. Ory identity). - if err := s.userProfiles.DeleteUser(ctx, userId); err != nil { - logger.L().Error(ctx, "failed to delete user identity provider record", zap.String("user_id", userId.String()), zap.Error(err)) - s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete user identity provider record") + // Resolve the external identity references while user_identities still exists. + handle, err := s.userProfiles.PrepareDeleteUser(ctx, userId) + if err != nil { + logger.L().Error(ctx, "failed to prepare user deletion", zap.String("user_id", userId.String()), zap.Error(err)) + s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to prepare user deletion") return } // Delete from public.users (cascades to user_identities via FK). + // Done before the IdP removal so a DB failure does not orphan the identity. if err := s.authDB.Write.DeletePublicUser(ctx, userId); err != nil { logger.L().Error(ctx, "failed to delete public user", zap.String("user_id", userId.String()), zap.Error(err)) s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete public user record") @@ -29,5 +31,13 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { return } + // Remove the external identity (e.g. Ory) using pre-fetched references. + if err := handle.Execute(ctx); err != nil { + logger.L().Error(ctx, "failed to delete user identity provider record", zap.String("user_id", userId.String()), zap.Error(err)) + s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete user identity provider record") + + return + } + c.Status(http.StatusNoContent) } diff --git a/packages/dashboard-api/internal/userprofile/ory.go b/packages/dashboard-api/internal/userprofile/ory.go index f76bef5372..52690dfd99 100644 --- a/packages/dashboard-api/internal/userprofile/ory.go +++ b/packages/dashboard-api/internal/userprofile/ory.go @@ -179,19 +179,33 @@ func (p *oryProvider) GetTeamCreatorContext(ctx context.Context, userID uuid.UUI return creatorContextFromOryIdentity(identities[0]), nil } -func (p *oryProvider) DeleteUser(ctx context.Context, userID uuid.UUID) error { +func (p *oryProvider) PrepareDeleteUser(ctx context.Context, userID uuid.UUID) (DeleteUserHandle, error) { if userID == uuid.Nil { - return errors.New("user id is required") + return nil, errors.New("user id is required") } - userIDBySubject, err := p.subjectsForUserIDs(ctx, []uuid.UUID{userID}) + subjectsByUser, err := p.subjectsForUserIDs(ctx, []uuid.UUID{userID}) if err != nil { - return fmt.Errorf("lookup ory subject for user: %w", err) + return nil, fmt.Errorf("lookup ory subject for user: %w", err) + } + + subjects := make([]string, 0, len(subjectsByUser)) + for s := range subjectsByUser { + subjects = append(subjects, s) } - for subject := range userIDBySubject { - resp, err := p.identities.DeleteIdentityExecute( - p.identities.DeleteIdentity(p.authCtx(ctx), subject), + return &oryDeleteHandle{provider: p, subjects: subjects}, nil +} + +type oryDeleteHandle struct { + provider *oryProvider + subjects []string +} + +func (h *oryDeleteHandle) Execute(ctx context.Context) error { + for _, subject := range h.subjects { + resp, err := h.provider.identities.DeleteIdentityExecute( + h.provider.identities.DeleteIdentity(h.provider.authCtx(ctx), subject), ) if resp != nil && resp.Body != nil { _ = resp.Body.Close() diff --git a/packages/dashboard-api/internal/userprofile/provider.go b/packages/dashboard-api/internal/userprofile/provider.go index 77b23b44af..c873e07d19 100644 --- a/packages/dashboard-api/internal/userprofile/provider.go +++ b/packages/dashboard-api/internal/userprofile/provider.go @@ -21,7 +21,17 @@ type Provider interface { GetProfilesByUserID(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID]Profile, error) FindProfilesByEmail(ctx context.Context, email string) ([]Profile, error) GetTeamCreatorContext(ctx context.Context, userID uuid.UUID) (*sharedteamprovision.CreatorContextV1, error) - DeleteUser(ctx context.Context, userID uuid.UUID) error + // PrepareDeleteUser resolves the external identity references for the + // given user so they can be removed after the database rows are gone. + PrepareDeleteUser(ctx context.Context, userID uuid.UUID) (DeleteUserHandle, error) +} + +// DeleteUserHandle holds pre-fetched state needed to finalise user deletion +// after the database rows have been removed. +type DeleteUserHandle interface { + // Execute removes the external identity (e.g. Ory). It must be called + // only after the caller has already deleted the database rows. + Execute(ctx context.Context) error } func NewProvider(mode Mode, supa Provider, ory Provider) (Provider, error) { diff --git a/packages/dashboard-api/internal/userprofile/provider_test.go b/packages/dashboard-api/internal/userprofile/provider_test.go index 8c3552f2e1..11960f3b04 100644 --- a/packages/dashboard-api/internal/userprofile/provider_test.go +++ b/packages/dashboard-api/internal/userprofile/provider_test.go @@ -23,8 +23,8 @@ func (f *fakeProvider) GetTeamCreatorContext(_ context.Context, _ uuid.UUID) (*s return nil, nil } -func (f *fakeProvider) DeleteUser(_ context.Context, _ uuid.UUID) error { - return nil +func (f *fakeProvider) PrepareDeleteUser(_ context.Context, _ uuid.UUID) (DeleteUserHandle, error) { + return nil, nil } func TestNewProvider_FactorySelection(t *testing.T) { diff --git a/packages/dashboard-api/internal/userprofile/supabase.go b/packages/dashboard-api/internal/userprofile/supabase.go index 68c6eb9d28..63bac74e3b 100644 --- a/packages/dashboard-api/internal/userprofile/supabase.go +++ b/packages/dashboard-api/internal/userprofile/supabase.go @@ -66,8 +66,12 @@ func (p *supabaseProvider) FindProfilesByEmail(ctx context.Context, email string return profiles, nil } -func (p *supabaseProvider) DeleteUser(_ context.Context, _ uuid.UUID) error { - return errors.New("user deletion is not supported in supabase mode") +type noopDeleteHandle struct{} + +func (noopDeleteHandle) Execute(_ context.Context) error { return nil } + +func (p *supabaseProvider) PrepareDeleteUser(_ context.Context, _ uuid.UUID) (DeleteUserHandle, error) { + return nil, errors.New("user deletion is not supported in supabase mode") } func (p *supabaseProvider) GetTeamCreatorContext(ctx context.Context, userID uuid.UUID) (*sharedteamprovision.CreatorContextV1, error) { From 751beda9f1eb86b3b5372c3b962e74c5daf2b313 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:42:07 +0000 Subject: [PATCH 05/11] fix: remove unused noopDeleteHandle (lint) Co-Authored-By: ben@e2b.dev --- packages/dashboard-api/internal/userprofile/supabase.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/dashboard-api/internal/userprofile/supabase.go b/packages/dashboard-api/internal/userprofile/supabase.go index 63bac74e3b..76099aa5e9 100644 --- a/packages/dashboard-api/internal/userprofile/supabase.go +++ b/packages/dashboard-api/internal/userprofile/supabase.go @@ -66,10 +66,6 @@ func (p *supabaseProvider) FindProfilesByEmail(ctx context.Context, email string return profiles, nil } -type noopDeleteHandle struct{} - -func (noopDeleteHandle) Execute(_ context.Context) error { return nil } - func (p *supabaseProvider) PrepareDeleteUser(_ context.Context, _ uuid.UUID) (DeleteUserHandle, error) { return nil, errors.New("user deletion is not supported in supabase mode") } From 95b7223a4d9fedc4125581f4b49fcc14a1bc4ea6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:58:19 +0000 Subject: [PATCH 06/11] fix: add retry logic for identity deletion + proper error responses - Retry Ory identity deletion up to 3 times with backoff since DB rows are already gone at that point. - Return 404 when user not found (prepare or DB delete). - Return 409 when FK constraint prevents deletion (e.g. addons). - Return descriptive error when identity deletion fails after retries. Co-Authored-By: ben@e2b.dev --- .../internal/handlers/admin_users_delete.go | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/dashboard-api/internal/handlers/admin_users_delete.go b/packages/dashboard-api/internal/handlers/admin_users_delete.go index c11fd6aa4e..16ae7172e5 100644 --- a/packages/dashboard-api/internal/handlers/admin_users_delete.go +++ b/packages/dashboard-api/internal/handlers/admin_users_delete.go @@ -1,15 +1,20 @@ package handlers import ( + "fmt" "net/http" + "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "github.com/e2b-dev/infra/packages/dashboard-api/internal/api" + "github.com/e2b-dev/infra/packages/db/pkg/dberrors" "github.com/e2b-dev/infra/packages/shared/pkg/logger" ) +const identityDeleteMaxRetries = 3 + func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { ctx := c.Request.Context() @@ -17,7 +22,7 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { handle, err := s.userProfiles.PrepareDeleteUser(ctx, userId) if err != nil { logger.L().Error(ctx, "failed to prepare user deletion", zap.String("user_id", userId.String()), zap.Error(err)) - s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to prepare user deletion") + s.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("User %s not found or has no identity provider record", userId)) return } @@ -25,6 +30,18 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { // Delete from public.users (cascades to user_identities via FK). // Done before the IdP removal so a DB failure does not orphan the identity. if err := s.authDB.Write.DeletePublicUser(ctx, userId); err != nil { + if dberrors.IsNotFoundError(err) { + s.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("User %s not found", userId)) + return + } + + if dberrors.IsForeignKeyViolation(err) { + logger.L().Warn(ctx, "cannot delete user due to existing references", zap.String("user_id", userId.String()), zap.Error(err)) + s.sendAPIStoreError(c, http.StatusConflict, "Cannot delete user: existing references (e.g. addons) must be removed first") + + return + } + logger.L().Error(ctx, "failed to delete public user", zap.String("user_id", userId.String()), zap.Error(err)) s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete public user record") @@ -32,9 +49,27 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { } // Remove the external identity (e.g. Ory) using pre-fetched references. - if err := handle.Execute(ctx); err != nil { - logger.L().Error(ctx, "failed to delete user identity provider record", zap.String("user_id", userId.String()), zap.Error(err)) - s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to delete user identity provider record") + // Retry since the DB rows are already gone and we must not leave the IdP identity active. + var identityErr error + for attempt := range identityDeleteMaxRetries { + identityErr = handle.Execute(ctx) + if identityErr == nil { + break + } + + logger.L().Warn(ctx, "retrying identity deletion", + zap.String("user_id", userId.String()), + zap.Int("attempt", attempt+1), + zap.Error(identityErr), + ) + + time.Sleep(time.Duration(attempt+1) * 200 * time.Millisecond) + } + + if identityErr != nil { + logger.L().Error(ctx, "failed to delete user identity provider record after retries", zap.String("user_id", userId.String()), zap.Error(identityErr)) + s.sendAPIStoreError(c, http.StatusInternalServerError, + "User DB records deleted but identity provider removal failed — the IdP identity may need manual cleanup") return } From 6c8d4c1ce0d7c464eb5820bd9a815c2d395dac77 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:02:13 +0000 Subject: [PATCH 07/11] fix: add blank line before return (nlreturn lint) Co-Authored-By: ben@e2b.dev --- packages/dashboard-api/internal/handlers/admin_users_delete.go | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dashboard-api/internal/handlers/admin_users_delete.go b/packages/dashboard-api/internal/handlers/admin_users_delete.go index 16ae7172e5..f0ba2a5a5c 100644 --- a/packages/dashboard-api/internal/handlers/admin_users_delete.go +++ b/packages/dashboard-api/internal/handlers/admin_users_delete.go @@ -32,6 +32,7 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { if err := s.authDB.Write.DeletePublicUser(ctx, userId); err != nil { if dberrors.IsNotFoundError(err) { s.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("User %s not found", userId)) + return } From 84e09a0476c0814e0e96dbd66c43eca243722f9c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:34:20 +0000 Subject: [PATCH 08/11] fix: return 500 for PrepareDeleteUser server errors + add 409 to OpenAPI spec - PrepareDeleteUser failures now correctly return 500 (not 404) since the error is a server/DB issue, not a missing user. - Added 409 Conflict response to the OpenAPI spec for the delete user route (returned when FK constraints prevent deletion). - Regenerated api.gen.go. Co-Authored-By: ben@e2b.dev --- .../dashboard-api/internal/api/api.gen.go | 144 +++++++++--------- .../internal/handlers/admin_users_delete.go | 2 +- spec/openapi-dashboard.yml | 8 + 3 files changed, 83 insertions(+), 71 deletions(-) diff --git a/packages/dashboard-api/internal/api/api.gen.go b/packages/dashboard-api/internal/api/api.gen.go index 0f0ed70ea7..1d20284fb7 100644 --- a/packages/dashboard-api/internal/api/api.gen.go +++ b/packages/dashboard-api/internal/api/api.gen.go @@ -502,6 +502,9 @@ type N403 = Error // N404 defines model for 404. type N404 = Error +// N409 defines model for 409. +type N409 = Error + // N500 defines model for 500. type N500 = Error @@ -1349,76 +1352,77 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", + "AQAA//8=", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/packages/dashboard-api/internal/handlers/admin_users_delete.go b/packages/dashboard-api/internal/handlers/admin_users_delete.go index f0ba2a5a5c..e7c8d82c8d 100644 --- a/packages/dashboard-api/internal/handlers/admin_users_delete.go +++ b/packages/dashboard-api/internal/handlers/admin_users_delete.go @@ -22,7 +22,7 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { handle, err := s.userProfiles.PrepareDeleteUser(ctx, userId) if err != nil { logger.L().Error(ctx, "failed to prepare user deletion", zap.String("user_id", userId.String()), zap.Error(err)) - s.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("User %s not found or has no identity provider record", userId)) + s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to resolve identity provider record for user") return } diff --git a/spec/openapi-dashboard.yml b/spec/openapi-dashboard.yml index ce20a18515..675d266819 100644 --- a/spec/openapi-dashboard.yml +++ b/spec/openapi-dashboard.yml @@ -218,6 +218,12 @@ components: application/json: schema: $ref: "#/components/schemas/Error" + "409": + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "500": description: Server error content: @@ -1220,6 +1226,8 @@ paths: $ref: "#/components/responses/401" "404": $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" "500": $ref: "#/components/responses/500" From 8e49de62ea2d1031452df87533e22b440d072846 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:36:28 +0000 Subject: [PATCH 09/11] fix: return 404 when user has no identity mapping, 500 for server errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PrepareDeleteUser now returns ErrUserNotFound (→ 404) when the user_identities lookup yields zero rows. Other errors (DB/connection) still surface as 500. Co-Authored-By: ben@e2b.dev --- .../dashboard-api/internal/handlers/admin_users_delete.go | 8 ++++++++ packages/dashboard-api/internal/userprofile/ory.go | 4 ++++ packages/dashboard-api/internal/userprofile/provider.go | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/packages/dashboard-api/internal/handlers/admin_users_delete.go b/packages/dashboard-api/internal/handlers/admin_users_delete.go index e7c8d82c8d..db4c72dc31 100644 --- a/packages/dashboard-api/internal/handlers/admin_users_delete.go +++ b/packages/dashboard-api/internal/handlers/admin_users_delete.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "fmt" "net/http" "time" @@ -9,6 +10,7 @@ import ( "go.uber.org/zap" "github.com/e2b-dev/infra/packages/dashboard-api/internal/api" + "github.com/e2b-dev/infra/packages/dashboard-api/internal/userprofile" "github.com/e2b-dev/infra/packages/db/pkg/dberrors" "github.com/e2b-dev/infra/packages/shared/pkg/logger" ) @@ -21,6 +23,12 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { // Resolve the external identity references while user_identities still exists. handle, err := s.userProfiles.PrepareDeleteUser(ctx, userId) if err != nil { + if errors.Is(err, userprofile.ErrUserNotFound) { + s.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("User %s not found or has no identity provider record", userId)) + + return + } + logger.L().Error(ctx, "failed to prepare user deletion", zap.String("user_id", userId.String()), zap.Error(err)) s.sendAPIStoreError(c, http.StatusInternalServerError, "Failed to resolve identity provider record for user") diff --git a/packages/dashboard-api/internal/userprofile/ory.go b/packages/dashboard-api/internal/userprofile/ory.go index 52690dfd99..522cfa8c1e 100644 --- a/packages/dashboard-api/internal/userprofile/ory.go +++ b/packages/dashboard-api/internal/userprofile/ory.go @@ -189,6 +189,10 @@ func (p *oryProvider) PrepareDeleteUser(ctx context.Context, userID uuid.UUID) ( return nil, fmt.Errorf("lookup ory subject for user: %w", err) } + if len(subjectsByUser) == 0 { + return nil, fmt.Errorf("%w: no identity mapping for user %s", ErrUserNotFound, userID) + } + subjects := make([]string, 0, len(subjectsByUser)) for s := range subjectsByUser { subjects = append(subjects, s) diff --git a/packages/dashboard-api/internal/userprofile/provider.go b/packages/dashboard-api/internal/userprofile/provider.go index c873e07d19..5874b627aa 100644 --- a/packages/dashboard-api/internal/userprofile/provider.go +++ b/packages/dashboard-api/internal/userprofile/provider.go @@ -2,6 +2,7 @@ package userprofile import ( "context" + "errors" "fmt" "github.com/google/uuid" @@ -9,6 +10,9 @@ import ( sharedteamprovision "github.com/e2b-dev/infra/packages/shared/pkg/teamprovision" ) +// ErrUserNotFound is returned when the requested user has no identity mapping. +var ErrUserNotFound = errors.New("user not found") + type Profile struct { UserID uuid.UUID Email string From 006226a544c5f59de72a2061a539384fce340820 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Jun 2026 19:38:48 +0000 Subject: [PATCH 10/11] chore: auto-commit generated changes --- packages/dashboard-api/internal/userprofile/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard-api/internal/userprofile/provider.go b/packages/dashboard-api/internal/userprofile/provider.go index 5874b627aa..64e90ff156 100644 --- a/packages/dashboard-api/internal/userprofile/provider.go +++ b/packages/dashboard-api/internal/userprofile/provider.go @@ -10,7 +10,7 @@ import ( sharedteamprovision "github.com/e2b-dev/infra/packages/shared/pkg/teamprovision" ) -// ErrUserNotFound is returned when the requested user has no identity mapping. +// ErrUserNotFound is returned when the requested user has no identity mapping. var ErrUserNotFound = errors.New("user not found") type Profile struct { From f6284abb175ba79e89bf966fba6916cd1fc26bb3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:57:38 +0000 Subject: [PATCH 11/11] fix: use detached context for IdP cleanup after DB delete After the irreversible DB delete, the Ory identity cleanup now runs with context.WithoutCancel so a client disconnect does not abort the retries. Bounded by a 30s timeout. Co-Authored-By: ben@e2b.dev --- .../dashboard-api/internal/handlers/admin_users_delete.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/dashboard-api/internal/handlers/admin_users_delete.go b/packages/dashboard-api/internal/handlers/admin_users_delete.go index db4c72dc31..41e8be8daa 100644 --- a/packages/dashboard-api/internal/handlers/admin_users_delete.go +++ b/packages/dashboard-api/internal/handlers/admin_users_delete.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "errors" "fmt" "net/http" @@ -59,9 +60,13 @@ func (s *APIStore) DeleteAdminUsersUserId(c *gin.Context, userId api.UserId) { // Remove the external identity (e.g. Ory) using pre-fetched references. // Retry since the DB rows are already gone and we must not leave the IdP identity active. + // Use a detached context so a client disconnect does not cancel the cleanup. + cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cleanupCancel() + var identityErr error for attempt := range identityDeleteMaxRetries { - identityErr = handle.Execute(ctx) + identityErr = handle.Execute(cleanupCtx) if identityErr == nil { break }