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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions