-
Notifications
You must be signed in to change notification settings - Fork 156
Description
Allow adding users as workspace owners via API
Context
Currently, when using POST /api/workspaces.inviteMember to add a user to a workspace, the role is hardcoded to "member" (see workspace_service.go:650), even when providing full owner permissions.
This creates a limitation for programmatic workspace provisioning where we need to automatically add users as owners without requiring email invitation acceptance + manual upgrade.
Current Behavior
For existing users
POST /api/workspaces.inviteMember
{
"workspace_id": "myworkspace",
"email": "user@example.com",
"permissions": { /* full permissions */ }
}Result: User is added directly with role = "member" (line 650 in workspace_service.go)
For new users
Result: Invitation sent by email, user becomes "member" after accepting
Problem
There is no API endpoint to:
- Add a user as owner directly when they exist
- Promote a member to owner (without using
TransferOwnershipwhich demotes the current owner)
Attempted Workarounds
- ❌
setUserPermissions: Only updates permissions, not the role - ❌
TransferOwnership: Exists in service but not exposed via HTTP, and it demotes the current owner (not suitable for multi-owner workspaces) - ✅ Direct DB UPDATE: Works but bypasses API validation
Proposed Solutions
Option 1: Add optional role parameter to inviteMember (Recommended)
Pros:
- Backward compatible (defaults to "member")
- Uses existing endpoint
- Simple implementation
Implementation:
// workspace_service.go
func (s *WorkspaceService) InviteMember(ctx context.Context, workspaceID, email string, permissions domain.UserPermissions, role string) (*domain.WorkspaceInvitation, string, error) {
// ...existing code...
// Default role to "member" if not specified
if role == "" {
role = "member"
}
// Validate role
if role != "member" && role != "owner" {
return nil, "", fmt.Errorf("invalid role: must be 'member' or 'owner'")
}
// Line 647: Use the provided role instead of hardcoded "member"
userWorkspace := &domain.UserWorkspace{
UserID: existingUser.ID,
WorkspaceID: workspaceID,
Role: role, // ✅ Use parameter
Permissions: permissions,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
// ...
}HTTP Handler:
// workspace_handler.go
type InviteMemberRequest struct {
WorkspaceID string `json:"workspace_id"`
Email string `json:"email"`
Permissions domain.UserPermissions `json:"permissions"`
Role string `json:"role,omitempty"` // ✅ Optional, defaults to "member"
}Option 2: Create new endpoint POST /api/workspaces.promoteToOwner
Pros:
- Explicit API
- No breaking changes
- Clear intent
Implementation:
// workspace_service.go
func (s *WorkspaceService) PromoteToOwner(ctx context.Context, workspaceID, userID string) error {
// ... auth checks ...
targetUserWorkspace, err := s.repo.GetUserWorkspace(ctx, userID, workspaceID)
if err != nil {
return fmt.Errorf("user is not a member of the workspace")
}
if targetUserWorkspace.Role == "owner" {
return nil // Already owner
}
targetUserWorkspace.Role = "owner"
targetUserWorkspace.Permissions = domain.FullPermissions
targetUserWorkspace.UpdatedAt = time.Now().UTC()
return s.repo.AddUserToWorkspace(ctx, targetUserWorkspace)
}Use Case
Automated SaaS provisioning (e.g., Web-Dashboard provisioning Notifuse workspaces):
// 1. Root admin creates workspace
const workspace = await createWorkspace(workspaceId);
// 2. Invite user AS OWNER immediately
await inviteMember(workspaceId, userEmail, fullPermissions, 'owner');
// OR with Option 2:
// await inviteMember(workspaceId, userEmail, fullPermissions);
// await promoteToOwner(workspaceId, userId);
// ✅ User has immediate owner access (if existing user)
// ✅ No email acceptance required
// ✅ No database bypass neededTesting
Tested with existing users in workspace testazernew:
- ✅
inviteMemberadds user directly (has user_id) - ❌ Role is hardcoded to "member"
- ❌
setUserPermissionsdoesn't change role - ✅ Direct DB UPDATE works:
UPDATE user_workspaces SET role = 'owner'
Recommendation
Option 1 (add role parameter to inviteMember) is preferred because:
- Backward compatible
- Single API call
- Consistent with existing patterns
- Minimal code changes
Related Files
internal/service/workspace_service.go(lines 589-704)internal/http/workspace_handler.gointernal/domain/workspace.go