DeleteWorkspace soft-deletes a workspace. SaaS only. Cancels any active Stripe subscription before deleting, then switches to the next available workspace and returns new auth tokens.
(ctx context.Context, req *connect.Request[v1pb.DeleteWorkspaceRequest])
| 209 | // Cancels any active Stripe subscription before deleting, then switches |
| 210 | // to the next available workspace and returns new auth tokens. |
| 211 | func (s *WorkspaceService) DeleteWorkspace(ctx context.Context, req *connect.Request[v1pb.DeleteWorkspaceRequest]) (*connect.Response[v1pb.LoginResponse], error) { |
| 212 | if !s.profile.SaaS { |
| 213 | return nil, connect.NewError(connect.CodeFailedPrecondition, errors.New("workspace deletion is only supported in SaaS mode")) |
| 214 | } |
| 215 | |
| 216 | user, ok := GetUserFromContext(ctx) |
| 217 | if !ok || user == nil { |
| 218 | return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user not found")) |
| 219 | } |
| 220 | |
| 221 | workspaceID, err := common.GetWorkspaceID(req.Msg.Name) |
| 222 | if err != nil { |
| 223 | return nil, connect.NewError(connect.CodeInvalidArgument, err) |
| 224 | } |
| 225 | if contextWorkspaceID := common.GetWorkspaceIDFromContext(ctx); workspaceID != contextWorkspaceID { |
| 226 | return nil, connect.NewError(connect.CodePermissionDenied, errors.Errorf("cannot delete workspace %q from workspace %q", workspaceID, contextWorkspaceID)) |
| 227 | } |
| 228 | |
| 229 | // Cancel active Stripe subscription if any. |
| 230 | sub, err := s.store.GetSubscriptionByWorkspace(ctx, workspaceID) |
| 231 | if err != nil { |
| 232 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to check subscription")) |
| 233 | } |
| 234 | if sub != nil && sub.Payload != nil && sub.Payload.StripeSubscriptionId != "" { |
| 235 | if _, err := stripeplugin.CancelSubscription(sub.Payload.StripeSubscriptionId, workspaceID, true, "other", "Delete workspace"); err != nil { |
| 236 | slog.Warn("failed to cancel Stripe subscription during workspace deletion", |
| 237 | slog.String("workspace", workspaceID), |
| 238 | log.BBError(err), |
| 239 | ) |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | if err := s.store.DeleteWorkspace(ctx, workspaceID); err != nil { |
| 244 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to delete workspace")) |
| 245 | } |
| 246 | |
| 247 | // Find the next workspace and switch to it. |
| 248 | nextWS, err := s.store.FindWorkspace(ctx, &store.FindWorkspaceMessage{ |
| 249 | Email: user.Email, |
| 250 | }) |
| 251 | if err != nil { |
| 252 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to find next workspace")) |
| 253 | } |
| 254 | if nextWS == nil { |
| 255 | // No remaining workspace — clear auth cookies and return empty response. |
| 256 | // Frontend redirects to login which will provision a new workspace. |
| 257 | resp := connect.NewResponse(&v1pb.LoginResponse{}) |
| 258 | s.authService.clearSessionAndSetCookies(ctx, req.Header(), resp.Header(), workspaceID) |
| 259 | return resp, nil |
| 260 | } |
| 261 | |
| 262 | isWeb := auth.GetRefreshTokenFromCookie(req.Header()) != "" |
| 263 | return s.authService.switchWorkspaceInternal(ctx, user, nextWS.ResourceID, isWeb, req.Header()) |
| 264 | } |
| 265 | |
| 266 | // LeaveWorkspace removes the calling user from a workspace's IAM bindings, |
| 267 | // then switches to the next available workspace. |
nothing calls this directly
no test coverage detected