Skip to content

feat: Allow adding users as workspace owners via API #208

@Christ-Roy

Description

@Christ-Roy

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:

  1. Add a user as owner directly when they exist
  2. Promote a member to owner (without using TransferOwnership which 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 needed

Testing

Tested with existing users in workspace testazernew:

  • inviteMember adds user directly (has user_id)
  • ❌ Role is hardcoded to "member"
  • setUserPermissions doesn'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.go
  • internal/domain/workspace.go

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions